From 7074e45ec34aee588f9daf8f6cb960a1d5e7c9ef Mon Sep 17 00:00:00 2001 From: OpenHands Date: Fri, 15 Nov 2024 19:41:48 -0500 Subject: [PATCH 01/56] Fix issue #5059: [Bug]: Github resolver looking for wrong PR number (#5062) Co-authored-by: Graham Neubig --- openhands/resolver/issue_definitions.py | 41 +++++--- .../test_issue_handler_error_handling.py | 94 +++++++++++++++++++ tests/unit/resolver/test_issue_references.py | 34 +++++++ 3 files changed, 157 insertions(+), 12 deletions(-) create mode 100644 tests/unit/resolver/test_issue_handler_error_handling.py create mode 100644 tests/unit/resolver/test_issue_references.py diff --git a/openhands/resolver/issue_definitions.py b/openhands/resolver/issue_definitions.py index 2d40923ca568..10a134b812cf 100644 --- a/openhands/resolver/issue_definitions.py +++ b/openhands/resolver/issue_definitions.py @@ -83,7 +83,21 @@ def _extract_image_urls(self, issue_body: str) -> list[str]: return re.findall(image_pattern, issue_body) def _extract_issue_references(self, body: str) -> list[int]: - pattern = r'#(\d+)' + # First, remove code blocks as they may contain false positives + body = re.sub(r'```.*?```', '', body, flags=re.DOTALL) + + # Remove inline code + body = re.sub(r'`[^`]*`', '', body) + + # Remove URLs that contain hash symbols + body = re.sub(r'https?://[^\s)]*#\d+[^\s)]*', '', body) + + # Now extract issue numbers, making sure they're not part of other text + # The pattern matches #number that: + # 1. Is at the start of text or after whitespace/punctuation + # 2. Is followed by whitespace, punctuation, or end of text + # 3. Is not part of a URL + pattern = r'(?:^|[\s\[({]|[^\w#])#(\d+)(?=[\s,.\])}]|$)' return [int(match) for match in re.findall(pattern, body)] def _get_issue_comments( @@ -455,17 +469,20 @@ def __get_context_from_external_issues_references( ) for issue_number in unique_issue_references: - url = f'https://api.github.com/repos/{self.owner}/{self.repo}/issues/{issue_number}' - headers = { - 'Authorization': f'Bearer {self.token}', - 'Accept': 'application/vnd.github.v3+json', - } - response = requests.get(url, headers=headers) - response.raise_for_status() - issue_data = response.json() - issue_body = issue_data.get('body', '') - if issue_body: - closing_issues.append(issue_body) + try: + url = f'https://api.github.com/repos/{self.owner}/{self.repo}/issues/{issue_number}' + headers = { + 'Authorization': f'Bearer {self.token}', + 'Accept': 'application/vnd.github.v3+json', + } + response = requests.get(url, headers=headers) + response.raise_for_status() + issue_data = response.json() + issue_body = issue_data.get('body', '') + if issue_body: + closing_issues.append(issue_body) + except requests.exceptions.RequestException as e: + logger.warning(f'Failed to fetch issue {issue_number}: {str(e)}') return closing_issues diff --git a/tests/unit/resolver/test_issue_handler_error_handling.py b/tests/unit/resolver/test_issue_handler_error_handling.py new file mode 100644 index 000000000000..54adff3466fd --- /dev/null +++ b/tests/unit/resolver/test_issue_handler_error_handling.py @@ -0,0 +1,94 @@ +import pytest +import requests +from unittest.mock import patch, MagicMock + +from openhands.resolver.issue_definitions import PRHandler +from openhands.resolver.github_issue import ReviewThread + + +def test_handle_nonexistent_issue_reference(): + handler = PRHandler("test-owner", "test-repo", "test-token") + + # Mock the requests.get to simulate a 404 error + mock_response = MagicMock() + mock_response.raise_for_status.side_effect = requests.exceptions.HTTPError("404 Client Error: Not Found") + + with patch('requests.get', return_value=mock_response): + # Call the method with a non-existent issue reference + result = handler._PRHandler__get_context_from_external_issues_references( + closing_issues=[], + closing_issue_numbers=[], + issue_body="This references #999999", # Non-existent issue + review_comments=[], + review_threads=[], + thread_comments=None + ) + + # The method should return an empty list since the referenced issue couldn't be fetched + assert result == [] + + +def test_handle_rate_limit_error(): + handler = PRHandler("test-owner", "test-repo", "test-token") + + # Mock the requests.get to simulate a rate limit error + mock_response = MagicMock() + mock_response.raise_for_status.side_effect = requests.exceptions.HTTPError( + "403 Client Error: Rate Limit Exceeded" + ) + + with patch('requests.get', return_value=mock_response): + # Call the method with an issue reference + result = handler._PRHandler__get_context_from_external_issues_references( + closing_issues=[], + closing_issue_numbers=[], + issue_body="This references #123", + review_comments=[], + review_threads=[], + thread_comments=None + ) + + # The method should return an empty list since the request was rate limited + assert result == [] + + +def test_handle_network_error(): + handler = PRHandler("test-owner", "test-repo", "test-token") + + # Mock the requests.get to simulate a network error + with patch('requests.get', side_effect=requests.exceptions.ConnectionError("Network Error")): + # Call the method with an issue reference + result = handler._PRHandler__get_context_from_external_issues_references( + closing_issues=[], + closing_issue_numbers=[], + issue_body="This references #123", + review_comments=[], + review_threads=[], + thread_comments=None + ) + + # The method should return an empty list since the network request failed + assert result == [] + + +def test_successful_issue_reference(): + handler = PRHandler("test-owner", "test-repo", "test-token") + + # Mock a successful response + mock_response = MagicMock() + mock_response.raise_for_status.return_value = None + mock_response.json.return_value = {"body": "This is the referenced issue body"} + + with patch('requests.get', return_value=mock_response): + # Call the method with an issue reference + result = handler._PRHandler__get_context_from_external_issues_references( + closing_issues=[], + closing_issue_numbers=[], + issue_body="This references #123", + review_comments=[], + review_threads=[], + thread_comments=None + ) + + # The method should return a list with the referenced issue body + assert result == ["This is the referenced issue body"] \ No newline at end of file diff --git a/tests/unit/resolver/test_issue_references.py b/tests/unit/resolver/test_issue_references.py new file mode 100644 index 000000000000..e4da644983db --- /dev/null +++ b/tests/unit/resolver/test_issue_references.py @@ -0,0 +1,34 @@ +from openhands.resolver.issue_definitions import IssueHandler + + +def test_extract_issue_references(): + handler = IssueHandler("test-owner", "test-repo", "test-token") + + # Test basic issue reference + assert handler._extract_issue_references("Fixes #123") == [123] + + # Test multiple issue references + assert handler._extract_issue_references("Fixes #123, #456") == [123, 456] + + # Test issue references in code blocks should be ignored + assert handler._extract_issue_references(""" + Here's a code block: + ```python + # This is a comment with #123 + def func(): + pass # Another #456 + ``` + But this #789 should be extracted + """) == [789] + + # Test issue references in inline code should be ignored + assert handler._extract_issue_references("This `#123` should be ignored but #456 should be extracted") == [456] + + # Test issue references in URLs should be ignored + assert handler._extract_issue_references("Check http://example.com/#123 but #456 should be extracted") == [456] + + # Test issue references in markdown links should be extracted + assert handler._extract_issue_references("[Link to #123](http://example.com) and #456") == [123, 456] + + # Test issue references with text around them + assert handler._extract_issue_references("Issue #123 is fixed and #456 is pending") == [123, 456] From 2b7932b46c37aec05e867e5a4b7f724e41c6f89a Mon Sep 17 00:00:00 2001 From: OpenHands Date: Fri, 15 Nov 2024 20:43:49 -0500 Subject: [PATCH 02/56] Fix issue #5070: [Bug]: lint-fix workflow is failing (#5078) --- .github/workflows/lint-fix.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint-fix.yml b/.github/workflows/lint-fix.yml index a882d6802946..78fdef9afcfb 100644 --- a/.github/workflows/lint-fix.yml +++ b/.github/workflows/lint-fix.yml @@ -43,7 +43,7 @@ jobs: run: pip install pre-commit==3.7.0 - name: Fix python lint issues run: | - pre-commit run --files openhands/**/* evaluation/**/* tests/**/* --config ./dev_config/python/.pre-commit-config.yaml --all-files + pre-commit run --files openhands/**/* evaluation/**/* tests/**/* --config ./dev_config/python/.pre-commit-config.yaml # Commit and push changes if any - name: Check for changes From f7652bd55889de52dcfef07c8a03cb4515b3bebb Mon Sep 17 00:00:00 2001 From: OpenHands Date: Sat, 16 Nov 2024 00:55:41 -0500 Subject: [PATCH 03/56] Fix issue #5080: [Bug]: lint-fix.yml github action doesn't work on a branch not from this repo (#5081) --- .github/workflows/lint-fix.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/lint-fix.yml b/.github/workflows/lint-fix.yml index 78fdef9afcfb..e3021b887c53 100644 --- a/.github/workflows/lint-fix.yml +++ b/.github/workflows/lint-fix.yml @@ -16,6 +16,7 @@ jobs: - uses: actions/checkout@v4 with: ref: ${{ github.head_ref }} + repository: ${{ github.event.pull_request.head.repo.full_name }} fetch-depth: 0 token: ${{ secrets.GITHUB_TOKEN }} From 9d47ddba38d27b1222df795cfd96285c69609f45 Mon Sep 17 00:00:00 2001 From: "sp.wack" <83104063+amanape@users.noreply.github.com> Date: Sat, 16 Nov 2024 07:57:41 +0200 Subject: [PATCH 04/56] Reduce output from frontend tests (#5023) --- frontend/vite.config.ts | 1 + frontend/vitest.setup.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 4c403aca8496..6ec8467fc2c7 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -91,6 +91,7 @@ export default defineConfig(({ mode }) => { test: { environment: "jsdom", setupFiles: ["vitest.setup.ts"], + reporters: "basic", exclude: [...configDefaults.exclude, "tests"], coverage: { reporter: ["text", "json", "html", "lcov", "text-summary"], diff --git a/frontend/vitest.setup.ts b/frontend/vitest.setup.ts index 9ef8e081682e..5ee3071b3ea6 100644 --- a/frontend/vitest.setup.ts +++ b/frontend/vitest.setup.ts @@ -13,7 +13,7 @@ vi.mock("react-i18next", async (importOriginal) => ({ })); // Mock requests during tests -beforeAll(() => server.listen()); +beforeAll(() => server.listen({ onUnhandledRequest: "bypass" })); afterEach(() => { server.resetHandlers(); // Cleanup the document body after each test From 97f3249205e25abafc13e2c42037dd9b309fbe73 Mon Sep 17 00:00:00 2001 From: "Ryan H. Tran" Date: Sat, 16 Nov 2024 12:58:26 +0700 Subject: [PATCH 05/56] Move linter and diff utils to openhands-aci (#5020) --- openhands/linter/__init__.py | 8 +- openhands/linter/base.py | 79 ---- openhands/linter/languages/python.py | 98 ----- openhands/linter/languages/treesitter.py | 74 ---- openhands/linter/linter.py | 122 ------ openhands/linter/utils/__init__.py | 3 - openhands/linter/utils/cmd.py | 37 -- openhands/runtime/utils/edit.py | 9 +- openhands/utils/diff.py | 41 -- poetry.lock | 13 +- pyproject.toml | 2 +- tests/runtime/test_edit.py | 8 +- tests/unit/linters/conftest.py | 75 ---- tests/unit/linters/test_lint_diff.py | 417 ------------------- tests/unit/linters/test_python_linter.py | 84 ---- tests/unit/linters/test_treesitter_linter.py | 113 ----- tests/unit/linters/test_visualize.py | 86 ---- 17 files changed, 18 insertions(+), 1251 deletions(-) delete mode 100644 openhands/linter/base.py delete mode 100644 openhands/linter/languages/python.py delete mode 100644 openhands/linter/languages/treesitter.py delete mode 100644 openhands/linter/linter.py delete mode 100644 openhands/linter/utils/__init__.py delete mode 100644 openhands/linter/utils/cmd.py delete mode 100644 openhands/utils/diff.py delete mode 100644 tests/unit/linters/conftest.py delete mode 100644 tests/unit/linters/test_lint_diff.py delete mode 100644 tests/unit/linters/test_python_linter.py delete mode 100644 tests/unit/linters/test_treesitter_linter.py delete mode 100644 tests/unit/linters/test_visualize.py diff --git a/openhands/linter/__init__.py b/openhands/linter/__init__.py index 43d3b07d7d01..23e5d0de6e48 100644 --- a/openhands/linter/__init__.py +++ b/openhands/linter/__init__.py @@ -1,9 +1,11 @@ """Linter module for OpenHands. -Part of this Linter module is adapted from Aider (Apache 2.0 License, [original code](https://github.com/paul-gauthier/aider/blob/main/aider/linter.py)). Please see the [original repository](https://github.com/paul-gauthier/aider) for more information. +Part of this Linter module is adapted from Aider (Apache 2.0 License, [original +code](https://github.com/paul-gauthier/aider/blob/main/aider/linter.py)). +- Please see the [original repository](https://github.com/paul-gauthier/aider) for more information. +- The detailed implementation of the linter can be found at: https://github.com/All-Hands-AI/openhands-aci. """ -from openhands.linter.base import LintResult -from openhands.linter.linter import DefaultLinter +from openhands_aci.linter import DefaultLinter, LintResult __all__ = ['DefaultLinter', 'LintResult'] diff --git a/openhands/linter/base.py b/openhands/linter/base.py deleted file mode 100644 index e50b750b4880..000000000000 --- a/openhands/linter/base.py +++ /dev/null @@ -1,79 +0,0 @@ -from abc import ABC, abstractmethod - -from pydantic import BaseModel - - -class LintResult(BaseModel): - file: str - line: int # 1-indexed - column: int # 1-indexed - message: str - - def visualize(self, half_window: int = 3) -> str: - """Visualize the lint result by print out all the lines where the lint result is found. - - Args: - half_window: The number of context lines to display around the error on each side. - """ - with open(self.file, 'r') as f: - file_lines = f.readlines() - - # Add line numbers - _span_size = len(str(len(file_lines))) - file_lines = [ - f'{i + 1:>{_span_size}}|{line.rstrip()}' - for i, line in enumerate(file_lines) - ] - - # Get the window of lines to display - assert self.line <= len(file_lines) and self.line > 0 - line_idx = self.line - 1 - begin_window = max(0, line_idx - half_window) - end_window = min(len(file_lines), line_idx + half_window + 1) - - selected_lines = file_lines[begin_window:end_window] - line_idx_in_window = line_idx - begin_window - - # Add character hint - _character_hint = ( - _span_size * ' ' - + ' ' * (self.column) - + '^' - + ' ERROR HERE: ' - + self.message - ) - selected_lines[line_idx_in_window] = ( - f'\033[91m{selected_lines[line_idx_in_window]}\033[0m' - + '\n' - + _character_hint - ) - return '\n'.join(selected_lines) - - -class LinterException(Exception): - """Base class for all linter exceptions.""" - - pass - - -class BaseLinter(ABC): - """Base class for all linters. - - Each linter should be able to lint files of a specific type and return a list of (parsed) lint results. - """ - - encoding: str = 'utf-8' - - @property - @abstractmethod - def supported_extensions(self) -> list[str]: - """The file extensions that this linter supports, such as .py or .tsx.""" - return [] - - @abstractmethod - def lint(self, file_path: str) -> list[LintResult]: - """Lint the given file. - - file_path: The path to the file to lint. Required to be absolute. - """ - pass diff --git a/openhands/linter/languages/python.py b/openhands/linter/languages/python.py deleted file mode 100644 index 9b7e944a2868..000000000000 --- a/openhands/linter/languages/python.py +++ /dev/null @@ -1,98 +0,0 @@ -from typing import List - -from openhands.core.logger import openhands_logger as logger -from openhands.linter.base import BaseLinter, LintResult -from openhands.linter.utils import run_cmd - - -def python_compile_lint(fname: str) -> list[LintResult]: - try: - with open(fname, 'r') as f: - code = f.read() - compile(code, fname, 'exec') # USE TRACEBACK BELOW HERE - return [] - except SyntaxError as err: - err_lineno = getattr(err, 'end_lineno', err.lineno) - err_offset = getattr(err, 'end_offset', err.offset) - if err_offset and err_offset < 0: - err_offset = err.offset - return [ - LintResult( - file=fname, line=err_lineno, column=err_offset or 1, message=err.msg - ) - ] - - -def flake_lint(filepath: str) -> list[LintResult]: - fatal = 'F821,F822,F831,E112,E113,E999,E902' - flake8_cmd = f'flake8 --select={fatal} --isolated {filepath}' - - try: - cmd_outputs = run_cmd(flake8_cmd) - except FileNotFoundError: - return [] - results: list[LintResult] = [] - if not cmd_outputs: - return results - for line in cmd_outputs.splitlines(): - parts = line.split(':') - if len(parts) >= 4: - _msg = parts[3].strip() - if len(parts) > 4: - _msg += ': ' + parts[4].strip() - - try: - line_num = int(parts[1]) - except ValueError as e: - logger.warning( - f'Error parsing flake8 output for line: {e}. Parsed parts: {parts}. Skipping...' - ) - continue - - try: - column_num = int(parts[2]) - except ValueError as e: - column_num = 1 - _msg = ( - parts[2].strip() + ' ' + _msg - ) # add the unparsed message to the original message - logger.warning( - f'Error parsing flake8 output for column: {e}. Parsed parts: {parts}. Using default column 1.' - ) - - results.append( - LintResult( - file=filepath, - line=line_num, - column=column_num, - message=_msg, - ) - ) - return results - - -class PythonLinter(BaseLinter): - @property - def supported_extensions(self) -> List[str]: - return ['.py'] - - def lint(self, file_path: str) -> list[LintResult]: - error = flake_lint(file_path) - if not error: - error = python_compile_lint(file_path) - return error - - def compile_lint(self, file_path: str, code: str) -> List[LintResult]: - try: - compile(code, file_path, 'exec') - return [] - except SyntaxError as e: - return [ - LintResult( - file=file_path, - line=e.lineno, - column=e.offset, - message=str(e), - rule='SyntaxError', - ) - ] diff --git a/openhands/linter/languages/treesitter.py b/openhands/linter/languages/treesitter.py deleted file mode 100644 index 83b5d466aecc..000000000000 --- a/openhands/linter/languages/treesitter.py +++ /dev/null @@ -1,74 +0,0 @@ -import warnings - -from grep_ast import TreeContext, filename_to_lang -from grep_ast.parsers import PARSERS -from tree_sitter_languages import get_parser - -from openhands.linter.base import BaseLinter, LintResult - -# tree_sitter is throwing a FutureWarning -warnings.simplefilter('ignore', category=FutureWarning) - - -def tree_context(fname, code, line_nums): - context = TreeContext( - fname, - code, - color=False, - line_number=True, - child_context=False, - last_line=False, - margin=0, - mark_lois=True, - loi_pad=3, - # header_max=30, - show_top_of_file_parent_scope=False, - ) - line_nums = set(line_nums) - context.add_lines_of_interest(line_nums) - context.add_context() - output = context.format() - return output - - -def traverse_tree(node): - """Traverses the tree to find errors.""" - errors = [] - if node.type == 'ERROR' or node.is_missing: - line_no = node.start_point[0] + 1 - col_no = node.start_point[1] + 1 - error_type = 'Missing node' if node.is_missing else 'Syntax error' - errors.append((line_no, col_no, error_type)) - - for child in node.children: - errors += traverse_tree(child) - - return errors - - -class TreesitterBasicLinter(BaseLinter): - @property - def supported_extensions(self) -> list[str]: - return list(PARSERS.keys()) - - def lint(self, file_path: str) -> list[LintResult]: - """Use tree-sitter to look for syntax errors, display them with tree context.""" - lang = filename_to_lang(file_path) - if not lang: - return [] - parser = get_parser(lang) - with open(file_path, 'r') as f: - code = f.read() - tree = parser.parse(bytes(code, 'utf-8')) - errors = traverse_tree(tree.root_node) - if not errors: - return [] - return [ - LintResult( - file=file_path, - line=int(line), - column=int(col), - message=error_details, - ) - for line, col, error_details in errors - ] diff --git a/openhands/linter/linter.py b/openhands/linter/linter.py deleted file mode 100644 index a7a4a23ca2c1..000000000000 --- a/openhands/linter/linter.py +++ /dev/null @@ -1,122 +0,0 @@ -import os -from collections import defaultdict -from difflib import SequenceMatcher - -from openhands.linter.base import BaseLinter, LinterException, LintResult -from openhands.linter.languages.python import PythonLinter -from openhands.linter.languages.treesitter import TreesitterBasicLinter - - -class DefaultLinter(BaseLinter): - def __init__(self): - self.linters: dict[str, list[BaseLinter]] = defaultdict(list) - self.linters['.py'] = [PythonLinter()] - - # Add treesitter linter as a fallback for all linters - self.basic_linter = TreesitterBasicLinter() - for extension in self.basic_linter.supported_extensions: - self.linters[extension].append(self.basic_linter) - self._supported_extensions = list(self.linters.keys()) - - @property - def supported_extensions(self) -> list[str]: - return self._supported_extensions - - def lint(self, file_path: str) -> list[LintResult]: - if not os.path.isabs(file_path): - raise LinterException(f'File path {file_path} is not an absolute path') - file_extension = os.path.splitext(file_path)[1] - - linters: list[BaseLinter] = self.linters.get(file_extension, []) - for linter in linters: - res = linter.lint(file_path) - # We always return the first linter's result (higher priority) - if res: - return res - return [] - - def lint_file_diff( - self, original_file_path: str, updated_file_path: str - ) -> list[LintResult]: - """Only return lint errors that are introduced by the diff. - - Args: - original_file_path: The original file path. - updated_file_path: The updated file path. - - Returns: - A list of lint errors that are introduced by the diff. - """ - # 1. Lint the original and updated file - original_lint_errors: list[LintResult] = self.lint(original_file_path) - updated_lint_errors: list[LintResult] = self.lint(updated_file_path) - - # 2. Load the original and updated file content - with open(original_file_path, 'r') as f: - old_lines = f.readlines() - with open(updated_file_path, 'r') as f: - new_lines = f.readlines() - - # 3. Get line numbers that are changed & unchanged - # Map the line number of the original file to the updated file - # NOTE: this only works for lines that are not changed (i.e., equal) - old_to_new_line_no_mapping: dict[int, int] = {} - replace_or_inserted_lines: list[int] = [] - for ( - tag, - old_idx_start, - old_idx_end, - new_idx_start, - new_idx_end, - ) in SequenceMatcher( - isjunk=None, - a=old_lines, - b=new_lines, - ).get_opcodes(): - if tag == 'equal': - for idx, _ in enumerate(old_lines[old_idx_start:old_idx_end]): - old_to_new_line_no_mapping[old_idx_start + idx + 1] = ( - new_idx_start + idx + 1 - ) - elif tag == 'replace' or tag == 'insert': - for idx, _ in enumerate(old_lines[old_idx_start:old_idx_end]): - replace_or_inserted_lines.append(new_idx_start + idx + 1) - else: - # omit the case of delete - pass - - # 4. Get pre-existing errors in unchanged lines - # increased error elsewhere introduced by the newlines - # i.e., we omit errors that are already in original files and report new one - new_line_no_to_original_errors: dict[int, list[LintResult]] = defaultdict(list) - for error in original_lint_errors: - if error.line in old_to_new_line_no_mapping: - new_line_no_to_original_errors[ - old_to_new_line_no_mapping[error.line] - ].append(error) - - # 5. Select errors from lint results in new file to report - selected_errors = [] - for error in updated_lint_errors: - # 5.1. Error introduced by replace/insert - if error.line in replace_or_inserted_lines: - selected_errors.append(error) - # 5.2. Error introduced by modified lines that impacted - # the unchanged lines that HAVE pre-existing errors - elif error.line in new_line_no_to_original_errors: - # skip if the error is already reported - # or add if the error is new - if not any( - original_error.message == error.message - and original_error.column == error.column - for original_error in new_line_no_to_original_errors[error.line] - ): - selected_errors.append(error) - # 5.3. Error introduced by modified lines that impacted - # the unchanged lines that have NO pre-existing errors - else: - selected_errors.append(error) - - # 6. Sort errors by line and column - selected_errors.sort(key=lambda x: (x.line, x.column)) - return selected_errors diff --git a/openhands/linter/utils/__init__.py b/openhands/linter/utils/__init__.py deleted file mode 100644 index e48f26f076b5..000000000000 --- a/openhands/linter/utils/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .cmd import check_tool_installed, run_cmd - -__all__ = ['run_cmd', 'check_tool_installed'] diff --git a/openhands/linter/utils/cmd.py b/openhands/linter/utils/cmd.py deleted file mode 100644 index f5c2803c3d77..000000000000 --- a/openhands/linter/utils/cmd.py +++ /dev/null @@ -1,37 +0,0 @@ -import os -import subprocess - - -def run_cmd(cmd: str, cwd: str | None = None) -> str | None: - """Run a command and return the output. - - If the command succeeds, return None. If the command fails, return the stdout. - """ - - process = subprocess.Popen( - cmd.split(), - cwd=cwd, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - encoding='utf-8', - errors='replace', - ) - stdout, _ = process.communicate() - if process.returncode == 0: - return None - return stdout - - -def check_tool_installed(tool_name: str) -> bool: - """Check if a tool is installed.""" - try: - subprocess.run( - [tool_name, '--version'], - check=True, - cwd=os.getcwd(), - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - return True - except (subprocess.CalledProcessError, FileNotFoundError): - return False diff --git a/openhands/runtime/utils/edit.py b/openhands/runtime/utils/edit.py index cd3ffd0b71ce..bcb876f865d9 100644 --- a/openhands/runtime/utils/edit.py +++ b/openhands/runtime/utils/edit.py @@ -4,13 +4,11 @@ import tempfile from abc import ABC, abstractmethod +from openhands_aci.utils.diff import get_diff + from openhands.core.config import AppConfig from openhands.core.logger import openhands_logger as logger -from openhands.events.action import ( - FileEditAction, - FileReadAction, - FileWriteAction, -) +from openhands.events.action import FileEditAction, FileReadAction, FileWriteAction from openhands.events.observation import ( ErrorObservation, FileEditObservation, @@ -22,7 +20,6 @@ from openhands.llm.llm import LLM from openhands.llm.metrics import Metrics from openhands.utils.chunk_localizer import Chunk, get_top_k_chunk_matches -from openhands.utils.diff import get_diff SYS_MSG = """Your job is to produce a new version of the file based on the old version and the provided draft of the new version. The provided draft may be incomplete (it may skip lines) and/or incorrectly indented. You should try to apply the changes present in the draft to the old version, and output a new version of the file. diff --git a/openhands/utils/diff.py b/openhands/utils/diff.py deleted file mode 100644 index 71d00a2eb943..000000000000 --- a/openhands/utils/diff.py +++ /dev/null @@ -1,41 +0,0 @@ -import difflib - -import whatthepatch - - -def get_diff(old_contents: str, new_contents: str, filepath: str = 'file') -> str: - diff = list( - difflib.unified_diff( - old_contents.split('\n'), - new_contents.split('\n'), - fromfile=filepath, - tofile=filepath, - # do not output unchange lines - # because they can cause `parse_diff` to fail - n=0, - ) - ) - return '\n'.join(map(lambda x: x.rstrip(), diff)) - - -def parse_diff(diff_patch: str) -> list[whatthepatch.patch.Change]: - # handle empty patch - if diff_patch.strip() == '': - return [] - - patch = whatthepatch.parse_patch(diff_patch) - patch_list = list(patch) - assert len(patch_list) == 1, ( - 'parse_diff only supports single file diff. But got:\nPATCH:\n' - + diff_patch - + '\nPATCH LIST:\n' - + str(patch_list) - ) - changes = patch_list[0].changes - - # ignore changes that are the same (i.e., old_lineno == new_lineno) - output_changes = [] - for change in changes: - if change.old != change.new: - output_changes.append(change) - return output_changes diff --git a/poetry.lock b/poetry.lock index 80d6b20882c8..70d888cafed6 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. [[package]] name = "aenum" @@ -5629,7 +5629,6 @@ optional = false python-versions = ">=3.6" files = [ {file = "opencv-python-4.10.0.84.tar.gz", hash = "sha256:72d234e4582e9658ffea8e9cae5b63d488ad06994ef12d81dc303b17472f3526"}, - {file = "opencv_python-4.10.0.84-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:fc182f8f4cda51b45f01c64e4cbedfc2f00aff799debebc305d8d0210c43f251"}, {file = "opencv_python-4.10.0.84-cp37-abi3-macosx_12_0_x86_64.whl", hash = "sha256:71e575744f1d23f79741450254660442785f45a0797212852ee5199ef12eed98"}, {file = "opencv_python-4.10.0.84-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09a332b50488e2dda866a6c5573ee192fe3583239fb26ff2f7f9ceb0bc119ea6"}, {file = "opencv_python-4.10.0.84-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ace140fc6d647fbe1c692bcb2abce768973491222c067c131d80957c595b71f"}, @@ -5642,17 +5641,18 @@ numpy = {version = ">=1.26.0", markers = "python_version >= \"3.12\""} [[package]] name = "openhands-aci" -version = "0.1.0" +version = "0.1.1" description = "An Agent-Computer Interface (ACI) designed for software development agents OpenHands." optional = false python-versions = "<4.0,>=3.12" files = [ - {file = "openhands_aci-0.1.0-py3-none-any.whl", hash = "sha256:f28e5a32e394d1e643f79bf8af27fe44d039cb71729d590f9f3ee0c23c075f00"}, - {file = "openhands_aci-0.1.0.tar.gz", hash = "sha256:babc55f516efbb27eb7e528662e14b75c902965c48a110408fda824b83ea4461"}, + {file = "openhands_aci-0.1.1-py3-none-any.whl", hash = "sha256:8831f97b887571005dca0d70a9f6f0a4f9feb35d3d41f499e70d72b5fb68a599"}, + {file = "openhands_aci-0.1.1.tar.gz", hash = "sha256:705b74a12a8f428e64295b5de125f553500f62ef5ab3a5a6284d8fcf638025e6"}, ] [package.dependencies] diskcache = ">=5.6.3,<6.0.0" +flake8 = "*" gitpython = "*" grep-ast = "0.3.3" litellm = "*" @@ -5661,6 +5661,7 @@ numpy = "*" pandas = "*" scipy = "*" tree-sitter = "0.21.3" +whatthepatch = ">=1.0.6,<2.0.0" [[package]] name = "opentelemetry-api" @@ -10211,4 +10212,4 @@ testing = ["coverage[toml]", "zope.event", "zope.testing"] [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "8718ffe2ed836fca6c646c37bdad2c9c8e63ebd7ec881f420148fef5095d19e4" +content-hash = "b710448cff0788b563f4d7614fca438ab0b9fe19903a061750012c56da95ff37" diff --git a/pyproject.toml b/pyproject.toml index ce49694e14aa..1caad8bf9fc7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,7 +63,7 @@ opentelemetry-exporter-otlp-proto-grpc = "1.25.0" modal = "^0.64.145" runloop-api-client = "0.7.0" pygithub = "^2.5.0" -openhands-aci = "^0.1.0" +openhands-aci = "^0.1.1" [tool.poetry.group.llama-index.dependencies] llama-index = "*" diff --git a/tests/runtime/test_edit.py b/tests/runtime/test_edit.py index b1508af226c1..99a7ce113b18 100644 --- a/tests/runtime/test_edit.py +++ b/tests/runtime/test_edit.py @@ -3,16 +3,12 @@ import os import pytest -from conftest import ( - TEST_IN_CI, - _close_test_runtime, - _load_runtime, -) +from conftest import TEST_IN_CI, _close_test_runtime, _load_runtime +from openhands_aci.utils.diff import get_diff from openhands.core.logger import openhands_logger as logger from openhands.events.action import FileEditAction, FileReadAction from openhands.events.observation import FileEditObservation -from openhands.utils.diff import get_diff ORGINAL = """from flask import Flask app = Flask(__name__) diff --git a/tests/unit/linters/conftest.py b/tests/unit/linters/conftest.py deleted file mode 100644 index 4a2b51812bb9..000000000000 --- a/tests/unit/linters/conftest.py +++ /dev/null @@ -1,75 +0,0 @@ -import pytest - - -@pytest.fixture -def syntax_error_py_file(tmp_path): - file_content = """ - def foo(): - print("Hello, World!") - print("Wrong indent") - foo( - """ - file_path = tmp_path / 'test_file.py' - file_path.write_text(file_content) - return str(file_path) - - -@pytest.fixture -def wrongly_indented_py_file(tmp_path): - file_content = """ - def foo(): - print("Hello, World!") - """ - file_path = tmp_path / 'test_file.py' - file_path.write_text(file_content) - return str(file_path) - - -@pytest.fixture -def simple_correct_py_file(tmp_path): - file_content = 'print("Hello, World!")\n' - file_path = tmp_path / 'test_file.py' - file_path.write_text(file_content) - return str(file_path) - - -@pytest.fixture -def simple_correct_py_func_def(tmp_path): - file_content = """def foo(): - print("Hello, World!") -foo() -""" - file_path = tmp_path / 'test_file.py' - file_path.write_text(file_content) - return str(file_path) - - -@pytest.fixture -def simple_correct_ruby_file(tmp_path): - file_content = """def foo - puts "Hello, World!" -end -foo -""" - file_path = tmp_path / 'test_file.rb' - file_path.write_text(file_content) - return str(file_path) - - -@pytest.fixture -def simple_incorrect_ruby_file(tmp_path): - file_content = """def foo(): - print("Hello, World!") -foo() -""" - file_path = tmp_path / 'test_file.rb' - file_path.write_text(file_content) - return str(file_path) - - -@pytest.fixture -def parenthesis_incorrect_ruby_file(tmp_path): - file_content = """def print_hello_world()\n puts 'Hello World'\n""" - file_path = tmp_path / 'test_file.rb' - file_path.write_text(file_content) - return str(file_path) diff --git a/tests/unit/linters/test_lint_diff.py b/tests/unit/linters/test_lint_diff.py deleted file mode 100644 index f3b560c3df32..000000000000 --- a/tests/unit/linters/test_lint_diff.py +++ /dev/null @@ -1,417 +0,0 @@ -from openhands.linter import DefaultLinter, LintResult -from openhands.utils.diff import get_diff, parse_diff - -OLD_CONTENT = """ -def foo(): - print("Hello, World!") - x = UNDEFINED_VARIABLE -foo() -""" - -NEW_CONTENT_V1 = ( - OLD_CONTENT - + """ -def new_function_that_causes_error(): - y = ANOTHER_UNDEFINED_VARIABLE -""" -) - -NEW_CONTENT_V2 = """ -def foo(): - print("Hello, World!") - x = UNDEFINED_VARIABLE - y = ANOTHER_UNDEFINED_VARIABLE -foo() -""" - - -def test_get_and_parse_diff(tmp_path): - diff = get_diff(OLD_CONTENT, NEW_CONTENT_V1, 'test.py') - print(diff) - assert ( - diff - == """ ---- test.py -+++ test.py -@@ -6,0 +7,3 @@ -+def new_function_that_causes_error(): -+ y = ANOTHER_UNDEFINED_VARIABLE -+ -""".strip() - ) - - print( - '\n'.join( - [f'{i+1}|{line}' for i, line in enumerate(NEW_CONTENT_V1.splitlines())] - ) - ) - changes = parse_diff(diff) - assert len(changes) == 3 - assert ( - changes[0].old is None - and changes[0].new == 7 - and changes[0].line == 'def new_function_that_causes_error():' - ) - assert ( - changes[1].old is None - and changes[1].new == 8 - and changes[1].line == ' y = ANOTHER_UNDEFINED_VARIABLE' - ) - assert changes[2].old is None and changes[2].new == 9 and changes[2].line == '' - - -def test_lint_with_diff_append(tmp_path): - with open(tmp_path / 'old.py', 'w') as f: - f.write(OLD_CONTENT) - with open(tmp_path / 'new.py', 'w') as f: - f.write(NEW_CONTENT_V1) - - linter = DefaultLinter() - result: list[LintResult] = linter.lint_file_diff( - str(tmp_path / 'old.py'), - str(tmp_path / 'new.py'), - ) - print(result) - assert len(result) == 1 - assert ( - result[0].line == 8 - and result[0].column == 9 - and result[0].message == "F821 undefined name 'ANOTHER_UNDEFINED_VARIABLE'" - ) - - -def test_lint_with_diff_insert(tmp_path): - with open(tmp_path / 'old.py', 'w') as f: - f.write(OLD_CONTENT) - with open(tmp_path / 'new.py', 'w') as f: - f.write(NEW_CONTENT_V2) - - linter = DefaultLinter() - result: list[LintResult] = linter.lint_file_diff( - str(tmp_path / 'old.py'), - str(tmp_path / 'new.py'), - ) - assert len(result) == 1 - assert ( - result[0].line == 5 - and result[0].column == 9 - and result[0].message == "F821 undefined name 'ANOTHER_UNDEFINED_VARIABLE'" - ) - - -def test_lint_with_multiple_changes_and_errors(tmp_path): - old_content = """ -def foo(): - print("Hello, World!") - x = 10 -foo() -""" - new_content = """ -def foo(): - print("Hello, World!") - x = UNDEFINED_VARIABLE - y = 20 - -def bar(): - z = ANOTHER_UNDEFINED_VARIABLE - return z + 1 - -foo() -bar() -""" - with open(tmp_path / 'old.py', 'w') as f: - f.write(old_content) - with open(tmp_path / 'new.py', 'w') as f: - f.write(new_content) - - linter = DefaultLinter() - result: list[LintResult] = linter.lint_file_diff( - str(tmp_path / 'old.py'), - str(tmp_path / 'new.py'), - ) - assert len(result) == 2 - assert ( - result[0].line == 4 - and result[0].column == 9 - and result[0].message == "F821 undefined name 'UNDEFINED_VARIABLE'" - ) - assert ( - result[1].line == 8 - and result[1].column == 9 - and result[1].message == "F821 undefined name 'ANOTHER_UNDEFINED_VARIABLE'" - ) - - -def test_lint_with_introduced_and_fixed_errors(tmp_path): - old_content = """ -x = UNDEFINED_VARIABLE -y = 10 -""" - new_content = """ -x = 5 -y = ANOTHER_UNDEFINED_VARIABLE -z = UNDEFINED_VARIABLE -""" - with open(tmp_path / 'old.py', 'w') as f: - f.write(old_content) - with open(tmp_path / 'new.py', 'w') as f: - f.write(new_content) - - linter = DefaultLinter() - result: list[LintResult] = linter.lint_file_diff( - str(tmp_path / 'old.py'), - str(tmp_path / 'new.py'), - ) - assert len(result) == 2 - assert ( - result[0].line == 3 - and result[0].column == 5 - and result[0].message == "F821 undefined name 'ANOTHER_UNDEFINED_VARIABLE'" - ) - assert ( - result[1].line == 4 - and result[1].column == 5 - and result[1].message == "F821 undefined name 'UNDEFINED_VARIABLE'" - ) - - -def test_lint_with_multiline_changes(tmp_path): - old_content = """ -def complex_function(a, b, c): - return (a + - b + - c) -""" - new_content = """ -def complex_function(a, b, c): - return (a + - UNDEFINED_VARIABLE + - b + - c) -""" - with open(tmp_path / 'old.py', 'w') as f: - f.write(old_content) - with open(tmp_path / 'new.py', 'w') as f: - f.write(new_content) - - linter = DefaultLinter() - result: list[LintResult] = linter.lint_file_diff( - str(tmp_path / 'old.py'), - str(tmp_path / 'new.py'), - ) - assert len(result) == 1 - assert ( - result[0].line == 4 - and result[0].column == 13 - and result[0].message == "F821 undefined name 'UNDEFINED_VARIABLE'" - ) - - -def test_lint_with_syntax_error(tmp_path): - old_content = """ -def foo(): - print("Hello, World!") -""" - new_content = """ -def foo(): - print("Hello, World!" -""" - with open(tmp_path / 'old.py', 'w') as f: - f.write(old_content) - with open(tmp_path / 'new.py', 'w') as f: - f.write(new_content) - - linter = DefaultLinter() - result: list[LintResult] = linter.lint_file_diff( - str(tmp_path / 'old.py'), - str(tmp_path / 'new.py'), - ) - assert len(result) == 1 - assert ( - result[0].line == 3 - and result[0].column == 11 - and result[0].message == "E999 SyntaxError: '(' was never closed" - ) - - -def test_lint_with_docstring_changes(tmp_path): - old_content = ''' -def foo(): - """This is a function.""" - print("Hello, World!") -''' - new_content = ''' -def foo(): - """ - This is a function. - It now has a multi-line docstring with an UNDEFINED_VARIABLE. - """ - print("Hello, World!") -''' - with open(tmp_path / 'old.py', 'w') as f: - f.write(old_content) - with open(tmp_path / 'new.py', 'w') as f: - f.write(new_content) - - linter = DefaultLinter() - result: list[LintResult] = linter.lint_file_diff( - str(tmp_path / 'old.py'), - str(tmp_path / 'new.py'), - ) - assert len(result) == 0 # Linter should ignore changes in docstrings - - -def test_lint_with_multiple_errors_on_same_line(tmp_path): - old_content = """ -def foo(): - print("Hello, World!") - x = 10 -foo() -""" - new_content = """ -def foo(): - print("Hello, World!") - x = UNDEFINED_VARIABLE + ANOTHER_UNDEFINED_VARIABLE -foo() -""" - with open(tmp_path / 'old.py', 'w') as f: - f.write(old_content) - with open(tmp_path / 'new.py', 'w') as f: - f.write(new_content) - - linter = DefaultLinter() - result: list[LintResult] = linter.lint_file_diff( - str(tmp_path / 'old.py'), - str(tmp_path / 'new.py'), - ) - print(result) - assert len(result) == 2 - assert ( - result[0].line == 4 - and result[0].column == 9 - and result[0].message == "F821 undefined name 'UNDEFINED_VARIABLE'" - ) - assert ( - result[1].line == 4 - and result[1].column == 30 - and result[1].message == "F821 undefined name 'ANOTHER_UNDEFINED_VARIABLE'" - ) - - -def test_parse_diff_with_empty_patch(): - diff_patch = '' - changes = parse_diff(diff_patch) - assert len(changes) == 0 - - -def test_lint_file_diff_ignore_existing_errors(tmp_path): - """ - Make sure we allow edits as long as it does not introduce new errors. In other - words, we don't care about existing linting errors. Although they might be - real syntax issues, sometimes they are just false positives, or errors that - we don't care about. - """ - content = """def some_valid_but_weird_function(): - # this function is legitimate, yet static analysis tools like flake8 - # reports 'F821 undefined name' - if 'variable' in locals(): - print(variable) -def some_wrong_but_unused_function(): - # this function has a linting error, but it is not modified by us, and - # who knows, this function might be completely dead code - x = 1 -def sum(a, b): - return a - b -""" - new_content = content.replace(' return a - b', ' return a + b') - temp_file_old_path = tmp_path / 'problematic-file-test.py' - temp_file_old_path.write_text(content) - temp_file_new_path = tmp_path / 'problematic-file-test-new.py' - temp_file_new_path.write_text(new_content) - - linter = DefaultLinter() - result: list[LintResult] = linter.lint_file_diff( - str(temp_file_old_path), - str(temp_file_new_path), - ) - assert len(result) == 0 # no new errors introduced - - -def test_lint_file_diff_catch_new_errors_in_edits(tmp_path): - """ - Make sure we catch new linting errors in our edit chunk, and at the same - time, ignore old linting errors (in this case, the old linting error is - a false positive) - """ - content = """def some_valid_but_weird_function(): - # this function is legitimate, yet static analysis tools like flake8 - # reports 'F821 undefined name' - if 'variable' in locals(): - print(variable) -def sum(a, b): - return a - b -""" - - temp_file_old_path = tmp_path / 'problematic-file-test.py' - temp_file_old_path.write_text(content) - new_content = content.replace(' return a - b', ' return a + variable') - temp_file_new_path = tmp_path / 'problematic-file-test-new.py' - temp_file_new_path.write_text(new_content) - - linter = DefaultLinter() - result: list[LintResult] = linter.lint_file_diff( - str(temp_file_old_path), - str(temp_file_new_path), - ) - print(result) - assert len(result) == 1 - assert ( - result[0].line == 7 - and result[0].column == 16 - and result[0].message == "F821 undefined name 'variable'" - ) - - -def test_lint_file_diff_catch_new_errors_outside_edits(tmp_path): - """ - Make sure we catch new linting errors induced by our edits, even - though the error itself is not in the edit chunk - """ - content = """def valid_func1(): - print(my_sum(1, 2)) -def my_sum(a, b): - return a - b -def valid_func2(): - print(my_sum(0, 0)) -""" - # Add 100 lines of invalid code, which linter shall ignore - # because they are not being edited. For testing purpose, we - # must add these existing linting errors, otherwise the pre-edit - # linting would pass, and thus there won't be any comparison - # between pre-edit and post-edit linting. - for _ in range(100): - content += '\ninvalid_func()' - - temp_file_old_path = tmp_path / 'problematic-file-test.py' - temp_file_old_path.write_text(content) - - new_content = content.replace('def my_sum(a, b):', 'def my_sum2(a, b):') - temp_file_new_path = tmp_path / 'problematic-file-test-new.py' - temp_file_new_path.write_text(new_content) - - linter = DefaultLinter() - result: list[LintResult] = linter.lint_file_diff( - str(temp_file_old_path), - str(temp_file_new_path), - ) - assert len(result) == 2 - assert ( - result[0].line == 2 - and result[0].column == 11 - and result[0].message == "F821 undefined name 'my_sum'" - ) - assert ( - result[1].line == 6 - and result[1].column == 11 - and result[1].message == "F821 undefined name 'my_sum'" - ) diff --git a/tests/unit/linters/test_python_linter.py b/tests/unit/linters/test_python_linter.py deleted file mode 100644 index 40aed81ec3f3..000000000000 --- a/tests/unit/linters/test_python_linter.py +++ /dev/null @@ -1,84 +0,0 @@ -from openhands.linter import DefaultLinter, LintResult -from openhands.linter.languages.python import ( - PythonLinter, - flake_lint, - python_compile_lint, -) - - -def test_wrongly_indented_py_file(wrongly_indented_py_file): - # Test Python linter - linter = PythonLinter() - assert '.py' in linter.supported_extensions - result = linter.lint(wrongly_indented_py_file) - print(result) - assert isinstance(result, list) and len(result) == 1 - assert result[0] == LintResult( - file=wrongly_indented_py_file, - line=2, - column=5, - message='E999 IndentationError: unexpected indent', - ) - print(result[0].visualize()) - assert result[0].visualize() == ( - '1|\n' - '\033[91m2| def foo():\033[0m\n' - ' ^ ERROR HERE: E999 IndentationError: unexpected indent\n' - '3| print("Hello, World!")\n' - '4|' - ) - - # General linter should have same result as Python linter - # bc it uses PythonLinter under the hood - general_linter = DefaultLinter() - assert '.py' in general_linter.supported_extensions - result = general_linter.lint(wrongly_indented_py_file) - assert result == linter.lint(wrongly_indented_py_file) - - # Test flake8_lint - assert result == flake_lint(wrongly_indented_py_file) - - # Test python_compile_lint - compile_result = python_compile_lint(wrongly_indented_py_file) - assert isinstance(compile_result, list) and len(compile_result) == 1 - assert compile_result[0] == LintResult( - file=wrongly_indented_py_file, line=2, column=4, message='unexpected indent' - ) - - -def test_simple_correct_py_file(simple_correct_py_file): - linter = PythonLinter() - assert '.py' in linter.supported_extensions - result = linter.lint(simple_correct_py_file) - assert result == [] - - general_linter = DefaultLinter() - assert '.py' in general_linter.supported_extensions - result = general_linter.lint(simple_correct_py_file) - assert result == linter.lint(simple_correct_py_file) - - # Test python_compile_lint - compile_result = python_compile_lint(simple_correct_py_file) - assert compile_result == [] - - # Test flake_lint - flake_result = flake_lint(simple_correct_py_file) - assert flake_result == [] - - -def test_simple_correct_py_func_def(simple_correct_py_func_def): - linter = PythonLinter() - result = linter.lint(simple_correct_py_func_def) - assert result == [] - - general_linter = DefaultLinter() - assert '.py' in general_linter.supported_extensions - result = general_linter.lint(simple_correct_py_func_def) - assert result == linter.lint(simple_correct_py_func_def) - - # Test flake_lint - assert result == flake_lint(simple_correct_py_func_def) - - # Test python_compile_lint - compile_result = python_compile_lint(simple_correct_py_func_def) - assert compile_result == [] diff --git a/tests/unit/linters/test_treesitter_linter.py b/tests/unit/linters/test_treesitter_linter.py deleted file mode 100644 index 195a48bf3632..000000000000 --- a/tests/unit/linters/test_treesitter_linter.py +++ /dev/null @@ -1,113 +0,0 @@ -from openhands.linter import DefaultLinter, LintResult -from openhands.linter.languages.treesitter import TreesitterBasicLinter - - -def test_syntax_error_py_file(syntax_error_py_file): - linter = TreesitterBasicLinter() - result = linter.lint(syntax_error_py_file) - print(result) - assert isinstance(result, list) and len(result) == 1 - assert result[0] == LintResult( - file=syntax_error_py_file, - line=5, - column=5, - message='Syntax error', - ) - - assert ( - result[0].visualize() - == ( - '2| def foo():\n' - '3| print("Hello, World!")\n' - '4| print("Wrong indent")\n' - '\033[91m5| foo(\033[0m\n' # color red - ' ^ ERROR HERE: Syntax error\n' - '6|' - ) - ) - print(result[0].visualize()) - - general_linter = DefaultLinter() - general_result = general_linter.lint(syntax_error_py_file) - # NOTE: general linter returns different result - # because it uses flake8 first, which is different from treesitter - assert general_result != result - - -def test_simple_correct_ruby_file(simple_correct_ruby_file): - linter = TreesitterBasicLinter() - result = linter.lint(simple_correct_ruby_file) - assert isinstance(result, list) and len(result) == 0 - - # Test that the general linter also returns the same result - general_linter = DefaultLinter() - general_result = general_linter.lint(simple_correct_ruby_file) - assert general_result == result - - -def test_simple_incorrect_ruby_file(simple_incorrect_ruby_file): - linter = TreesitterBasicLinter() - result = linter.lint(simple_incorrect_ruby_file) - print(result) - assert isinstance(result, list) and len(result) == 2 - assert result[0] == LintResult( - file=simple_incorrect_ruby_file, - line=1, - column=1, - message='Syntax error', - ) - print(result[0].visualize()) - assert ( - result[0].visualize() - == ( - '\033[91m1|def foo():\033[0m\n' # color red - ' ^ ERROR HERE: Syntax error\n' - '2| print("Hello, World!")\n' - '3|foo()' - ) - ) - assert result[1] == LintResult( - file=simple_incorrect_ruby_file, - line=1, - column=10, - message='Syntax error', - ) - print(result[1].visualize()) - assert ( - result[1].visualize() - == ( - '\033[91m1|def foo():\033[0m\n' # color red - ' ^ ERROR HERE: Syntax error\n' - '2| print("Hello, World!")\n' - '3|foo()' - ) - ) - - # Test that the general linter also returns the same result - general_linter = DefaultLinter() - general_result = general_linter.lint(simple_incorrect_ruby_file) - assert general_result == result - - -def test_parenthesis_incorrect_ruby_file(parenthesis_incorrect_ruby_file): - linter = TreesitterBasicLinter() - result = linter.lint(parenthesis_incorrect_ruby_file) - print(result) - assert isinstance(result, list) and len(result) == 1 - assert result[0] == LintResult( - file=parenthesis_incorrect_ruby_file, - line=1, - column=1, - message='Syntax error', - ) - print(result[0].visualize()) - assert result[0].visualize() == ( - '\033[91m1|def print_hello_world()\033[0m\n' - ' ^ ERROR HERE: Syntax error\n' - "2| puts 'Hello World'" - ) - - # Test that the general linter also returns the same result - general_linter = DefaultLinter() - general_result = general_linter.lint(parenthesis_incorrect_ruby_file) - assert general_result == result diff --git a/tests/unit/linters/test_visualize.py b/tests/unit/linters/test_visualize.py deleted file mode 100644 index e8232afd0117..000000000000 --- a/tests/unit/linters/test_visualize.py +++ /dev/null @@ -1,86 +0,0 @@ -from unittest.mock import mock_open, patch - -import pytest - -from openhands.linter.base import LintResult - - -@pytest.fixture -def mock_file_content(): - return '\n'.join([f'Line {i}' for i in range(1, 21)]) - - -def test_visualize_standard_case(mock_file_content): - lint_result = LintResult( - file='test_file.py', line=10, column=5, message='Test error message' - ) - - with patch('builtins.open', mock_open(read_data=mock_file_content)): - result = lint_result.visualize(half_window=3) - - expected_output = ( - " 7|Line 7\n" - " 8|Line 8\n" - " 9|Line 9\n" - "\033[91m10|Line 10\033[0m\n" - f" {' ' * lint_result.column}^ ERROR HERE: Test error message\n" - "11|Line 11\n" - "12|Line 12\n" - "13|Line 13" - ) - - assert result == expected_output - - -def test_visualize_small_window(mock_file_content): - lint_result = LintResult( - file='test_file.py', line=10, column=5, message='Test error message' - ) - - with patch('builtins.open', mock_open(read_data=mock_file_content)): - result = lint_result.visualize(half_window=1) - - expected_output = ( - " 9|Line 9\n" - "\033[91m10|Line 10\033[0m\n" - f" {' ' * lint_result.column}^ ERROR HERE: Test error message\n" - "11|Line 11" - ) - - assert result == expected_output - - -def test_visualize_error_at_start(mock_file_content): - lint_result = LintResult( - file='test_file.py', line=1, column=3, message='Start error' - ) - - with patch('builtins.open', mock_open(read_data=mock_file_content)): - result = lint_result.visualize(half_window=2) - - expected_output = ( - "\033[91m 1|Line 1\033[0m\n" - f" {' ' * lint_result.column}^ ERROR HERE: Start error\n" - " 2|Line 2\n" - " 3|Line 3" - ) - - assert result == expected_output - - -def test_visualize_error_at_end(mock_file_content): - lint_result = LintResult( - file='test_file.py', line=20, column=1, message='End error' - ) - - with patch('builtins.open', mock_open(read_data=mock_file_content)): - result = lint_result.visualize(half_window=2) - - expected_output = ( - "18|Line 18\n" - "19|Line 19\n" - "\033[91m20|Line 20\033[0m\n" - f" {' ' * lint_result.column}^ ERROR HERE: End error" - ) - - assert result == expected_output From 104f52bcdd4859c68d241a00d4f46d0bad658572 Mon Sep 17 00:00:00 2001 From: Graham Neubig Date: Sat, 16 Nov 2024 08:10:56 -0500 Subject: [PATCH 06/56] Add a "community" page with maintainer info (#4962) --- COMMUNITY.md | 40 ++++++++++++++++++++++++++++++++++++++++ README.md | 22 ++++++---------------- 2 files changed, 46 insertions(+), 16 deletions(-) create mode 100644 COMMUNITY.md diff --git a/COMMUNITY.md b/COMMUNITY.md new file mode 100644 index 000000000000..ffebf7fd5ae5 --- /dev/null +++ b/COMMUNITY.md @@ -0,0 +1,40 @@ +# 🙌 The OpenHands Community + +The OpenHands community is built around the belief that (1) AI and AI agents are going to fundamentally change the way we build software, and (2) if this is true, we should do everything we can to make sure that the benefits provided by such powerful technology are accessible to everyone. + +If this resonates with you, we'd love to have you join us in our quest! + +## 🤝 How to Join + +We do most of our communication through Slack, so this is the best place to start, but we also are happy to have you contact us on Discord or Github. + +- [Join our Slack workspace](https://join.slack.com/t/openhands-ai/shared_invite/zt-2tom0er4l-JeNUGHt_AxpEfIBstbLPiw) - Here we talk about research, architecture, and future development. +- [Join our Discord server](https://discord.gg/ESHStjSjD4) - This is a community-run server for general discussion, questions, and feedback. +- [Read and post Github Issues](https://github.com/All-Hands-AI/OpenHands/issues) - Check out the issues we're working on, or add your own ideas. + +## 💪 Becoming a Contributor + +We welcome contributions from everyone! Whether you're a developer, a researcher, or simply enthusiastic about advancing the field of software engineering with AI, there are many ways to get involved: + +- **Code Contributions:** Help us develop new core functionality, improve our agents, improve the frontend and other interfaces, or anything else that would help make OpenHands better. +- **Research and Evaluation:** Contribute to our understanding of LLMs in software engineering, participate in evaluating the models, or suggest improvements. +- **Feedback and Testing:** Use the OpenHands toolset, report bugs, suggest features, or provide feedback on usability. + +For details, please check [CONTRIBUTING.md](./CONTRIBUTING.md). + +## Code of Conduct + +We have a [Code of Conduct](./CODE_OF_CONDUCT.md) that we expect all contributors to adhere to. +Long story short, we are aiming for an open, welcoming, diverse, inclusive, and healthy community. +All contributors are expected to contribute to building this sort of community. + +## 🛠️ Becoming a Maintainer + +For contributors who have made significant and sustained contributions to the project, there is a possibility of joining the maintainer team. +The process for this is as follows: + +1. Any contributor who has made sustained and high-quality contributions to the codebase can be nominated by any maintainer. If you feel that you may qualify you can reach out to any of the maintainers that have reviewed your PRs and ask if you can be nominated. +2. Once a maintainer nominates a new maintainer, there will be a discussion period among the maintainers for at least 3 days. +3. If no concerns are raised the nomination will be accepted by acclamation, and if concerns are raised there will be a discussion and possible vote. + +Note that just making many PRs does not immediately imply that you will become a maintainer. We will be looking at sustained high-quality contributions over a period of time, as well as good teamwork and adherence to our [Code of Conduct](./CODE_OF_CONDUCT.md). diff --git a/README.md b/README.md index 48ac4d9986d5..cd35635d56de 100644 --- a/README.md +++ b/README.md @@ -77,25 +77,15 @@ To learn more about the project, and for tips on using OpenHands, There you'll find resources on how to use different LLM providers, troubleshooting resources, and advanced configuration options. -## 🤝 How to Contribute +## 🤝 How to Join the Community -OpenHands is a community-driven project, and we welcome contributions from everyone. -Whether you're a developer, a researcher, or simply enthusiastic about advancing the field of -software engineering with AI, there are many ways to get involved: +OpenHands is a community-driven project, and we welcome contributions from everyone. As a first step, you can: -- **Code Contributions:** Help us develop new agents, core functionality, the frontend and other interfaces, or sandboxing solutions. -- **Research and Evaluation:** Contribute to our understanding of LLMs in software engineering, participate in evaluating the models, or suggest improvements. -- **Feedback and Testing:** Use the OpenHands toolset, report bugs, suggest features, or provide feedback on usability. +- [Join our Slack workspace](https://join.slack.com/t/openhands-ai/shared_invite/zt-2tom0er4l-JeNUGHt_AxpEfIBstbLPiw) - Here we talk about research, architecture, and future development. +- [Join our Discord server](https://discord.gg/ESHStjSjD4) - This is a community-run server for general discussion, questions, and feedback. +- [Read or post Github Issues](https://github.com/All-Hands-AI/OpenHands/issues) - Check out the issues we're working on, or add your own ideas. -For details, please check [CONTRIBUTING.md](./CONTRIBUTING.md). - -## 🤖 Join Our Community - -Whether you're a developer, a researcher, or simply enthusiastic about OpenHands, we'd love to have you in our community. -Let's make software engineering better together! - -- [Slack workspace](https://join.slack.com/t/openhands-ai/shared_invite/zt-2tom0er4l-JeNUGHt_AxpEfIBstbLPiw) - Here we talk about research, architecture, and future development. -- [Discord server](https://discord.gg/ESHStjSjD4) - This is a community-run server for general discussion, questions, and feedback. +See more about the community in [COMMUNITY.md](./COMMUNITY.md) or find details on contributing in [CONTRIBUTING.md](./CONTRIBUTING.md). ## 📈 Progress From 088e895a3d281f0be2cabd3b9270b3faa50fd952 Mon Sep 17 00:00:00 2001 From: Faraz Shamim Date: Sun, 17 Nov 2024 19:35:30 +0545 Subject: [PATCH 07/56] Fix #4997 (#5006) Co-authored-by: Graham Neubig --- .../current/usage/how-to/github-action.md | 2 +- .../current/usage/how-to/github-action.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/i18n/fr/docusaurus-plugin-content-docs/current/usage/how-to/github-action.md b/docs/i18n/fr/docusaurus-plugin-content-docs/current/usage/how-to/github-action.md index 391e1b42aa82..bcbd9107cf67 100644 --- a/docs/i18n/fr/docusaurus-plugin-content-docs/current/usage/how-to/github-action.md +++ b/docs/i18n/fr/docusaurus-plugin-content-docs/current/usage/how-to/github-action.md @@ -14,4 +14,4 @@ Pour utiliser l'Action GitHub OpenHands dans le dépôt OpenHands, un mainteneur ## Installation de l'Action dans un nouveau dépôt -Pour installer l'Action GitHub OpenHands dans votre propre dépôt, suivez les [instructions dans le dépôt OpenHands Resolver](https://github.com/All-Hands-AI/OpenHands-resolver?tab=readme-ov-file#using-the-github-actions-workflow). +Pour installer l'Action GitHub OpenHands dans votre propre dépôt, suivez les [instructions dans le dépôt OpenHands Resolver](https://github.com/All-Hands-AI/OpenHands/blob/main/openhands/resolver/README.md). diff --git a/docs/i18n/zh-Hans/docusaurus-plugin-content-docs/current/usage/how-to/github-action.md b/docs/i18n/zh-Hans/docusaurus-plugin-content-docs/current/usage/how-to/github-action.md index 4b3304ffd202..e5f1f93dcb44 100644 --- a/docs/i18n/zh-Hans/docusaurus-plugin-content-docs/current/usage/how-to/github-action.md +++ b/docs/i18n/zh-Hans/docusaurus-plugin-content-docs/current/usage/how-to/github-action.md @@ -12,4 +12,4 @@ ## 在新仓库中安装 Action -要在你自己的仓库中安装 OpenHands GitHub Action,请按照 [OpenHands Resolver 仓库中的说明](https://github.com/All-Hands-AI/OpenHands-resolver?tab=readme-ov-file#using-the-github-actions-workflow) 进行操作。 +要在你自己的仓库中安装 OpenHands GitHub Action,请按照 [OpenHands Resolver 仓库中的说明](https://github.com/All-Hands-AI/OpenHands/blob/main/openhands/resolver/README.md) 进行操作。 From de821718fd579448150a8a614be9da550fd743bf Mon Sep 17 00:00:00 2001 From: mamoodi Date: Mon, 18 Nov 2024 10:41:56 -0500 Subject: [PATCH 08/56] Use How to join community as reference for slack, discord, issues links (#5097) --- COMMUNITY.md | 29 ++++++++++++++++------------- README.md | 3 ++- docs/modules/usage/about.md | 5 +---- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/COMMUNITY.md b/COMMUNITY.md index ffebf7fd5ae5..6edb4dff311e 100644 --- a/COMMUNITY.md +++ b/COMMUNITY.md @@ -1,23 +1,24 @@ # 🙌 The OpenHands Community -The OpenHands community is built around the belief that (1) AI and AI agents are going to fundamentally change the way we build software, and (2) if this is true, we should do everything we can to make sure that the benefits provided by such powerful technology are accessible to everyone. +The OpenHands community is built around the belief that (1) AI and AI agents are going to fundamentally change the way +we build software, and (2) if this is true, we should do everything we can to make sure that the benefits provided by +such powerful technology are accessible to everyone. If this resonates with you, we'd love to have you join us in our quest! ## 🤝 How to Join -We do most of our communication through Slack, so this is the best place to start, but we also are happy to have you contact us on Discord or Github. - -- [Join our Slack workspace](https://join.slack.com/t/openhands-ai/shared_invite/zt-2tom0er4l-JeNUGHt_AxpEfIBstbLPiw) - Here we talk about research, architecture, and future development. -- [Join our Discord server](https://discord.gg/ESHStjSjD4) - This is a community-run server for general discussion, questions, and feedback. -- [Read and post Github Issues](https://github.com/All-Hands-AI/OpenHands/issues) - Check out the issues we're working on, or add your own ideas. +Check out our [How to Join the Community section.](https://github.com/All-Hands-AI/OpenHands?tab=readme-ov-file#-how-to-join-the-community) ## 💪 Becoming a Contributor -We welcome contributions from everyone! Whether you're a developer, a researcher, or simply enthusiastic about advancing the field of software engineering with AI, there are many ways to get involved: +We welcome contributions from everyone! Whether you're a developer, a researcher, or simply enthusiastic about advancing +the field of software engineering with AI, there are many ways to get involved: -- **Code Contributions:** Help us develop new core functionality, improve our agents, improve the frontend and other interfaces, or anything else that would help make OpenHands better. -- **Research and Evaluation:** Contribute to our understanding of LLMs in software engineering, participate in evaluating the models, or suggest improvements. +- **Code Contributions:** Help us develop new core functionality, improve our agents, improve the frontend and other +interfaces, or anything else that would help make OpenHands better. +- **Research and Evaluation:** Contribute to our understanding of LLMs in software engineering, participate in +evaluating the models, or suggest improvements. - **Feedback and Testing:** Use the OpenHands toolset, report bugs, suggest features, or provide feedback on usability. For details, please check [CONTRIBUTING.md](./CONTRIBUTING.md). @@ -30,11 +31,13 @@ All contributors are expected to contribute to building this sort of community. ## 🛠️ Becoming a Maintainer -For contributors who have made significant and sustained contributions to the project, there is a possibility of joining the maintainer team. -The process for this is as follows: +For contributors who have made significant and sustained contributions to the project, there is a possibility of joining +the maintainer team. The process for this is as follows: -1. Any contributor who has made sustained and high-quality contributions to the codebase can be nominated by any maintainer. If you feel that you may qualify you can reach out to any of the maintainers that have reviewed your PRs and ask if you can be nominated. +1. Any contributor who has made sustained and high-quality contributions to the codebase can be nominated by any +maintainer. If you feel that you may qualify you can reach out to any of the maintainers that have reviewed your PRs and ask if you can be nominated. 2. Once a maintainer nominates a new maintainer, there will be a discussion period among the maintainers for at least 3 days. 3. If no concerns are raised the nomination will be accepted by acclamation, and if concerns are raised there will be a discussion and possible vote. -Note that just making many PRs does not immediately imply that you will become a maintainer. We will be looking at sustained high-quality contributions over a period of time, as well as good teamwork and adherence to our [Code of Conduct](./CODE_OF_CONDUCT.md). +Note that just making many PRs does not immediately imply that you will become a maintainer. We will be looking +at sustained high-quality contributions over a period of time, as well as good teamwork and adherence to our [Code of Conduct](./CODE_OF_CONDUCT.md). diff --git a/README.md b/README.md index cd35635d56de..97de3104472c 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,8 @@ troubleshooting resources, and advanced configuration options. ## 🤝 How to Join the Community -OpenHands is a community-driven project, and we welcome contributions from everyone. As a first step, you can: +OpenHands is a community-driven project, and we welcome contributions from everyone. We do most of our communication +through Slack, so this is the best place to start, but we also are happy to have you contact us on Discord or Github: - [Join our Slack workspace](https://join.slack.com/t/openhands-ai/shared_invite/zt-2tom0er4l-JeNUGHt_AxpEfIBstbLPiw) - Here we talk about research, architecture, and future development. - [Join our Discord server](https://discord.gg/ESHStjSjD4) - This is a community-run server for general discussion, questions, and feedback. diff --git a/docs/modules/usage/about.md b/docs/modules/usage/about.md index 93454bb4d9e8..2732fbeb31b4 100644 --- a/docs/modules/usage/about.md +++ b/docs/modules/usage/about.md @@ -21,9 +21,6 @@ OpenHands is built using a combination of powerful frameworks and libraries, pro Please note that the selection of these technologies is in progress, and additional technologies may be added or existing ones may be removed as the project evolves. We strive to adopt the most suitable and efficient tools to enhance the capabilities of OpenHands. -## Licensing, Contributing, Community Servers +## License Distributed under MIT [License](https://github.com/All-Hands-AI/OpenHands/blob/main/LICENSE). - -For guides on how to contribute to OpenHands, joining our Discord and Slack servers -[check out the OpenHands README.](https://github.com/All-Hands-AI/OpenHands?tab=readme-ov-file#-how-to-contribute) From a87b8599eb0866b874c765f510f2bb196a71591c Mon Sep 17 00:00:00 2001 From: Graham Neubig Date: Mon, 18 Nov 2024 13:38:29 -0500 Subject: [PATCH 09/56] fix: run only linting hooks in lint-fix workflow (#5107) Co-authored-by: openhands --- .github/workflows/lint-fix.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/lint-fix.yml b/.github/workflows/lint-fix.yml index e3021b887c53..b877de6bb4d2 100644 --- a/.github/workflows/lint-fix.yml +++ b/.github/workflows/lint-fix.yml @@ -44,7 +44,11 @@ jobs: run: pip install pre-commit==3.7.0 - name: Fix python lint issues run: | - pre-commit run --files openhands/**/* evaluation/**/* tests/**/* --config ./dev_config/python/.pre-commit-config.yaml + pre-commit run trailing-whitespace --files openhands/**/* evaluation/**/* tests/**/* --config ./dev_config/python/.pre-commit-config.yaml + pre-commit run end-of-file-fixer --files openhands/**/* evaluation/**/* tests/**/* --config ./dev_config/python/.pre-commit-config.yaml + pre-commit run pyproject-fmt --files openhands/**/* evaluation/**/* tests/**/* --config ./dev_config/python/.pre-commit-config.yaml + pre-commit run ruff --files openhands/**/* evaluation/**/* tests/**/* --config ./dev_config/python/.pre-commit-config.yaml + pre-commit run ruff-format --files openhands/**/* evaluation/**/* tests/**/* --config ./dev_config/python/.pre-commit-config.yaml # Commit and push changes if any - name: Check for changes From 6b893863982be9330056c0f11d21a7469bf63635 Mon Sep 17 00:00:00 2001 From: Robert Brennan Date: Mon, 18 Nov 2024 17:34:18 -0500 Subject: [PATCH 10/56] fix 404 issue for /config (#5114) --- frontend/src/api/open-hands.ts | 3 +-- openhands/server/listen.py | 27 ++++++++++----------------- 2 files changed, 11 insertions(+), 19 deletions(-) diff --git a/frontend/src/api/open-hands.ts b/frontend/src/api/open-hands.ts index 072588ce476e..33f08d94f21e 100644 --- a/frontend/src/api/open-hands.ts +++ b/frontend/src/api/open-hands.ts @@ -185,8 +185,7 @@ class OpenHands { } static async getRuntimeId(): Promise<{ runtime_id: string }> { - const response = await request("/api/config"); - const data = await response.json(); + const data = await request("/api/conversation"); return data; } diff --git a/openhands/server/listen.py b/openhands/server/listen.py index c1b178b9c2b7..8724daf1905f 100644 --- a/openhands/server/listen.py +++ b/openhands/server/listen.py @@ -11,7 +11,6 @@ from pathspec import PathSpec from pathspec.patterns import GitWildMatchPattern -from openhands.runtime.impl.remote.remote_runtime import RemoteRuntime from openhands.security.options import SecurityAnalyzers from openhands.server.data_models.feedback import FeedbackDataModel, store_feedback from openhands.server.github import ( @@ -565,27 +564,21 @@ def sanitize_filename(filename): return filename -@app.get('/api/config') +@app.get('/api/conversation') async def get_remote_runtime_config(request: Request): """Retrieve the remote runtime configuration. Currently, this is the runtime ID. """ - try: - runtime = request.state.conversation.runtime - if isinstance(runtime, RemoteRuntime): - return JSONResponse(content={'runtime_id': runtime.runtime_id}) - else: - return JSONResponse( - status_code=status.HTTP_404_NOT_FOUND, - content={'error': 'Runtime ID not available in this environment'}, - ) - except Exception as e: - logger.error(e) - return JSONResponse( - status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, - content={'error': 'Something went wrong'}, - ) + runtime = request.state.conversation.runtime + runtime_id = runtime.runtime_id if hasattr(runtime, 'runtime_id') else None + session_id = runtime.sid if hasattr(runtime, 'sid') else None + return JSONResponse( + content={ + 'runtime_id': runtime_id, + 'session_id': session_id, + } + ) @app.post('/api/upload-files') From c75ca7d9766b0e5aa8fe42286d32b5f775c3cd82 Mon Sep 17 00:00:00 2001 From: Rohit Malhotra Date: Mon, 18 Nov 2024 17:53:46 -0500 Subject: [PATCH 11/56] Bug/resolver context fix (#5115) --- openhands/resolver/issue_definitions.py | 4 +- .../prompts/resolve/basic-followup.jinja | 7 +- tests/unit/resolver/test_resolve_issues.py | 586 +++++++++--------- 3 files changed, 301 insertions(+), 296 deletions(-) diff --git a/openhands/resolver/issue_definitions.py b/openhands/resolver/issue_definitions.py index 10a134b812cf..c9e386178adf 100644 --- a/openhands/resolver/issue_definitions.py +++ b/openhands/resolver/issue_definitions.py @@ -576,9 +576,7 @@ def get_instruction( # Format thread comments if they exist thread_context = '' if issue.thread_comments: - thread_context = '\n\nPR Thread Comments:\n' + '\n---\n'.join( - issue.thread_comments - ) + thread_context = '\n---\n'.join(issue.thread_comments) images.extend(self._extract_image_urls(thread_context)) instruction = template.render( diff --git a/openhands/resolver/prompts/resolve/basic-followup.jinja b/openhands/resolver/prompts/resolve/basic-followup.jinja index cf26d80b3bdd..b7bdc2ae9e18 100644 --- a/openhands/resolver/prompts/resolve/basic-followup.jinja +++ b/openhands/resolver/prompts/resolve/basic-followup.jinja @@ -3,7 +3,7 @@ The feedback may be addressed to specific code files. In this case the file loca Please update the code based on the feedback for the repository in /workspace. An environment has been set up for you to start working. You may assume all necessary tools are installed. -# Issues addressed +# Issues addressed {{ issues }} # Review comments @@ -15,10 +15,13 @@ An environment has been set up for you to start working. You may assume all nece # Review thread files {{ files }} +# PR Thread Comments +{{ thread_context }} + IMPORTANT: You should ONLY interact with the environment provided to you AND NEVER ASK FOR HUMAN HELP. You SHOULD INCLUDE PROPER INDENTATION in your edit commands.{% if repo_instruction %} Some basic information about this repository: {{ repo_instruction }}{% endif %} -When you think you have fixed the issue through code changes, please finish the interaction. \ No newline at end of file +When you think you have fixed the issue through code changes, please finish the interaction. diff --git a/tests/unit/resolver/test_resolve_issues.py b/tests/unit/resolver/test_resolve_issues.py index bcf5f36481e7..6eb3bb9f2767 100644 --- a/tests/unit/resolver/test_resolve_issues.py +++ b/tests/unit/resolver/test_resolve_issues.py @@ -1,57 +1,57 @@ import os import tempfile -import pytest +from unittest.mock import AsyncMock, MagicMock, patch +import pytest -from unittest.mock import AsyncMock, patch, MagicMock +from openhands.core.config import LLMConfig +from openhands.events.action import CmdRunAction +from openhands.events.observation import CmdOutputObservation, NullObservation +from openhands.resolver.github_issue import GithubIssue, ReviewThread from openhands.resolver.issue_definitions import IssueHandler, PRHandler from openhands.resolver.resolve_issue import ( - initialize_runtime, complete_runtime, + initialize_runtime, process_issue, ) -from openhands.resolver.github_issue import GithubIssue, ReviewThread -from openhands.events.action import CmdRunAction -from openhands.events.observation import CmdOutputObservation, NullObservation from openhands.resolver.resolver_output import ResolverOutput -from openhands.core.config import LLMConfig @pytest.fixture def mock_output_dir(): with tempfile.TemporaryDirectory() as temp_dir: - repo_path = os.path.join(temp_dir, "repo") + repo_path = os.path.join(temp_dir, 'repo') # Initialize a GitHub repo in "repo" and add a commit with "README.md" os.makedirs(repo_path) - os.system(f"git init {repo_path}") - readme_path = os.path.join(repo_path, "README.md") - with open(readme_path, "w") as f: - f.write("hello world") - os.system(f"git -C {repo_path} add README.md") + os.system(f'git init {repo_path}') + readme_path = os.path.join(repo_path, 'README.md') + with open(readme_path, 'w') as f: + f.write('hello world') + os.system(f'git -C {repo_path} add README.md') os.system(f"git -C {repo_path} commit -m 'Initial commit'") yield temp_dir @pytest.fixture def mock_subprocess(): - with patch("subprocess.check_output") as mock_check_output: + with patch('subprocess.check_output') as mock_check_output: yield mock_check_output @pytest.fixture def mock_os(): - with patch("os.system") as mock_system, patch("os.path.join") as mock_join: + with patch('os.system') as mock_system, patch('os.path.join') as mock_join: yield mock_system, mock_join @pytest.fixture def mock_prompt_template(): - return "Issue: {{ body }}\n\nPlease fix this issue." + return 'Issue: {{ body }}\n\nPlease fix this issue.' @pytest.fixture def mock_followup_prompt_template(): - return "Issue context: {{ issues }}\n\nReview comments: {{ review_comments }}\n\nReview threads: {{ review_threads }}\n\nFiles: {{ files }}\n\nPlease fix this issue." + return 'Issue context: {{ issues }}\n\nReview comments: {{ review_comments }}\n\nReview threads: {{ review_threads }}\n\nFiles: {{ files }}\n\nThread comments: {{ thread_context }}\n\nPlease fix this issue.' def create_cmd_output(exit_code: int, content: str, command_id: int, command: str): @@ -64,11 +64,11 @@ def test_initialize_runtime(): mock_runtime = MagicMock() mock_runtime.run_action.side_effect = [ create_cmd_output( - exit_code=0, content="", command_id=1, command="cd /workspace" + exit_code=0, content='', command_id=1, command='cd /workspace' ), create_cmd_output( exit_code=0, - content="", + content='', command_id=2, command='git config --global core.pager ""', ), @@ -77,26 +77,26 @@ def test_initialize_runtime(): initialize_runtime(mock_runtime) assert mock_runtime.run_action.call_count == 2 - mock_runtime.run_action.assert_any_call(CmdRunAction(command="cd /workspace")) + mock_runtime.run_action.assert_any_call(CmdRunAction(command='cd /workspace')) mock_runtime.run_action.assert_any_call( CmdRunAction(command='git config --global core.pager ""') ) def test_download_issues_from_github(): - handler = IssueHandler("owner", "repo", "token") + handler = IssueHandler('owner', 'repo', 'token') mock_issues_response = MagicMock() mock_issues_response.json.side_effect = [ [ - {"number": 1, "title": "Issue 1", "body": "This is an issue"}, + {'number': 1, 'title': 'Issue 1', 'body': 'This is an issue'}, { - "number": 2, - "title": "PR 1", - "body": "This is a pull request", - "pull_request": {}, + 'number': 2, + 'title': 'PR 1', + 'body': 'This is a pull request', + 'pull_request': {}, }, - {"number": 3, "title": "Issue 2", "body": "This is another issue"}, + {'number': 3, 'title': 'Issue 2', 'body': 'This is another issue'}, ], None, ] @@ -107,41 +107,41 @@ def test_download_issues_from_github(): mock_comments_response.raise_for_status = MagicMock() def get_mock_response(url, *args, **kwargs): - if "/comments" in url: + if '/comments' in url: return mock_comments_response return mock_issues_response - with patch("requests.get", side_effect=get_mock_response): + with patch('requests.get', side_effect=get_mock_response): issues = handler.get_converted_issues() assert len(issues) == 2 - assert handler.issue_type == "issue" + assert handler.issue_type == 'issue' assert all(isinstance(issue, GithubIssue) for issue in issues) assert [issue.number for issue in issues] == [1, 3] - assert [issue.title for issue in issues] == ["Issue 1", "Issue 2"] + assert [issue.title for issue in issues] == ['Issue 1', 'Issue 2'] assert [issue.review_comments for issue in issues] == [None, None] assert [issue.closing_issues for issue in issues] == [None, None] assert [issue.thread_ids for issue in issues] == [None, None] def test_download_pr_from_github(): - handler = PRHandler("owner", "repo", "token") + handler = PRHandler('owner', 'repo', 'token') mock_pr_response = MagicMock() mock_pr_response.json.side_effect = [ [ { - "number": 1, - "title": "PR 1", - "body": "This is a pull request", - "head": {"ref": "b1"}, + 'number': 1, + 'title': 'PR 1', + 'body': 'This is a pull request', + 'head': {'ref': 'b1'}, }, { - "number": 2, - "title": "My PR", - "body": "This is another pull request", - "head": {"ref": "b2"}, + 'number': 2, + 'title': 'My PR', + 'body': 'This is another pull request', + 'head': {'ref': 'b2'}, }, - {"number": 3, "title": "PR 3", "body": "Final PR", "head": {"ref": "b3"}}, + {'number': 3, 'title': 'PR 3', 'body': 'Final PR', 'head': {'ref': 'b3'}}, ], None, ] @@ -155,55 +155,55 @@ def test_download_pr_from_github(): # Mock for GraphQL request (for download_pr_metadata) mock_graphql_response = MagicMock() mock_graphql_response.json.side_effect = lambda: { - "data": { - "repository": { - "pullRequest": { - "closingIssuesReferences": { - "edges": [ - {"node": {"body": "Issue 1 body", "number": 1}}, - {"node": {"body": "Issue 2 body", "number": 2}}, + 'data': { + 'repository': { + 'pullRequest': { + 'closingIssuesReferences': { + 'edges': [ + {'node': {'body': 'Issue 1 body', 'number': 1}}, + {'node': {'body': 'Issue 2 body', 'number': 2}}, ] }, - "reviewThreads": { - "edges": [ + 'reviewThreads': { + 'edges': [ { - "node": { - "isResolved": False, - "id": "1", - "comments": { - "nodes": [ + 'node': { + 'isResolved': False, + 'id': '1', + 'comments': { + 'nodes': [ { - "body": "Unresolved comment 1", - "path": "/frontend/header.tsx", + 'body': 'Unresolved comment 1', + 'path': '/frontend/header.tsx', }, - {"body": "Follow up thread"}, + {'body': 'Follow up thread'}, ] }, } }, { - "node": { - "isResolved": True, - "id": "2", - "comments": { - "nodes": [ + 'node': { + 'isResolved': True, + 'id': '2', + 'comments': { + 'nodes': [ { - "body": "Resolved comment 1", - "path": "/some/file.py", + 'body': 'Resolved comment 1', + 'path': '/some/file.py', } ] }, } }, { - "node": { - "isResolved": False, - "id": "3", - "comments": { - "nodes": [ + 'node': { + 'isResolved': False, + 'id': '3', + 'comments': { + 'nodes': [ { - "body": "Unresolved comment 3", - "path": "/another/file.py", + 'body': 'Unresolved comment 3', + 'path': '/another/file.py', } ] }, @@ -219,34 +219,34 @@ def test_download_pr_from_github(): mock_graphql_response.raise_for_status = MagicMock() def get_mock_response(url, *args, **kwargs): - if "/comments" in url: + if '/comments' in url: return mock_comments_response return mock_pr_response - with patch("requests.get", side_effect=get_mock_response): - with patch("requests.post", return_value=mock_graphql_response): + with patch('requests.get', side_effect=get_mock_response): + with patch('requests.post', return_value=mock_graphql_response): issues = handler.get_converted_issues() assert len(issues) == 3 - assert handler.issue_type == "pr" + assert handler.issue_type == 'pr' assert all(isinstance(issue, GithubIssue) for issue in issues) assert [issue.number for issue in issues] == [1, 2, 3] - assert [issue.title for issue in issues] == ["PR 1", "My PR", "PR 3"] - assert [issue.head_branch for issue in issues] == ["b1", "b2", "b3"] + assert [issue.title for issue in issues] == ['PR 1', 'My PR', 'PR 3'] + assert [issue.head_branch for issue in issues] == ['b1', 'b2', 'b3'] assert len(issues[0].review_threads) == 2 # Only unresolved threads assert ( issues[0].review_threads[0].comment - == "Unresolved comment 1\n---\nlatest feedback:\nFollow up thread\n" + == 'Unresolved comment 1\n---\nlatest feedback:\nFollow up thread\n' ) - assert issues[0].review_threads[0].files == ["/frontend/header.tsx"] + assert issues[0].review_threads[0].files == ['/frontend/header.tsx'] assert ( issues[0].review_threads[1].comment - == "latest feedback:\nUnresolved comment 3\n" + == 'latest feedback:\nUnresolved comment 3\n' ) - assert issues[0].review_threads[1].files == ["/another/file.py"] - assert issues[0].closing_issues == ["Issue 1 body", "Issue 2 body"] - assert issues[0].thread_ids == ["1", "3"] + assert issues[0].review_threads[1].files == ['/another/file.py'] + assert issues[0].closing_issues == ['Issue 1 body', 'Issue 2 body'] + assert issues[0].thread_ids == ['1', '3'] @pytest.mark.asyncio @@ -254,34 +254,34 @@ async def test_complete_runtime(): mock_runtime = MagicMock() mock_runtime.run_action.side_effect = [ create_cmd_output( - exit_code=0, content="", command_id=1, command="cd /workspace" + exit_code=0, content='', command_id=1, command='cd /workspace' ), create_cmd_output( exit_code=0, - content="", + content='', command_id=2, command='git config --global core.pager ""', ), create_cmd_output( exit_code=0, - content="", + content='', command_id=3, - command="git config --global --add safe.directory /workspace", + command='git config --global --add safe.directory /workspace', ), create_cmd_output( exit_code=0, - content="", + content='', command_id=4, - command="git diff base_commit_hash fix", + command='git diff base_commit_hash fix', ), create_cmd_output( - exit_code=0, content="git diff content", command_id=5, command="git apply" + exit_code=0, content='git diff content', command_id=5, command='git apply' ), ] - result = await complete_runtime(mock_runtime, "base_commit_hash") + result = await complete_runtime(mock_runtime, 'base_commit_hash') - assert result == {"git_patch": "git diff content"} + assert result == {'git_patch': 'git diff content'} assert mock_runtime.run_action.call_count == 5 @@ -296,65 +296,65 @@ async def test_process_issue(mock_output_dir, mock_prompt_template): # Set up test data issue = GithubIssue( - owner="test_owner", - repo="test_repo", + owner='test_owner', + repo='test_repo', number=1, - title="Test Issue", - body="This is a test issue", + title='Test Issue', + body='This is a test issue', ) - base_commit = "abcdef1234567890" - repo_instruction = "Resolve this repo" + base_commit = 'abcdef1234567890' + repo_instruction = 'Resolve this repo' max_iterations = 5 - llm_config = LLMConfig(model="test_model", api_key="test_api_key") - runtime_container_image = "test_image:latest" + llm_config = LLMConfig(model='test_model', api_key='test_api_key') + runtime_container_image = 'test_image:latest' # Test cases for different scenarios test_cases = [ { - "name": "successful_run", - "run_controller_return": MagicMock( - history=[NullObservation(content="")], + 'name': 'successful_run', + 'run_controller_return': MagicMock( + history=[NullObservation(content='')], metrics=MagicMock( - get=MagicMock(return_value={"test_result": "passed"}) + get=MagicMock(return_value={'test_result': 'passed'}) ), last_error=None, ), - "run_controller_raises": None, - "expected_success": True, - "expected_error": None, - "expected_explanation": "Issue resolved successfully", + 'run_controller_raises': None, + 'expected_success': True, + 'expected_error': None, + 'expected_explanation': 'Issue resolved successfully', }, { - "name": "value_error", - "run_controller_return": None, - "run_controller_raises": ValueError("Test value error"), - "expected_success": False, - "expected_error": "Agent failed to run or crashed", - "expected_explanation": "Agent failed to run", + 'name': 'value_error', + 'run_controller_return': None, + 'run_controller_raises': ValueError('Test value error'), + 'expected_success': False, + 'expected_error': 'Agent failed to run or crashed', + 'expected_explanation': 'Agent failed to run', }, { - "name": "runtime_error", - "run_controller_return": None, - "run_controller_raises": RuntimeError("Test runtime error"), - "expected_success": False, - "expected_error": "Agent failed to run or crashed", - "expected_explanation": "Agent failed to run", + 'name': 'runtime_error', + 'run_controller_return': None, + 'run_controller_raises': RuntimeError('Test runtime error'), + 'expected_success': False, + 'expected_error': 'Agent failed to run or crashed', + 'expected_explanation': 'Agent failed to run', }, { - "name": "json_decode_error", - "run_controller_return": MagicMock( - history=[NullObservation(content="")], + 'name': 'json_decode_error', + 'run_controller_return': MagicMock( + history=[NullObservation(content='')], metrics=MagicMock( - get=MagicMock(return_value={"test_result": "passed"}) + get=MagicMock(return_value={'test_result': 'passed'}) ), last_error=None, ), - "run_controller_raises": None, - "expected_success": True, - "expected_error": None, - "expected_explanation": "Non-JSON explanation", - "is_pr": True, - "comment_success": [ + 'run_controller_raises': None, + 'expected_success': True, + 'expected_error': None, + 'expected_explanation': 'Non-JSON explanation', + 'is_pr': True, + 'comment_success': [ True, False, ], # To trigger the PR success logging code path @@ -371,31 +371,31 @@ async def test_process_issue(mock_output_dir, mock_prompt_template): # Mock return values mock_create_runtime.return_value = MagicMock(connect=AsyncMock()) - if test_case["run_controller_raises"]: - mock_run_controller.side_effect = test_case["run_controller_raises"] + if test_case['run_controller_raises']: + mock_run_controller.side_effect = test_case['run_controller_raises'] else: - mock_run_controller.return_value = test_case["run_controller_return"] + mock_run_controller.return_value = test_case['run_controller_return'] mock_run_controller.side_effect = None - mock_complete_runtime.return_value = {"git_patch": "test patch"} + mock_complete_runtime.return_value = {'git_patch': 'test patch'} handler_instance.guess_success.return_value = ( - test_case["expected_success"], - test_case.get("comment_success", None), - test_case["expected_explanation"], + test_case['expected_success'], + test_case.get('comment_success', None), + test_case['expected_explanation'], ) - handler_instance.get_instruction.return_value = ("Test instruction", []) - handler_instance.issue_type = "pr" if test_case.get("is_pr", False) else "issue" + handler_instance.get_instruction.return_value = ('Test instruction', []) + handler_instance.issue_type = 'pr' if test_case.get('is_pr', False) else 'issue' with patch( - "openhands.resolver.resolve_issue.create_runtime", mock_create_runtime + 'openhands.resolver.resolve_issue.create_runtime', mock_create_runtime ), patch( - "openhands.resolver.resolve_issue.initialize_runtime", + 'openhands.resolver.resolve_issue.initialize_runtime', mock_initialize_runtime, ), patch( - "openhands.resolver.resolve_issue.run_controller", mock_run_controller + 'openhands.resolver.resolve_issue.run_controller', mock_run_controller ), patch( - "openhands.resolver.resolve_issue.complete_runtime", mock_complete_runtime - ), patch("openhands.resolver.resolve_issue.logger"): + 'openhands.resolver.resolve_issue.complete_runtime', mock_complete_runtime + ), patch('openhands.resolver.resolve_issue.logger'): # Call the function result = await process_issue( issue, @@ -411,15 +411,15 @@ async def test_process_issue(mock_output_dir, mock_prompt_template): ) # Assert the result - expected_issue_type = "pr" if test_case.get("is_pr", False) else "issue" + expected_issue_type = 'pr' if test_case.get('is_pr', False) else 'issue' assert handler_instance.issue_type == expected_issue_type assert isinstance(result, ResolverOutput) assert result.issue == issue assert result.base_commit == base_commit - assert result.git_patch == "test patch" - assert result.success == test_case["expected_success"] - assert result.success_explanation == test_case["expected_explanation"] - assert result.error == test_case["expected_error"] + assert result.git_patch == 'test patch' + assert result.success == test_case['expected_success'] + assert result.success_explanation == test_case['expected_explanation'] + assert result.error == test_case['expected_error'] # Assert that the mocked functions were called mock_create_runtime.assert_called_once() @@ -428,7 +428,7 @@ async def test_process_issue(mock_output_dir, mock_prompt_template): mock_complete_runtime.assert_called_once() # Assert that guess_success was called only for successful runs - if test_case["expected_success"]: + if test_case['expected_success']: handler_instance.guess_success.assert_called_once() else: handler_instance.guess_success.assert_not_called() @@ -436,60 +436,64 @@ async def test_process_issue(mock_output_dir, mock_prompt_template): def test_get_instruction(mock_prompt_template, mock_followup_prompt_template): issue = GithubIssue( - owner="test_owner", - repo="test_repo", + owner='test_owner', + repo='test_repo', number=123, - title="Test Issue", - body="This is a test issue refer to image ![First Image](https://sampleimage.com/image1.png)", + title='Test Issue', + body='This is a test issue refer to image ![First Image](https://sampleimage.com/image1.png)', ) - issue_handler = IssueHandler("owner", "repo", "token") + issue_handler = IssueHandler('owner', 'repo', 'token') instruction, images_urls = issue_handler.get_instruction( issue, mock_prompt_template, None ) - expected_instruction = "Issue: Test Issue\n\nThis is a test issue refer to image ![First Image](https://sampleimage.com/image1.png)\n\nPlease fix this issue." + expected_instruction = 'Issue: Test Issue\n\nThis is a test issue refer to image ![First Image](https://sampleimage.com/image1.png)\n\nPlease fix this issue.' - assert images_urls == ["https://sampleimage.com/image1.png"] - assert issue_handler.issue_type == "issue" + assert images_urls == ['https://sampleimage.com/image1.png'] + assert issue_handler.issue_type == 'issue' assert instruction == expected_instruction issue = GithubIssue( - owner="test_owner", - repo="test_repo", + owner='test_owner', + repo='test_repo', number=123, - title="Test Issue", - body="This is a test issue", - closing_issues=["Issue 1 fix the type"], + title='Test Issue', + body='This is a test issue', + closing_issues=['Issue 1 fix the type'], review_threads=[ ReviewThread( comment="There is still a typo 'pthon' instead of 'python'", files=[] ) ], + thread_comments=[ + "I've left review comments, please address them", + 'This is a valid concern.', + ], ) - pr_handler = PRHandler("owner", "repo", "token") + pr_handler = PRHandler('owner', 'repo', 'token') instruction, images_urls = pr_handler.get_instruction( issue, mock_followup_prompt_template, None ) - expected_instruction = "Issue context: [\n \"Issue 1 fix the type\"\n]\n\nReview comments: None\n\nReview threads: [\n \"There is still a typo 'pthon' instead of 'python'\"\n]\n\nFiles: []\n\nPlease fix this issue." + expected_instruction = "Issue context: [\n \"Issue 1 fix the type\"\n]\n\nReview comments: None\n\nReview threads: [\n \"There is still a typo 'pthon' instead of 'python'\"\n]\n\nFiles: []\n\nThread comments: I've left review comments, please address them\n---\nThis is a valid concern.\n\nPlease fix this issue." assert images_urls == [] - assert pr_handler.issue_type == "pr" + assert pr_handler.issue_type == 'pr' assert instruction == expected_instruction def test_file_instruction(): issue = GithubIssue( - owner="test_owner", - repo="test_repo", + owner='test_owner', + repo='test_repo', number=123, - title="Test Issue", - body="This is a test issue ![image](https://sampleimage.com/sample.png)", + title='Test Issue', + body='This is a test issue ![image](https://sampleimage.com/sample.png)', ) # load prompt from openhands/resolver/prompts/resolve/basic.jinja - with open("openhands/resolver/prompts/resolve/basic.jinja", "r") as f: + with open('openhands/resolver/prompts/resolve/basic.jinja', 'r') as f: prompt = f.read() # Test without thread comments - issue_handler = IssueHandler("owner", "repo", "token") + issue_handler = IssueHandler('owner', 'repo', 'token') instruction, images_urls = issue_handler.get_instruction(issue, prompt, None) expected_instruction = """Please fix the following issue for the repository in /workspace. An environment has been set up for you to start working. You may assume all necessary tools are installed. @@ -505,28 +509,28 @@ def test_file_instruction(): When you think you have fixed the issue through code changes, please finish the interaction.""" assert instruction == expected_instruction - assert images_urls == ["https://sampleimage.com/sample.png"] + assert images_urls == ['https://sampleimage.com/sample.png'] def test_file_instruction_with_repo_instruction(): issue = GithubIssue( - owner="test_owner", - repo="test_repo", + owner='test_owner', + repo='test_repo', number=123, - title="Test Issue", - body="This is a test issue", + title='Test Issue', + body='This is a test issue', ) # load prompt from openhands/resolver/prompts/resolve/basic.jinja - with open("openhands/resolver/prompts/resolve/basic.jinja", "r") as f: + with open('openhands/resolver/prompts/resolve/basic.jinja', 'r') as f: prompt = f.read() # load repo instruction from openhands/resolver/prompts/repo_instructions/all-hands-ai___openhands-resolver.txt with open( - "openhands/resolver/prompts/repo_instructions/all-hands-ai___openhands-resolver.txt", - "r", + 'openhands/resolver/prompts/repo_instructions/all-hands-ai___openhands-resolver.txt', + 'r', ) as f: repo_instruction = f.read() - issue_handler = IssueHandler("owner", "repo", "token") + issue_handler = IssueHandler('owner', 'repo', 'token') instruction, image_urls = issue_handler.get_instruction( issue, prompt, repo_instruction ) @@ -549,226 +553,226 @@ def test_file_instruction_with_repo_instruction(): When you think you have fixed the issue through code changes, please finish the interaction.""" assert instruction == expected_instruction - assert issue_handler.issue_type == "issue" + assert issue_handler.issue_type == 'issue' assert image_urls == [] def test_guess_success(): mock_issue = GithubIssue( - owner="test_owner", - repo="test_repo", + owner='test_owner', + repo='test_repo', number=1, - title="Test Issue", - body="This is a test issue", + title='Test Issue', + body='This is a test issue', ) mock_history = [ create_cmd_output( - exit_code=0, content="", command_id=1, command="cd /workspace" + exit_code=0, content='', command_id=1, command='cd /workspace' ) ] - mock_llm_config = LLMConfig(model="test_model", api_key="test_api_key") + mock_llm_config = LLMConfig(model='test_model', api_key='test_api_key') mock_completion_response = MagicMock() mock_completion_response.choices = [ MagicMock( message=MagicMock( - content="--- success\ntrue\n--- explanation\nIssue resolved successfully" + content='--- success\ntrue\n--- explanation\nIssue resolved successfully' ) ) ] - issue_handler = IssueHandler("owner", "repo", "token") + issue_handler = IssueHandler('owner', 'repo', 'token') - with patch("litellm.completion", MagicMock(return_value=mock_completion_response)): + with patch('litellm.completion', MagicMock(return_value=mock_completion_response)): success, comment_success, explanation = issue_handler.guess_success( mock_issue, mock_history, mock_llm_config ) - assert issue_handler.issue_type == "issue" + assert issue_handler.issue_type == 'issue' assert comment_success is None assert success - assert explanation == "Issue resolved successfully" + assert explanation == 'Issue resolved successfully' def test_guess_success_with_thread_comments(): mock_issue = GithubIssue( - owner="test_owner", - repo="test_repo", + owner='test_owner', + repo='test_repo', number=1, - title="Test Issue", - body="This is a test issue", + title='Test Issue', + body='This is a test issue', thread_comments=[ - "First comment", - "Second comment", - "latest feedback:\nPlease add tests", + 'First comment', + 'Second comment', + 'latest feedback:\nPlease add tests', ], ) - mock_history = [MagicMock(message="I have added tests for this case")] - mock_llm_config = LLMConfig(model="test_model", api_key="test_api_key") + mock_history = [MagicMock(message='I have added tests for this case')] + mock_llm_config = LLMConfig(model='test_model', api_key='test_api_key') mock_completion_response = MagicMock() mock_completion_response.choices = [ MagicMock( message=MagicMock( - content="--- success\ntrue\n--- explanation\nTests have been added to verify thread comments handling" + content='--- success\ntrue\n--- explanation\nTests have been added to verify thread comments handling' ) ) ] - issue_handler = IssueHandler("owner", "repo", "token") + issue_handler = IssueHandler('owner', 'repo', 'token') - with patch("litellm.completion", MagicMock(return_value=mock_completion_response)): + with patch('litellm.completion', MagicMock(return_value=mock_completion_response)): success, comment_success, explanation = issue_handler.guess_success( mock_issue, mock_history, mock_llm_config ) - assert issue_handler.issue_type == "issue" + assert issue_handler.issue_type == 'issue' assert comment_success is None assert success - assert "Tests have been added" in explanation + assert 'Tests have been added' in explanation def test_instruction_with_thread_comments(): # Create an issue with thread comments issue = GithubIssue( - owner="test_owner", - repo="test_repo", + owner='test_owner', + repo='test_repo', number=123, - title="Test Issue", - body="This is a test issue", + title='Test Issue', + body='This is a test issue', thread_comments=[ - "First comment", - "Second comment", - "latest feedback:\nPlease add tests", + 'First comment', + 'Second comment', + 'latest feedback:\nPlease add tests', ], ) # Load the basic prompt template - with open("openhands/resolver/prompts/resolve/basic.jinja", "r") as f: + with open('openhands/resolver/prompts/resolve/basic.jinja', 'r') as f: prompt = f.read() - issue_handler = IssueHandler("owner", "repo", "token") + issue_handler = IssueHandler('owner', 'repo', 'token') instruction, images_urls = issue_handler.get_instruction(issue, prompt, None) # Verify that thread comments are included in the instruction - assert "First comment" in instruction - assert "Second comment" in instruction - assert "Please add tests" in instruction - assert "Issue Thread Comments:" in instruction + assert 'First comment' in instruction + assert 'Second comment' in instruction + assert 'Please add tests' in instruction + assert 'Issue Thread Comments:' in instruction assert images_urls == [] def test_guess_success_failure(): mock_issue = GithubIssue( - owner="test_owner", - repo="test_repo", + owner='test_owner', + repo='test_repo', number=1, - title="Test Issue", - body="This is a test issue", + title='Test Issue', + body='This is a test issue', thread_comments=[ - "First comment", - "Second comment", - "latest feedback:\nPlease add tests", + 'First comment', + 'Second comment', + 'latest feedback:\nPlease add tests', ], ) - mock_history = [MagicMock(message="I have added tests for this case")] - mock_llm_config = LLMConfig(model="test_model", api_key="test_api_key") + mock_history = [MagicMock(message='I have added tests for this case')] + mock_llm_config = LLMConfig(model='test_model', api_key='test_api_key') mock_completion_response = MagicMock() mock_completion_response.choices = [ MagicMock( message=MagicMock( - content="--- success\ntrue\n--- explanation\nTests have been added to verify thread comments handling" + content='--- success\ntrue\n--- explanation\nTests have been added to verify thread comments handling' ) ) ] - issue_handler = IssueHandler("owner", "repo", "token") + issue_handler = IssueHandler('owner', 'repo', 'token') - with patch("litellm.completion", MagicMock(return_value=mock_completion_response)): + with patch('litellm.completion', MagicMock(return_value=mock_completion_response)): success, comment_success, explanation = issue_handler.guess_success( mock_issue, mock_history, mock_llm_config ) - assert issue_handler.issue_type == "issue" + assert issue_handler.issue_type == 'issue' assert comment_success is None assert success - assert "Tests have been added" in explanation + assert 'Tests have been added' in explanation def test_guess_success_negative_case(): mock_issue = GithubIssue( - owner="test_owner", - repo="test_repo", + owner='test_owner', + repo='test_repo', number=1, - title="Test Issue", - body="This is a test issue", + title='Test Issue', + body='This is a test issue', ) mock_history = [ create_cmd_output( - exit_code=0, content="", command_id=1, command="cd /workspace" + exit_code=0, content='', command_id=1, command='cd /workspace' ) ] - mock_llm_config = LLMConfig(model="test_model", api_key="test_api_key") + mock_llm_config = LLMConfig(model='test_model', api_key='test_api_key') mock_completion_response = MagicMock() mock_completion_response.choices = [ MagicMock( message=MagicMock( - content="--- success\nfalse\n--- explanation\nIssue not resolved" + content='--- success\nfalse\n--- explanation\nIssue not resolved' ) ) ] - issue_handler = IssueHandler("owner", "repo", "token") + issue_handler = IssueHandler('owner', 'repo', 'token') - with patch("litellm.completion", MagicMock(return_value=mock_completion_response)): + with patch('litellm.completion', MagicMock(return_value=mock_completion_response)): success, comment_success, explanation = issue_handler.guess_success( mock_issue, mock_history, mock_llm_config ) - assert issue_handler.issue_type == "issue" + assert issue_handler.issue_type == 'issue' assert comment_success is None assert not success - assert explanation == "Issue not resolved" + assert explanation == 'Issue not resolved' def test_guess_success_invalid_output(): mock_issue = GithubIssue( - owner="test_owner", - repo="test_repo", + owner='test_owner', + repo='test_repo', number=1, - title="Test Issue", - body="This is a test issue", + title='Test Issue', + body='This is a test issue', ) mock_history = [ create_cmd_output( - exit_code=0, content="", command_id=1, command="cd /workspace" + exit_code=0, content='', command_id=1, command='cd /workspace' ) ] - mock_llm_config = LLMConfig(model="test_model", api_key="test_api_key") + mock_llm_config = LLMConfig(model='test_model', api_key='test_api_key') mock_completion_response = MagicMock() mock_completion_response.choices = [ - MagicMock(message=MagicMock(content="This is not a valid output")) + MagicMock(message=MagicMock(content='This is not a valid output')) ] - issue_handler = IssueHandler("owner", "repo", "token") + issue_handler = IssueHandler('owner', 'repo', 'token') - with patch("litellm.completion", MagicMock(return_value=mock_completion_response)): + with patch('litellm.completion', MagicMock(return_value=mock_completion_response)): success, comment_success, explanation = issue_handler.guess_success( mock_issue, mock_history, mock_llm_config ) - assert issue_handler.issue_type == "issue" + assert issue_handler.issue_type == 'issue' assert comment_success is None assert not success assert ( explanation - == "Failed to decode answer from LLM response: This is not a valid output" + == 'Failed to decode answer from LLM response: This is not a valid output' ) def test_download_pr_with_review_comments(): - handler = PRHandler("owner", "repo", "token") + handler = PRHandler('owner', 'repo', 'token') mock_pr_response = MagicMock() mock_pr_response.json.side_effect = [ [ { - "number": 1, - "title": "PR 1", - "body": "This is a pull request", - "head": {"ref": "b1"}, + 'number': 1, + 'title': 'PR 1', + 'body': 'This is a pull request', + 'head': {'ref': 'b1'}, }, ], None, @@ -783,14 +787,14 @@ def test_download_pr_with_review_comments(): # Mock for GraphQL request with review comments but no threads mock_graphql_response = MagicMock() mock_graphql_response.json.side_effect = lambda: { - "data": { - "repository": { - "pullRequest": { - "closingIssuesReferences": {"edges": []}, - "reviews": { - "nodes": [ - {"body": "Please fix this typo"}, - {"body": "Add more tests"}, + 'data': { + 'repository': { + 'pullRequest': { + 'closingIssuesReferences': {'edges': []}, + 'reviews': { + 'nodes': [ + {'body': 'Please fix this typo'}, + {'body': 'Add more tests'}, ] }, } @@ -801,32 +805,32 @@ def test_download_pr_with_review_comments(): mock_graphql_response.raise_for_status = MagicMock() def get_mock_response(url, *args, **kwargs): - if "/comments" in url: + if '/comments' in url: return mock_comments_response return mock_pr_response - with patch("requests.get", side_effect=get_mock_response): - with patch("requests.post", return_value=mock_graphql_response): + with patch('requests.get', side_effect=get_mock_response): + with patch('requests.post', return_value=mock_graphql_response): issues = handler.get_converted_issues() assert len(issues) == 1 - assert handler.issue_type == "pr" + assert handler.issue_type == 'pr' assert isinstance(issues[0], GithubIssue) assert issues[0].number == 1 - assert issues[0].title == "PR 1" - assert issues[0].head_branch == "b1" + assert issues[0].title == 'PR 1' + assert issues[0].head_branch == 'b1' # Verify review comments are set but threads are empty assert len(issues[0].review_comments) == 2 - assert issues[0].review_comments[0] == "Please fix this typo" - assert issues[0].review_comments[1] == "Add more tests" + assert issues[0].review_comments[0] == 'Please fix this typo' + assert issues[0].review_comments[1] == 'Add more tests' assert not issues[0].review_threads assert not issues[0].closing_issues assert not issues[0].thread_ids def test_download_issue_with_specific_comment(): - handler = IssueHandler("owner", "repo", "token") + handler = IssueHandler('owner', 'repo', 'token') # Define the specific comment_id to filter specific_comment_id = 101 @@ -835,7 +839,7 @@ def test_download_issue_with_specific_comment(): mock_issue_response = MagicMock() mock_issue_response.json.side_effect = [ [ - {"number": 1, "title": "Issue 1", "body": "This is an issue"}, + {'number': 1, 'title': 'Issue 1', 'body': 'This is an issue'}, ], None, ] @@ -844,32 +848,32 @@ def test_download_issue_with_specific_comment(): mock_comments_response = MagicMock() mock_comments_response.json.return_value = [ { - "id": specific_comment_id, - "body": "Specific comment body", - "issue_url": "https://api.github.com/repos/owner/repo/issues/1", + 'id': specific_comment_id, + 'body': 'Specific comment body', + 'issue_url': 'https://api.github.com/repos/owner/repo/issues/1', }, { - "id": 102, - "body": "Another comment body", - "issue_url": "https://api.github.com/repos/owner/repo/issues/2", + 'id': 102, + 'body': 'Another comment body', + 'issue_url': 'https://api.github.com/repos/owner/repo/issues/2', }, ] mock_comments_response.raise_for_status = MagicMock() def get_mock_response(url, *args, **kwargs): - if "/comments" in url: + if '/comments' in url: return mock_comments_response return mock_issue_response - with patch("requests.get", side_effect=get_mock_response): + with patch('requests.get', side_effect=get_mock_response): issues = handler.get_converted_issues(comment_id=specific_comment_id) assert len(issues) == 1 assert issues[0].number == 1 - assert issues[0].title == "Issue 1" - assert issues[0].thread_comments == ["Specific comment body"] + assert issues[0].title == 'Issue 1' + assert issues[0].thread_comments == ['Specific comment body'] -if __name__ == "__main__": +if __name__ == '__main__': pytest.main() From 422104c8778b51e02c6108d5517e33de34253f43 Mon Sep 17 00:00:00 2001 From: Xingyao Wang Date: Mon, 18 Nov 2024 20:21:46 -0600 Subject: [PATCH 12/56] fix #5111: add FunctionCallNotExistsError to handle cases where tool calling failed (#5113) --- openhands/agenthub/codeact_agent/function_calling.py | 5 ++++- openhands/controller/agent_controller.py | 2 ++ openhands/core/exceptions.py | 7 +++++++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/openhands/agenthub/codeact_agent/function_calling.py b/openhands/agenthub/codeact_agent/function_calling.py index 42ce1f98db87..a4ee35ff7b59 100644 --- a/openhands/agenthub/codeact_agent/function_calling.py +++ b/openhands/agenthub/codeact_agent/function_calling.py @@ -12,6 +12,7 @@ ModelResponse, ) +from openhands.core.exceptions import FunctionCallNotExistsError from openhands.core.logger import openhands_logger as logger from openhands.events.action import ( Action, @@ -484,7 +485,9 @@ def response_to_actions(response: ModelResponse) -> list[Action]: elif tool_call.function.name == 'browser': action = BrowseInteractiveAction(browser_actions=arguments['code']) else: - raise RuntimeError(f'Unknown tool call: {tool_call.function.name}') + raise FunctionCallNotExistsError( + f'Tool {tool_call.function.name} is not registered. (arguments: {arguments}). Please check the tool name and retry with an existing tool.' + ) # We only add thought to the first action if i == 0: diff --git a/openhands/controller/agent_controller.py b/openhands/controller/agent_controller.py index 9ba8c3d0a154..f9e4a8edb555 100644 --- a/openhands/controller/agent_controller.py +++ b/openhands/controller/agent_controller.py @@ -12,6 +12,7 @@ from openhands.controller.stuck import StuckDetector from openhands.core.config import AgentConfig, LLMConfig from openhands.core.exceptions import ( + FunctionCallNotExistsError, FunctionCallValidationError, LLMMalformedActionError, LLMNoActionError, @@ -488,6 +489,7 @@ async def _step(self) -> None: LLMNoActionError, LLMResponseError, FunctionCallValidationError, + FunctionCallNotExistsError, ) as e: self.event_stream.add_event( ErrorObservation( diff --git a/openhands/core/exceptions.py b/openhands/core/exceptions.py index 0c0a771191b4..bf5a29f60752 100644 --- a/openhands/core/exceptions.py +++ b/openhands/core/exceptions.py @@ -114,3 +114,10 @@ class FunctionCallValidationError(Exception): def __init__(self, message): super().__init__(message) + + +class FunctionCallNotExistsError(Exception): + """Exception raised when an LLM call a tool that is not registered.""" + + def __init__(self, message): + super().__init__(message) From a531413d8649640842d2e639e15b4e7ecadf35c5 Mon Sep 17 00:00:00 2001 From: Xingyao Wang Date: Mon, 18 Nov 2024 20:22:55 -0600 Subject: [PATCH 13/56] fix(eval): support setting hard timeout per evaluation instance (#5110) --- evaluation/swe_bench/run_infer.py | 2 +- evaluation/utils/shared.py | 61 ++++++++++++++++++++++++++++--- 2 files changed, 56 insertions(+), 7 deletions(-) diff --git a/evaluation/swe_bench/run_infer.py b/evaluation/swe_bench/run_infer.py index 386c0dd19238..9cb9dd77f498 100644 --- a/evaluation/swe_bench/run_infer.py +++ b/evaluation/swe_bench/run_infer.py @@ -145,7 +145,7 @@ def get_config( platform='linux/amd64', api_key=os.environ.get('ALLHANDS_API_KEY', None), remote_runtime_api_url=os.environ.get('SANDBOX_REMOTE_RUNTIME_API_URL'), - keep_remote_runtime_alive=False, + keep_runtime_alive=False, remote_runtime_init_timeout=3600, ), # do not mount workspace diff --git a/evaluation/utils/shared.py b/evaluation/utils/shared.py index 847eb16bb32e..517ecc523581 100644 --- a/evaluation/utils/shared.py +++ b/evaluation/utils/shared.py @@ -3,9 +3,11 @@ import multiprocessing as mp import os import pathlib +import signal import subprocess import time import traceback +from contextlib import contextmanager from typing import Any, Awaitable, Callable, TextIO import pandas as pd @@ -92,6 +94,27 @@ class EvalException(Exception): pass +class EvalTimeoutException(Exception): + pass + + +@contextmanager +def timeout(seconds: int): + def timeout_handler(signum, frame): + raise EvalTimeoutException(f'Function timed out after {seconds} seconds') + + # Set up the signal handler + original_handler = signal.signal(signal.SIGALRM, timeout_handler) + signal.alarm(seconds) + + try: + yield + finally: + # Restore the original handler and disable the alarm + signal.alarm(0) + signal.signal(signal.SIGALRM, original_handler) + + def codeact_user_response( state: State, encapsulate_solution: bool = False, @@ -280,15 +303,33 @@ def _process_instance_wrapper( metadata: EvalMetadata, use_mp: bool, max_retries: int = 5, + timeout_seconds: int | None = None, ) -> EvalOutput: - """Wrap the process_instance_func to handle retries and errors. - - Retry an instance up to max_retries times if it fails (e.g., due to transient network/runtime issues). - """ + """Wrap the process_instance_func to handle retries and errors.""" for attempt in range(max_retries + 1): try: - result = process_instance_func(instance, metadata, use_mp) + if timeout_seconds is not None: + with timeout(timeout_seconds): + result = process_instance_func(instance, metadata, use_mp) + else: + result = process_instance_func(instance, metadata, use_mp) return result + except EvalTimeoutException as e: + error = f'Timeout after {timeout_seconds} seconds' + stacktrace = traceback.format_exc() + msg = ( + '-' * 10 + + '\n' + + f'Timeout ({timeout_seconds} seconds) in instance [{instance.instance_id}], Stopped evaluation for this instance.' + + '\n' + + '-' * 10 + ) + logger.exception(e) + return EvalOutput( + instance_id=instance.instance_id, + test_result={}, + error=error, + ) except Exception as e: error = str(e) stacktrace = traceback.format_exc() @@ -337,6 +378,7 @@ def run_evaluation( [pd.Series, EvalMetadata, bool], Awaitable[EvalOutput] ], max_retries: int = 5, # number of retries for each instance + timeout_seconds: int | None = None, ): use_multiprocessing = num_workers > 1 @@ -357,7 +399,14 @@ def run_evaluation( if use_multiprocessing: with mp.Pool(num_workers) as pool: args_iter = ( - (process_instance_func, instance, metadata, True, max_retries) + ( + process_instance_func, + instance, + metadata, + True, + max_retries, + timeout_seconds, + ) for _, instance in dataset.iterrows() ) results = pool.imap_unordered(_process_instance_wrapper_mp, args_iter) From ca64c69b4a257d703e68ff44f9b4e4f435eabcfb Mon Sep 17 00:00:00 2001 From: young010101 <93481273+young010101@users.noreply.github.com> Date: Tue, 19 Nov 2024 10:45:06 +0800 Subject: [PATCH 14/56] Docs update runtime link (#5117) --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e69538d21e0b..847b6c469812 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -54,7 +54,7 @@ The agent needs a place to run code and commands. When you run OpenHands on your to do this by default. But there are other ways of creating a sandbox for the agent. If you work for a company that provides a cloud-based runtime, you could help us add support for that runtime -by implementing the [interface specified here](https://github.com/All-Hands-AI/OpenHands/blob/main/openhands/runtime/runtime.py). +by implementing the [interface specified here](https://github.com/All-Hands-AI/OpenHands/blob/main/openhands/runtime/base.py). #### Testing When you write code, it is also good to write tests. Please navigate to the `tests` folder to see existing test suites. From 2c580387c56273be95280ffb245a430a377cc339 Mon Sep 17 00:00:00 2001 From: Raymond Xu Date: Tue, 19 Nov 2024 04:16:29 -0800 Subject: [PATCH 15/56] Allow to merge to a specific target branch instead of main (#5109) --- openhands/resolver/send_pull_request.py | 32 +++++-- tests/unit/resolver/test_send_pull_request.py | 91 ++++++++++++++++--- 2 files changed, 101 insertions(+), 22 deletions(-) diff --git a/openhands/resolver/send_pull_request.py b/openhands/resolver/send_pull_request.py index eade7fcfc419..8a9d6118bedd 100644 --- a/openhands/resolver/send_pull_request.py +++ b/openhands/resolver/send_pull_request.py @@ -203,6 +203,7 @@ def send_pull_request( pr_type: str, fork_owner: str | None = None, additional_message: str | None = None, + target_branch: str | None = None, ) -> str: if pr_type not in ['branch', 'draft', 'ready']: raise ValueError(f'Invalid pr_type: {pr_type}') @@ -224,12 +225,19 @@ def send_pull_request( attempt += 1 branch_name = f'{base_branch_name}-try{attempt}' - # Get the default branch - print('Getting default branch...') - response = requests.get(f'{base_url}', headers=headers) - response.raise_for_status() - default_branch = response.json()['default_branch'] - print(f'Default branch: {default_branch}') + # Get the default branch or use specified target branch + print('Getting base branch...') + if target_branch: + base_branch = target_branch + # Verify the target branch exists + response = requests.get(f'{base_url}/branches/{target_branch}', headers=headers) + if response.status_code != 200: + raise ValueError(f'Target branch {target_branch} does not exist') + else: + response = requests.get(f'{base_url}', headers=headers) + response.raise_for_status() + base_branch = response.json()['default_branch'] + print(f'Base branch: {base_branch}') # Create and checkout the new branch print('Creating new branch...') @@ -279,7 +287,7 @@ def send_pull_request( 'title': pr_title, # No need to escape title for GitHub API 'body': pr_body, 'head': branch_name, - 'base': default_branch, + 'base': base_branch, 'draft': pr_type == 'draft', } response = requests.post(f'{base_url}/pulls', headers=headers, json=data) @@ -435,6 +443,7 @@ def process_single_issue( llm_config: LLMConfig, fork_owner: str | None, send_on_failure: bool, + target_branch: str | None = None, ) -> None: if not resolver_output.success and not send_on_failure: print( @@ -484,6 +493,7 @@ def process_single_issue( llm_config=llm_config, fork_owner=fork_owner, additional_message=resolver_output.success_explanation, + target_branch=target_branch, ) @@ -508,6 +518,7 @@ def process_all_successful_issues( llm_config, fork_owner, False, + None, ) @@ -573,6 +584,12 @@ def main(): default=None, help='Base URL for the LLM model.', ) + parser.add_argument( + '--target-branch', + type=str, + default=None, + help='Target branch to create the pull request against (defaults to repository default branch)', + ) my_args = parser.parse_args() github_token = ( @@ -625,6 +642,7 @@ def main(): llm_config, my_args.fork_owner, my_args.send_on_failure, + my_args.target_branch, ) diff --git a/tests/unit/resolver/test_send_pull_request.py b/tests/unit/resolver/test_send_pull_request.py index 951be1af006c..f83e2e97ec2f 100644 --- a/tests/unit/resolver/test_send_pull_request.py +++ b/tests/unit/resolver/test_send_pull_request.py @@ -322,7 +322,17 @@ def test_update_existing_pull_request( ) -@pytest.mark.parametrize('pr_type', ['branch', 'draft', 'ready']) +@pytest.mark.parametrize( + 'pr_type,target_branch', + [ + ('branch', None), + ('draft', None), + ('ready', None), + ('branch', 'feature'), + ('draft', 'develop'), + ('ready', 'staging'), + ], +) @patch('subprocess.run') @patch('requests.post') @patch('requests.get') @@ -334,14 +344,22 @@ def test_send_pull_request( mock_output_dir, mock_llm_config, pr_type, + target_branch, ): repo_path = os.path.join(mock_output_dir, 'repo') - # Mock API responses - mock_get.side_effect = [ - MagicMock(status_code=404), # Branch doesn't exist - MagicMock(json=lambda: {'default_branch': 'main'}), - ] + # Mock API responses based on whether target_branch is specified + if target_branch: + mock_get.side_effect = [ + MagicMock(status_code=404), # Branch doesn't exist + MagicMock(status_code=200), # Target branch exists + ] + else: + mock_get.side_effect = [ + MagicMock(status_code=404), # Branch doesn't exist + MagicMock(json=lambda: {'default_branch': 'main'}), # Get default branch + ] + mock_post.return_value.json.return_value = { 'html_url': 'https://github.com/test-owner/test-repo/pull/1' } @@ -360,10 +378,12 @@ def test_send_pull_request( patch_dir=repo_path, pr_type=pr_type, llm_config=mock_llm_config, + target_branch=target_branch, ) # Assert API calls - assert mock_get.call_count == 2 + expected_get_calls = 2 + assert mock_get.call_count == expected_get_calls # Check branch creation and push assert mock_run.call_count == 2 @@ -401,10 +421,41 @@ def test_send_pull_request( assert post_data['title'] == 'Fix issue #42: Test Issue' assert post_data['body'].startswith('This pull request fixes #42.') assert post_data['head'] == 'openhands-fix-issue-42' - assert post_data['base'] == 'main' + assert post_data['base'] == (target_branch if target_branch else 'main') assert post_data['draft'] == (pr_type == 'draft') +@patch('requests.get') +def test_send_pull_request_invalid_target_branch( + mock_get, mock_github_issue, mock_output_dir, mock_llm_config +): + """Test that an error is raised when specifying a non-existent target branch""" + repo_path = os.path.join(mock_output_dir, 'repo') + + # Mock API response for non-existent branch + mock_get.side_effect = [ + MagicMock(status_code=404), # Branch doesn't exist + MagicMock(status_code=404), # Target branch doesn't exist + ] + + # Test that ValueError is raised when target branch doesn't exist + with pytest.raises( + ValueError, match='Target branch nonexistent-branch does not exist' + ): + send_pull_request( + github_issue=mock_github_issue, + github_token='test-token', + github_username='test-user', + patch_dir=repo_path, + pr_type='ready', + llm_config=mock_llm_config, + target_branch='nonexistent-branch', + ) + + # Verify API calls + assert mock_get.call_count == 2 + + @patch('subprocess.run') @patch('requests.post') @patch('requests.get') @@ -616,6 +667,7 @@ def test_process_single_pr_update( mock_llm_config, None, False, + None, ) mock_initialize_repo.assert_called_once_with(mock_output_dir, 1, 'pr', 'branch 1') @@ -688,6 +740,7 @@ def test_process_single_issue( mock_llm_config, None, False, + None, ) # Assert that the mocked functions were called with correct arguments @@ -704,9 +757,10 @@ def test_process_single_issue( github_username=github_username, patch_dir=f'{mock_output_dir}/patches/issue_1', pr_type=pr_type, + llm_config=mock_llm_config, fork_owner=None, additional_message=resolver_output.success_explanation, - llm_config=mock_llm_config, + target_branch=None, ) @@ -757,6 +811,7 @@ def test_process_single_issue_unsuccessful( mock_llm_config, None, False, + None, ) # Assert that none of the mocked functions were called @@ -863,6 +918,7 @@ def test_process_all_successful_issues( mock_llm_config, None, False, + None, ), call( 'output_dir', @@ -873,6 +929,7 @@ def test_process_all_successful_issues( mock_llm_config, None, False, + None, ), ] ) @@ -971,6 +1028,7 @@ def test_main( mock_args.llm_model = 'mock_model' mock_args.llm_base_url = 'mock_url' mock_args.llm_api_key = 'mock_key' + mock_args.target_branch = None mock_parser.return_value.parse_args.return_value = mock_args # Setup environment variables @@ -994,12 +1052,8 @@ def test_main( api_key=mock_args.llm_api_key, ) - # Assert function calls - mock_parser.assert_called_once() - mock_getenv.assert_any_call('GITHUB_TOKEN') - mock_path_exists.assert_called_with('/mock/output') - mock_load_single_resolver_output.assert_called_with('/mock/output/output.jsonl', 42) - mock_process_single_issue.assert_called_with( + # Use any_call instead of assert_called_with for more flexible matching + assert mock_process_single_issue.call_args == call( '/mock/output', mock_resolver_output, 'mock_token', @@ -1008,8 +1062,15 @@ def test_main( llm_config, None, False, + mock_args.target_branch, ) + # Other assertions + mock_parser.assert_called_once() + mock_getenv.assert_any_call('GITHUB_TOKEN') + mock_path_exists.assert_called_with('/mock/output') + mock_load_single_resolver_output.assert_called_with('/mock/output/output.jsonl', 42) + # Test for 'all_successful' issue number mock_args.issue_number = 'all_successful' main() From 1f723293db928ce665e7081da20603bd8e0469ef Mon Sep 17 00:00:00 2001 From: Rohit Malhotra Date: Tue, 19 Nov 2024 08:34:25 -0500 Subject: [PATCH 16/56] Add macro invocations to example workflow (#5121) --- .../resolver/examples/openhands-resolver.yml | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/openhands/resolver/examples/openhands-resolver.yml b/openhands/resolver/examples/openhands-resolver.yml index 6555e15057c7..fb40dde8d97e 100644 --- a/openhands/resolver/examples/openhands-resolver.yml +++ b/openhands/resolver/examples/openhands-resolver.yml @@ -7,6 +7,10 @@ on: types: [labeled] issue_comment: types: [created] + pull_request_review_comment: + types: [created] + pull_request_review: + types: [submitted] permissions: contents: write @@ -16,12 +20,20 @@ permissions: jobs: call-openhands-resolver: if: | - ${{ - github.event.label.name == 'fix-me' || - (github.event_name == 'issue_comment' && - startsWith(github.event.comment.body, vars.OPENHANDS_MACRO || '@openhands-agent') && - (github.event.comment.author_association == 'OWNER' || github.event.comment.author_association == 'COLLABORATOR' || github.event.comment.author_association == 'MEMBER')) - }} + github.event.label.name == 'fix-me' || + + ( + ((github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment') && + (startsWith(github.event.comment.body, inputs.macro || '@openhands-agent') || startsWith(github.event.comment.body, inputs.macro || vars.OPENHANDS_MACRO)) && + (github.event.comment.author_association == 'OWNER' || github.event.comment.author_association == 'COLLABORATOR' || github.event.comment.author_association == 'MEMBER') + ) || + + (github.event_name == 'pull_request_review' && + (startsWith(github.event.review.body, inputs.macro || '@openhands-agent') || startsWith(github.event.review.body, inputs.macro || vars.OPENHANDS_MACRO)) && + (github.event.review.author_association == 'OWNER' || github.event.review.author_association == 'COLLABORATOR' || github.event.review.author_association == 'MEMBER') + ) + ) + uses: All-Hands-AI/OpenHands/.github/workflows/openhands-resolver.yml@main with: macro: ${{ vars.OPENHANDS_MACRO || '@openhands-agent' }} From ff84a3eede3d2f4e52e25822d525d676193abc78 Mon Sep 17 00:00:00 2001 From: Xingyao Wang Date: Tue, 19 Nov 2024 10:41:27 -0600 Subject: [PATCH 17/56] chore: remove specified sid (#5127) --- evaluation/discoverybench/run_infer.py | 5 +---- openhands/core/main.py | 5 ++++- openhands/resolver/resolve_issue.py | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/evaluation/discoverybench/run_infer.py b/evaluation/discoverybench/run_infer.py index 1b3a98c37e14..7cfd2dbac7ad 100644 --- a/evaluation/discoverybench/run_infer.py +++ b/evaluation/discoverybench/run_infer.py @@ -250,9 +250,6 @@ def process_instance( config = get_config(metadata) - # use a session id for concurrent evaluation - sid = 'ID_' + str(instance.instance_id) - # Setup the logger properly, so you can run # multi-processing to parallelize the evaluation if reset_logger: @@ -284,7 +281,7 @@ def process_instance( instruction += AGENT_CLS_TO_INST_SUFFIX[metadata.agent_class] # Here's how you can run the agent (similar to the `main` function) and get the final task state - runtime = create_runtime(config, sid=sid) + runtime = create_runtime(config) call_async_from_sync(runtime.connect) initialize_runtime(runtime, instance.data_files) diff --git a/openhands/core/main.py b/openhands/core/main.py index 06dede3d5d55..d55aa0175102 100644 --- a/openhands/core/main.py +++ b/openhands/core/main.py @@ -59,7 +59,8 @@ def create_runtime( """Create a runtime for the agent to run on. config: The app config. - sid: The session id. + sid: (optional) The session id. IMPORTANT: please don't set this unless you know what you're doing. + Set it to incompatible value will cause unexpected behavior on RemoteRuntime. headless_mode: Whether the agent is run in headless mode. `create_runtime` is typically called within evaluation scripts, where we don't want to have the VSCode UI open, so it defaults to True. """ @@ -105,6 +106,8 @@ async def run_controller( Args: config: The app config. initial_user_action: An Action object containing initial user input + sid: (optional) The session id. IMPORTANT: please don't set this unless you know what you're doing. + Set it to incompatible value will cause unexpected behavior on RemoteRuntime. runtime: (optional) A runtime for the agent to run on. agent: (optional) A agent to run. exit_on_message: quit if agent asks for a message from user (optional) diff --git a/openhands/resolver/resolve_issue.py b/openhands/resolver/resolve_issue.py index 67eb20bee1e0..cbe8cfa6df61 100644 --- a/openhands/resolver/resolve_issue.py +++ b/openhands/resolver/resolve_issue.py @@ -199,7 +199,7 @@ async def process_issue( ) config.set_llm_config(llm_config) - runtime = create_runtime(config, sid=f'{issue.number}') + runtime = create_runtime(config) await runtime.connect() async def on_event(evt): From de07fcfddcc0ceb28ac1d31e739f65191e075dd9 Mon Sep 17 00:00:00 2001 From: Rohit Malhotra Date: Tue, 19 Nov 2024 12:17:55 -0500 Subject: [PATCH 18/56] Moving resolver settings to repo variables (#5130) --- openhands/resolver/examples/openhands-resolver.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openhands/resolver/examples/openhands-resolver.yml b/openhands/resolver/examples/openhands-resolver.yml index fb40dde8d97e..2e2f42be0bac 100644 --- a/openhands/resolver/examples/openhands-resolver.yml +++ b/openhands/resolver/examples/openhands-resolver.yml @@ -37,7 +37,7 @@ jobs: uses: All-Hands-AI/OpenHands/.github/workflows/openhands-resolver.yml@main with: macro: ${{ vars.OPENHANDS_MACRO || '@openhands-agent' }} - max_iterations: 50 + max_iterations: ${{ vars.OPENHANDS_MAX_ITER || 50 }} secrets: PAT_TOKEN: ${{ secrets.PAT_TOKEN }} PAT_USERNAME: ${{ secrets.PAT_USERNAME }} From 7f5022c8fe9238af4d2818782601f4ea4452e827 Mon Sep 17 00:00:00 2001 From: Rohit Malhotra Date: Tue, 19 Nov 2024 12:23:42 -0500 Subject: [PATCH 19/56] Refactor issue filtering (#5129) --- openhands/resolver/issue_definitions.py | 35 +- openhands/resolver/resolve_all_issues.py | 7 +- openhands/resolver/resolve_issue.py | 7 +- tests/unit/resolver/test_issue_handler.py | 425 +++++++++++---------- tests/unit/resolver/test_resolve_issues.py | 10 +- 5 files changed, 256 insertions(+), 228 deletions(-) diff --git a/openhands/resolver/issue_definitions.py b/openhands/resolver/issue_definitions.py index c9e386178adf..a0d0eb570aa7 100644 --- a/openhands/resolver/issue_definitions.py +++ b/openhands/resolver/issue_definitions.py @@ -18,7 +18,9 @@ class IssueHandlerInterface(ABC): issue_type: ClassVar[str] @abstractmethod - def get_converted_issues(self, comment_id: int | None = None) -> list[GithubIssue]: + def get_converted_issues( + self, issue_numbers: list[int] | None = None, comment_id: int | None = None + ) -> list[GithubIssue]: """Download issues from GitHub.""" pass @@ -138,13 +140,29 @@ def _get_issue_comments( return all_comments if all_comments else None - def get_converted_issues(self, comment_id: int | None = None) -> list[GithubIssue]: + def get_converted_issues( + self, issue_numbers: list[int] | None = None, comment_id: int | None = None + ) -> list[GithubIssue]: """Download issues from Github. Returns: List of Github issues. """ + + if not issue_numbers: + raise ValueError('Unspecified issue number') + all_issues = self._download_issues_from_github() + logger.info(f'Limiting resolving to issues {issue_numbers}.') + all_issues = [ + issue + for issue in all_issues + if issue['number'] in issue_numbers and 'pull_request' not in issue + ] + + if len(issue_numbers) == 1 and not all_issues: + raise ValueError(f'Issue {issue_numbers[0]} not found') + converted_issues = [] for issue in all_issues: if any([issue.get(key) is None for key in ['number', 'title', 'body']]): @@ -153,9 +171,6 @@ def get_converted_issues(self, comment_id: int | None = None) -> list[GithubIssu ) continue - if 'pull_request' in issue: - continue - # Get issue thread comments thread_comments = self._get_issue_comments( issue['number'], comment_id=comment_id @@ -486,8 +501,16 @@ def __get_context_from_external_issues_references( return closing_issues - def get_converted_issues(self, comment_id: int | None = None) -> list[GithubIssue]: + def get_converted_issues( + self, issue_numbers: list[int] | None = None, comment_id: int | None = None + ) -> list[GithubIssue]: + if not issue_numbers: + raise ValueError('Unspecified issue numbers') + all_issues = self._download_issues_from_github() + logger.info(f'Limiting resolving to issues {issue_numbers}.') + all_issues = [issue for issue in all_issues if issue['number'] in issue_numbers] + converted_issues = [] for issue in all_issues: # For PRs, body can be None diff --git a/openhands/resolver/resolve_all_issues.py b/openhands/resolver/resolve_all_issues.py index a561b24a61a7..01d076446e97 100644 --- a/openhands/resolver/resolve_all_issues.py +++ b/openhands/resolver/resolve_all_issues.py @@ -83,11 +83,10 @@ async def resolve_issues( issue_handler = issue_handler_factory(issue_type, owner, repo, token) # Load dataset - issues: list[GithubIssue] = issue_handler.get_converted_issues() + issues: list[GithubIssue] = issue_handler.get_converted_issues( + issue_numbers=issue_numbers + ) - if issue_numbers is not None: - issues = [issue for issue in issues if issue.number in issue_numbers] - logger.info(f'Limiting resolving to issues {issue_numbers}.') if limit_issues is not None: issues = issues[:limit_issues] logger.info(f'Limiting resolving to first {limit_issues} issues.') diff --git a/openhands/resolver/resolve_issue.py b/openhands/resolver/resolve_issue.py index cbe8cfa6df61..b371d8160b20 100644 --- a/openhands/resolver/resolve_issue.py +++ b/openhands/resolver/resolve_issue.py @@ -339,13 +339,10 @@ async def resolve_issue( # Load dataset issues: list[GithubIssue] = issue_handler.get_converted_issues( - comment_id=comment_id + issue_numbers=[issue_number], comment_id=comment_id ) - # Find the specific issue - issue = next((i for i in issues if i.number == issue_number), None) - if not issue: - raise ValueError(f'Issue {issue_number} not found') + issue = issues[0] if comment_id is not None: if ( diff --git a/tests/unit/resolver/test_issue_handler.py b/tests/unit/resolver/test_issue_handler.py index 7df7f0fe4027..d0c17d9088e9 100644 --- a/tests/unit/resolver/test_issue_handler.py +++ b/tests/unit/resolver/test_issue_handler.py @@ -1,17 +1,18 @@ -from unittest.mock import patch, MagicMock -from openhands.resolver.issue_definitions import IssueHandler, PRHandler -from openhands.resolver.github_issue import GithubIssue, ReviewThread -from openhands.events.action.message import MessageAction +from unittest.mock import MagicMock, patch + from openhands.core.config import LLMConfig +from openhands.events.action.message import MessageAction +from openhands.resolver.github_issue import GithubIssue, ReviewThread +from openhands.resolver.issue_definitions import IssueHandler, PRHandler def test_get_converted_issues_initializes_review_comments(): # Mock the necessary dependencies - with patch("requests.get") as mock_get: + with patch('requests.get') as mock_get: # Mock the response for issues mock_issues_response = MagicMock() mock_issues_response.json.return_value = [ - {"number": 1, "title": "Test Issue", "body": "Test Body"} + {'number': 1, 'title': 'Test Issue', 'body': 'Test Body'} ] # Mock the response for comments mock_comments_response = MagicMock() @@ -26,10 +27,10 @@ def test_get_converted_issues_initializes_review_comments(): ] # Need two comment responses because we make two API calls # Create an instance of IssueHandler - handler = IssueHandler("test-owner", "test-repo", "test-token") + handler = IssueHandler('test-owner', 'test-repo', 'test-token') # Get converted issues - issues = handler.get_converted_issues() + issues = handler.get_converted_issues(issue_numbers=[1]) # Verify that we got exactly one issue assert len(issues) == 1 @@ -39,35 +40,35 @@ def test_get_converted_issues_initializes_review_comments(): # Verify other fields are set correctly assert issues[0].number == 1 - assert issues[0].title == "Test Issue" - assert issues[0].body == "Test Body" - assert issues[0].owner == "test-owner" - assert issues[0].repo == "test-repo" + assert issues[0].title == 'Test Issue' + assert issues[0].body == 'Test Body' + assert issues[0].owner == 'test-owner' + assert issues[0].repo == 'test-repo' def test_pr_handler_guess_success_with_thread_comments(): # Create a PR handler instance - handler = PRHandler("test-owner", "test-repo", "test-token") + handler = PRHandler('test-owner', 'test-repo', 'test-token') # Create a mock issue with thread comments but no review comments issue = GithubIssue( - owner="test-owner", - repo="test-repo", + owner='test-owner', + repo='test-repo', number=1, - title="Test PR", - body="Test Body", - thread_comments=["First comment", "Second comment"], - closing_issues=["Issue description"], + title='Test PR', + body='Test Body', + thread_comments=['First comment', 'Second comment'], + closing_issues=['Issue description'], review_comments=None, thread_ids=None, - head_branch="test-branch", + head_branch='test-branch', ) # Create mock history - history = [MessageAction(content="Fixed the issue by implementing X and Y")] + history = [MessageAction(content='Fixed the issue by implementing X and Y')] # Create mock LLM config - llm_config = LLMConfig(model="test-model", api_key="test-key") + llm_config = LLMConfig(model='test-model', api_key='test-key') # Mock the LLM response mock_response = MagicMock() @@ -84,7 +85,7 @@ def test_pr_handler_guess_success_with_thread_comments(): ] # Test the guess_success method - with patch("litellm.completion", return_value=mock_response): + with patch('litellm.completion', return_value=mock_response): success, success_list, explanation = handler.guess_success( issue, history, llm_config ) @@ -92,39 +93,39 @@ def test_pr_handler_guess_success_with_thread_comments(): # Verify the results assert success is True assert success_list == [True] - assert "successfully address" in explanation + assert 'successfully address' in explanation def test_pr_handler_get_converted_issues_with_comments(): # Mock the necessary dependencies - with patch("requests.get") as mock_get: + with patch('requests.get') as mock_get: # Mock the response for PRs mock_prs_response = MagicMock() mock_prs_response.json.return_value = [ { - "number": 1, - "title": "Test PR", - "body": "Test Body fixes #1", - "head": {"ref": "test-branch"}, + 'number': 1, + 'title': 'Test PR', + 'body': 'Test Body fixes #1', + 'head': {'ref': 'test-branch'}, } ] # Mock the response for PR comments mock_comments_response = MagicMock() mock_comments_response.json.return_value = [ - {"body": "First comment"}, - {"body": "Second comment"}, + {'body': 'First comment'}, + {'body': 'Second comment'}, ] # Mock the response for PR metadata (GraphQL) mock_graphql_response = MagicMock() mock_graphql_response.json.return_value = { - "data": { - "repository": { - "pullRequest": { - "closingIssuesReferences": {"edges": []}, - "reviews": {"nodes": []}, - "reviewThreads": {"edges": []}, + 'data': { + 'repository': { + 'pullRequest': { + 'closingIssuesReferences': {'edges': []}, + 'reviews': {'nodes': []}, + 'reviewThreads': {'edges': []}, } } } @@ -138,7 +139,7 @@ def test_pr_handler_get_converted_issues_with_comments(): # Mock the response for fetching the external issue referenced in PR body mock_external_issue_response = MagicMock() mock_external_issue_response.json.return_value = { - "body": "This is additional context from an externally referenced issue." + 'body': 'This is additional context from an externally referenced issue.' } mock_get.side_effect = [ @@ -150,56 +151,56 @@ def test_pr_handler_get_converted_issues_with_comments(): ] # Mock the post request for GraphQL - with patch("requests.post") as mock_post: + with patch('requests.post') as mock_post: mock_post.return_value = mock_graphql_response # Create an instance of PRHandler - handler = PRHandler("test-owner", "test-repo", "test-token") + handler = PRHandler('test-owner', 'test-repo', 'test-token') # Get converted issues - prs = handler.get_converted_issues() + prs = handler.get_converted_issues(issue_numbers=[1]) # Verify that we got exactly one PR assert len(prs) == 1 # Verify that thread_comments are set correctly - assert prs[0].thread_comments == ["First comment", "Second comment"] + assert prs[0].thread_comments == ['First comment', 'Second comment'] # Verify other fields are set correctly assert prs[0].number == 1 - assert prs[0].title == "Test PR" - assert prs[0].body == "Test Body fixes #1" - assert prs[0].owner == "test-owner" - assert prs[0].repo == "test-repo" - assert prs[0].head_branch == "test-branch" + assert prs[0].title == 'Test PR' + assert prs[0].body == 'Test Body fixes #1' + assert prs[0].owner == 'test-owner' + assert prs[0].repo == 'test-repo' + assert prs[0].head_branch == 'test-branch' assert prs[0].closing_issues == [ - "This is additional context from an externally referenced issue." + 'This is additional context from an externally referenced issue.' ] def test_pr_handler_guess_success_only_review_comments(): # Create a PR handler instance - handler = PRHandler("test-owner", "test-repo", "test-token") + handler = PRHandler('test-owner', 'test-repo', 'test-token') # Create a mock issue with only review comments issue = GithubIssue( - owner="test-owner", - repo="test-repo", + owner='test-owner', + repo='test-repo', number=1, - title="Test PR", - body="Test Body", + title='Test PR', + body='Test Body', thread_comments=None, - closing_issues=["Issue description"], - review_comments=["Please fix the formatting", "Add more tests"], + closing_issues=['Issue description'], + review_comments=['Please fix the formatting', 'Add more tests'], thread_ids=None, - head_branch="test-branch", + head_branch='test-branch', ) # Create mock history - history = [MessageAction(content="Fixed the formatting and added more tests")] + history = [MessageAction(content='Fixed the formatting and added more tests')] # Create mock LLM config - llm_config = LLMConfig(model="test-model", api_key="test-key") + llm_config = LLMConfig(model='test-model', api_key='test-key') # Mock the LLM response mock_response = MagicMock() @@ -216,7 +217,7 @@ def test_pr_handler_guess_success_only_review_comments(): ] # Test the guess_success method - with patch("litellm.completion", return_value=mock_response): + with patch('litellm.completion', return_value=mock_response): success, success_list, explanation = handler.guess_success( issue, history, llm_config ) @@ -224,32 +225,32 @@ def test_pr_handler_guess_success_only_review_comments(): # Verify the results assert success is True assert success_list == [True] - assert "successfully address" in explanation + assert 'successfully address' in explanation def test_pr_handler_guess_success_no_comments(): # Create a PR handler instance - handler = PRHandler("test-owner", "test-repo", "test-token") + handler = PRHandler('test-owner', 'test-repo', 'test-token') # Create a mock issue with no comments issue = GithubIssue( - owner="test-owner", - repo="test-repo", + owner='test-owner', + repo='test-repo', number=1, - title="Test PR", - body="Test Body", + title='Test PR', + body='Test Body', thread_comments=None, - closing_issues=["Issue description"], + closing_issues=['Issue description'], review_comments=None, thread_ids=None, - head_branch="test-branch", + head_branch='test-branch', ) # Create mock history - history = [MessageAction(content="Fixed the issue")] + history = [MessageAction(content='Fixed the issue')] # Create mock LLM config - llm_config = LLMConfig(model="test-model", api_key="test-key") + llm_config = LLMConfig(model='test-model', api_key='test-key') # Test that it returns appropriate message when no comments are present success, success_list, explanation = handler.guess_success( @@ -257,29 +258,29 @@ def test_pr_handler_guess_success_no_comments(): ) assert success is False assert success_list is None - assert explanation == "No feedback was found to process" + assert explanation == 'No feedback was found to process' def test_get_issue_comments_with_specific_comment_id(): # Mock the necessary dependencies - with patch("requests.get") as mock_get: + with patch('requests.get') as mock_get: # Mock the response for comments mock_comments_response = MagicMock() mock_comments_response.json.return_value = [ - {"id": 123, "body": "First comment"}, - {"id": 456, "body": "Second comment"}, + {'id': 123, 'body': 'First comment'}, + {'id': 456, 'body': 'Second comment'}, ] mock_get.return_value = mock_comments_response # Create an instance of IssueHandler - handler = IssueHandler("test-owner", "test-repo", "test-token") + handler = IssueHandler('test-owner', 'test-repo', 'test-token') # Get comments with a specific comment_id specific_comment = handler._get_issue_comments(issue_number=1, comment_id=123) # Verify only the specific comment is returned - assert specific_comment == ["First comment"] + assert specific_comment == ['First comment'] def test_pr_handler_get_converted_issues_with_specific_thread_comment(): @@ -287,50 +288,50 @@ def test_pr_handler_get_converted_issues_with_specific_thread_comment(): specific_comment_id = 123 # Mock GraphQL response for review threads - with patch("requests.get") as mock_get: + with patch('requests.get') as mock_get: # Mock the response for PRs mock_prs_response = MagicMock() mock_prs_response.json.return_value = [ { - "number": 1, - "title": "Test PR", - "body": "Test Body", - "head": {"ref": "test-branch"}, + 'number': 1, + 'title': 'Test PR', + 'body': 'Test Body', + 'head': {'ref': 'test-branch'}, } ] # Mock the response for PR comments mock_comments_response = MagicMock() mock_comments_response.json.return_value = [ - {"body": "First comment", "id": 123}, - {"body": "Second comment", "id": 124}, + {'body': 'First comment', 'id': 123}, + {'body': 'Second comment', 'id': 124}, ] # Mock the response for PR metadata (GraphQL) mock_graphql_response = MagicMock() mock_graphql_response.json.return_value = { - "data": { - "repository": { - "pullRequest": { - "closingIssuesReferences": {"edges": []}, - "reviews": {"nodes": []}, - "reviewThreads": { - "edges": [ + 'data': { + 'repository': { + 'pullRequest': { + 'closingIssuesReferences': {'edges': []}, + 'reviews': {'nodes': []}, + 'reviewThreads': { + 'edges': [ { - "node": { - "id": "review-thread-1", - "isResolved": False, - "comments": { - "nodes": [ + 'node': { + 'id': 'review-thread-1', + 'isResolved': False, + 'comments': { + 'nodes': [ { - "fullDatabaseId": 121, - "body": "Specific review comment", - "path": "file1.txt", + 'fullDatabaseId': 121, + 'body': 'Specific review comment', + 'path': 'file1.txt', }, { - "fullDatabaseId": 456, - "body": "Another review comment", - "path": "file2.txt", + 'fullDatabaseId': 456, + 'body': 'Another review comment', + 'path': 'file2.txt', }, ] }, @@ -356,30 +357,32 @@ def test_pr_handler_get_converted_issues_with_specific_thread_comment(): ] # Mock the post request for GraphQL - with patch("requests.post") as mock_post: + with patch('requests.post') as mock_post: mock_post.return_value = mock_graphql_response # Create an instance of PRHandler - handler = PRHandler("test-owner", "test-repo", "test-token") + handler = PRHandler('test-owner', 'test-repo', 'test-token') # Get converted issues - prs = handler.get_converted_issues(comment_id=specific_comment_id) + prs = handler.get_converted_issues( + issue_numbers=[1], comment_id=specific_comment_id + ) # Verify that we got exactly one PR assert len(prs) == 1 # Verify that thread_comments are set correctly - assert prs[0].thread_comments == ["First comment"] + assert prs[0].thread_comments == ['First comment'] assert prs[0].review_comments == [] assert prs[0].review_threads == [] # Verify other fields are set correctly assert prs[0].number == 1 - assert prs[0].title == "Test PR" - assert prs[0].body == "Test Body" - assert prs[0].owner == "test-owner" - assert prs[0].repo == "test-repo" - assert prs[0].head_branch == "test-branch" + assert prs[0].title == 'Test PR' + assert prs[0].body == 'Test Body' + assert prs[0].owner == 'test-owner' + assert prs[0].repo == 'test-repo' + assert prs[0].head_branch == 'test-branch' def test_pr_handler_get_converted_issues_with_specific_review_thread_comment(): @@ -387,50 +390,50 @@ def test_pr_handler_get_converted_issues_with_specific_review_thread_comment(): specific_comment_id = 123 # Mock GraphQL response for review threads - with patch("requests.get") as mock_get: + with patch('requests.get') as mock_get: # Mock the response for PRs mock_prs_response = MagicMock() mock_prs_response.json.return_value = [ { - "number": 1, - "title": "Test PR", - "body": "Test Body", - "head": {"ref": "test-branch"}, + 'number': 1, + 'title': 'Test PR', + 'body': 'Test Body', + 'head': {'ref': 'test-branch'}, } ] # Mock the response for PR comments mock_comments_response = MagicMock() mock_comments_response.json.return_value = [ - {"body": "First comment", "id": 120}, - {"body": "Second comment", "id": 124}, + {'body': 'First comment', 'id': 120}, + {'body': 'Second comment', 'id': 124}, ] # Mock the response for PR metadata (GraphQL) mock_graphql_response = MagicMock() mock_graphql_response.json.return_value = { - "data": { - "repository": { - "pullRequest": { - "closingIssuesReferences": {"edges": []}, - "reviews": {"nodes": []}, - "reviewThreads": { - "edges": [ + 'data': { + 'repository': { + 'pullRequest': { + 'closingIssuesReferences': {'edges': []}, + 'reviews': {'nodes': []}, + 'reviewThreads': { + 'edges': [ { - "node": { - "id": "review-thread-1", - "isResolved": False, - "comments": { - "nodes": [ + 'node': { + 'id': 'review-thread-1', + 'isResolved': False, + 'comments': { + 'nodes': [ { - "fullDatabaseId": specific_comment_id, - "body": "Specific review comment", - "path": "file1.txt", + 'fullDatabaseId': specific_comment_id, + 'body': 'Specific review comment', + 'path': 'file1.txt', }, { - "fullDatabaseId": 456, - "body": "Another review comment", - "path": "file1.txt", + 'fullDatabaseId': 456, + 'body': 'Another review comment', + 'path': 'file1.txt', }, ] }, @@ -456,14 +459,16 @@ def test_pr_handler_get_converted_issues_with_specific_review_thread_comment(): ] # Mock the post request for GraphQL - with patch("requests.post") as mock_post: + with patch('requests.post') as mock_post: mock_post.return_value = mock_graphql_response # Create an instance of PRHandler - handler = PRHandler("test-owner", "test-repo", "test-token") + handler = PRHandler('test-owner', 'test-repo', 'test-token') # Get converted issues - prs = handler.get_converted_issues(comment_id=specific_comment_id) + prs = handler.get_converted_issues( + issue_numbers=[1], comment_id=specific_comment_id + ) # Verify that we got exactly one PR assert len(prs) == 1 @@ -475,17 +480,17 @@ def test_pr_handler_get_converted_issues_with_specific_review_thread_comment(): assert isinstance(prs[0].review_threads[0], ReviewThread) assert ( prs[0].review_threads[0].comment - == "Specific review comment\n---\nlatest feedback:\nAnother review comment\n" + == 'Specific review comment\n---\nlatest feedback:\nAnother review comment\n' ) - assert prs[0].review_threads[0].files == ["file1.txt"] + assert prs[0].review_threads[0].files == ['file1.txt'] # Verify other fields are set correctly assert prs[0].number == 1 - assert prs[0].title == "Test PR" - assert prs[0].body == "Test Body" - assert prs[0].owner == "test-owner" - assert prs[0].repo == "test-repo" - assert prs[0].head_branch == "test-branch" + assert prs[0].title == 'Test PR' + assert prs[0].body == 'Test Body' + assert prs[0].owner == 'test-owner' + assert prs[0].repo == 'test-repo' + assert prs[0].head_branch == 'test-branch' def test_pr_handler_get_converted_issues_with_specific_comment_and_issue_refs(): @@ -493,50 +498,50 @@ def test_pr_handler_get_converted_issues_with_specific_comment_and_issue_refs(): specific_comment_id = 123 # Mock GraphQL response for review threads - with patch("requests.get") as mock_get: + with patch('requests.get') as mock_get: # Mock the response for PRs mock_prs_response = MagicMock() mock_prs_response.json.return_value = [ { - "number": 1, - "title": "Test PR fixes #3", - "body": "Test Body", - "head": {"ref": "test-branch"}, + 'number': 1, + 'title': 'Test PR fixes #3', + 'body': 'Test Body', + 'head': {'ref': 'test-branch'}, } ] # Mock the response for PR comments mock_comments_response = MagicMock() mock_comments_response.json.return_value = [ - {"body": "First comment", "id": 120}, - {"body": "Second comment", "id": 124}, + {'body': 'First comment', 'id': 120}, + {'body': 'Second comment', 'id': 124}, ] # Mock the response for PR metadata (GraphQL) mock_graphql_response = MagicMock() mock_graphql_response.json.return_value = { - "data": { - "repository": { - "pullRequest": { - "closingIssuesReferences": {"edges": []}, - "reviews": {"nodes": []}, - "reviewThreads": { - "edges": [ + 'data': { + 'repository': { + 'pullRequest': { + 'closingIssuesReferences': {'edges': []}, + 'reviews': {'nodes': []}, + 'reviewThreads': { + 'edges': [ { - "node": { - "id": "review-thread-1", - "isResolved": False, - "comments": { - "nodes": [ + 'node': { + 'id': 'review-thread-1', + 'isResolved': False, + 'comments': { + 'nodes': [ { - "fullDatabaseId": specific_comment_id, - "body": "Specific review comment that references #6", - "path": "file1.txt", + 'fullDatabaseId': specific_comment_id, + 'body': 'Specific review comment that references #6', + 'path': 'file1.txt', }, { - "fullDatabaseId": 456, - "body": "Another review comment referencing #7", - "path": "file2.txt", + 'fullDatabaseId': 456, + 'body': 'Another review comment referencing #7', + 'path': 'file2.txt', }, ] }, @@ -557,13 +562,13 @@ def test_pr_handler_get_converted_issues_with_specific_comment_and_issue_refs(): # Mock the response for fetching the external issue referenced in PR body mock_external_issue_response_in_body = MagicMock() mock_external_issue_response_in_body.json.return_value = { - "body": "External context #1." + 'body': 'External context #1.' } # Mock the response for fetching the external issue referenced in review thread mock_external_issue_response_review_thread = MagicMock() mock_external_issue_response_review_thread.json.return_value = { - "body": "External context #2." + 'body': 'External context #2.' } mock_get.side_effect = [ @@ -576,14 +581,16 @@ def test_pr_handler_get_converted_issues_with_specific_comment_and_issue_refs(): ] # Mock the post request for GraphQL - with patch("requests.post") as mock_post: + with patch('requests.post') as mock_post: mock_post.return_value = mock_graphql_response # Create an instance of PRHandler - handler = PRHandler("test-owner", "test-repo", "test-token") + handler = PRHandler('test-owner', 'test-repo', 'test-token') # Get converted issues - prs = handler.get_converted_issues(comment_id=specific_comment_id) + prs = handler.get_converted_issues( + issue_numbers=[1], comment_id=specific_comment_id + ) # Verify that we got exactly one PR assert len(prs) == 1 @@ -595,52 +602,52 @@ def test_pr_handler_get_converted_issues_with_specific_comment_and_issue_refs(): assert isinstance(prs[0].review_threads[0], ReviewThread) assert ( prs[0].review_threads[0].comment - == "Specific review comment that references #6\n---\nlatest feedback:\nAnother review comment referencing #7\n" + == 'Specific review comment that references #6\n---\nlatest feedback:\nAnother review comment referencing #7\n' ) assert prs[0].closing_issues == [ - "External context #1.", - "External context #2.", + 'External context #1.', + 'External context #2.', ] # Only includes references inside comment ID and body PR # Verify other fields are set correctly assert prs[0].number == 1 - assert prs[0].title == "Test PR fixes #3" - assert prs[0].body == "Test Body" - assert prs[0].owner == "test-owner" - assert prs[0].repo == "test-repo" - assert prs[0].head_branch == "test-branch" + assert prs[0].title == 'Test PR fixes #3' + assert prs[0].body == 'Test Body' + assert prs[0].owner == 'test-owner' + assert prs[0].repo == 'test-repo' + assert prs[0].head_branch == 'test-branch' def test_pr_handler_get_converted_issues_with_duplicate_issue_refs(): # Mock the necessary dependencies - with patch("requests.get") as mock_get: + with patch('requests.get') as mock_get: # Mock the response for PRs mock_prs_response = MagicMock() mock_prs_response.json.return_value = [ { - "number": 1, - "title": "Test PR", - "body": "Test Body fixes #1", - "head": {"ref": "test-branch"}, + 'number': 1, + 'title': 'Test PR', + 'body': 'Test Body fixes #1', + 'head': {'ref': 'test-branch'}, } ] # Mock the response for PR comments mock_comments_response = MagicMock() mock_comments_response.json.return_value = [ - {"body": "First comment addressing #1"}, - {"body": "Second comment addressing #2"}, + {'body': 'First comment addressing #1'}, + {'body': 'Second comment addressing #2'}, ] # Mock the response for PR metadata (GraphQL) mock_graphql_response = MagicMock() mock_graphql_response.json.return_value = { - "data": { - "repository": { - "pullRequest": { - "closingIssuesReferences": {"edges": []}, - "reviews": {"nodes": []}, - "reviewThreads": {"edges": []}, + 'data': { + 'repository': { + 'pullRequest': { + 'closingIssuesReferences': {'edges': []}, + 'reviews': {'nodes': []}, + 'reviewThreads': {'edges': []}, } } } @@ -654,13 +661,13 @@ def test_pr_handler_get_converted_issues_with_duplicate_issue_refs(): # Mock the response for fetching the external issue referenced in PR body mock_external_issue_response_in_body = MagicMock() mock_external_issue_response_in_body.json.return_value = { - "body": "External context #1." + 'body': 'External context #1.' } # Mock the response for fetching the external issue referenced in review thread mock_external_issue_response_in_comment = MagicMock() mock_external_issue_response_in_comment.json.return_value = { - "body": "External context #2." + 'body': 'External context #2.' } mock_get.side_effect = [ @@ -673,32 +680,32 @@ def test_pr_handler_get_converted_issues_with_duplicate_issue_refs(): ] # Mock the post request for GraphQL - with patch("requests.post") as mock_post: + with patch('requests.post') as mock_post: mock_post.return_value = mock_graphql_response # Create an instance of PRHandler - handler = PRHandler("test-owner", "test-repo", "test-token") + handler = PRHandler('test-owner', 'test-repo', 'test-token') # Get converted issues - prs = handler.get_converted_issues() + prs = handler.get_converted_issues(issue_numbers=[1]) # Verify that we got exactly one PR assert len(prs) == 1 # Verify that thread_comments are set correctly assert prs[0].thread_comments == [ - "First comment addressing #1", - "Second comment addressing #2", + 'First comment addressing #1', + 'Second comment addressing #2', ] # Verify other fields are set correctly assert prs[0].number == 1 - assert prs[0].title == "Test PR" - assert prs[0].body == "Test Body fixes #1" - assert prs[0].owner == "test-owner" - assert prs[0].repo == "test-repo" - assert prs[0].head_branch == "test-branch" + assert prs[0].title == 'Test PR' + assert prs[0].body == 'Test Body fixes #1' + assert prs[0].owner == 'test-owner' + assert prs[0].repo == 'test-repo' + assert prs[0].head_branch == 'test-branch' assert prs[0].closing_issues == [ - "External context #1.", - "External context #2.", + 'External context #1.', + 'External context #2.', ] diff --git a/tests/unit/resolver/test_resolve_issues.py b/tests/unit/resolver/test_resolve_issues.py index 6eb3bb9f2767..3f08db7e2bea 100644 --- a/tests/unit/resolver/test_resolve_issues.py +++ b/tests/unit/resolver/test_resolve_issues.py @@ -112,7 +112,7 @@ def get_mock_response(url, *args, **kwargs): return mock_issues_response with patch('requests.get', side_effect=get_mock_response): - issues = handler.get_converted_issues() + issues = handler.get_converted_issues(issue_numbers=[1, 3]) assert len(issues) == 2 assert handler.issue_type == 'issue' @@ -225,7 +225,7 @@ def get_mock_response(url, *args, **kwargs): with patch('requests.get', side_effect=get_mock_response): with patch('requests.post', return_value=mock_graphql_response): - issues = handler.get_converted_issues() + issues = handler.get_converted_issues(issue_numbers=[1, 2, 3]) assert len(issues) == 3 assert handler.issue_type == 'pr' @@ -811,7 +811,7 @@ def get_mock_response(url, *args, **kwargs): with patch('requests.get', side_effect=get_mock_response): with patch('requests.post', return_value=mock_graphql_response): - issues = handler.get_converted_issues() + issues = handler.get_converted_issues(issue_numbers=[1]) assert len(issues) == 1 assert handler.issue_type == 'pr' @@ -867,7 +867,9 @@ def get_mock_response(url, *args, **kwargs): return mock_issue_response with patch('requests.get', side_effect=get_mock_response): - issues = handler.get_converted_issues(comment_id=specific_comment_id) + issues = handler.get_converted_issues( + issue_numbers=[1], comment_id=specific_comment_id + ) assert len(issues) == 1 assert issues[0].number == 1 From f0ca45c59ef8586c5846703f86eb69dfbd265ef4 Mon Sep 17 00:00:00 2001 From: Rohit Malhotra Date: Tue, 19 Nov 2024 12:26:11 -0500 Subject: [PATCH 20/56] Add clarity for Openhands-resolver guide (#5124) --- docs/modules/usage/how-to/github-action.md | 35 ++++++++++++++++++++-- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/docs/modules/usage/how-to/github-action.md b/docs/modules/usage/how-to/github-action.md index 70c43beea174..3b55ab4952a7 100644 --- a/docs/modules/usage/how-to/github-action.md +++ b/docs/modules/usage/how-to/github-action.md @@ -4,13 +4,42 @@ This guide explains how to use the OpenHands GitHub Action, both within the Open ## Using the Action in the OpenHands Repository -To use the OpenHands GitHub Action in the OpenHands repository, an OpenHands maintainer can: +To use the OpenHands GitHub Action in a repository, you can: 1. Create an issue in the repository. -2. Add the `fix-me` label to the issue. -3. The action will automatically trigger and attempt to resolve the issue. +2. Add the `fix-me` label to the issue or leave a comment on the issue starting with `@openhands-agent`. + +The action will automatically trigger and attempt to resolve the issue. ## Installing the Action in a New Repository To install the OpenHands GitHub Action in your own repository, follow the [README for the OpenHands Resolver](https://github.com/All-Hands-AI/OpenHands/blob/main/openhands/resolver/README.md). + +## Usage Tips + +### Iterative resolution + +1. Create an issue in the repository. +2. Add the `fix-me` label to the issue, or leave a comment starting with `@openhands-agent` +3. Review the attempt to resolve the issue by checking the pull request +4. Follow up with feedback through general comments, review comments, or inline thread comments +5. Add the `fix-me` label to the pull request, or address a specific comment by starting with `@openhands-agent` + +### Label versus Macro + +- Label (`fix-me`): Requests OpenHands to address the **entire** issue or pull request. +- Macro (`@openhands-agent`): Requests OpenHands to consider only the issue/pull request description and **the specific comment**. + +## Advanced Settings + +### Add custom repository settings + +You can provide custom directions for OpenHands by following the [README for the resolver](https://github.com/All-Hands-AI/OpenHands/blob/main/openhands/resolver/README.md#providing-custom-instructions). + +### Configure custom macro + +To customize the default macro (`@openhands-agent`): + +1. [Create a repository variable](https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/store-information-in-variables#creating-configuration-variables-for-a-repository) named `OPENHANDS_MACRO` +2. Assign the variable a custom value From e052c25572fb3f9001b47a627d86048a5c65b548 Mon Sep 17 00:00:00 2001 From: Robert Brennan Date: Tue, 19 Nov 2024 12:49:20 -0500 Subject: [PATCH 21/56] Fix GitHub prompt (#5123) --- openhands/agenthub/codeact_agent/micro/github.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/openhands/agenthub/codeact_agent/micro/github.md b/openhands/agenthub/codeact_agent/micro/github.md index abd48fa5c75c..c03bb546cd80 100644 --- a/openhands/agenthub/codeact_agent/micro/github.md +++ b/openhands/agenthub/codeact_agent/micro/github.md @@ -21,11 +21,9 @@ Here are some instructions for pushing, but ONLY do this if the user asks you to * After opening or updating a pull request, send the user a short message with a link to the pull request. * Do all of the above in as few steps as possible. E.g. you could open a PR with one step by running the following bash commands: ```bash -git checkout -b create-widget -git add . -git commit -m "Create widget" -git push origin create-widget -curl -X POST "https://api.github.com/repos/CodeActOrg/openhands/pulls" \ +git remote -v && git branch # to find the current org, repo and branch +git checkout -b create-widget && git add . && git commit -m "Create widget" && git push -u origin create-widget +curl -X POST "https://api.github.com/repos/$ORG_NAME/$REPO_NAME/pulls" \ -H "Authorization: Bearer $GITHUB_TOKEN" \ -d '{"title":"Create widget","head":"create-widget","base":"openhands-workspace"}' ``` From c9ed9b166be89d448c01a2ada5ef4ee525eb74b3 Mon Sep 17 00:00:00 2001 From: Robert Brennan Date: Tue, 19 Nov 2024 13:46:03 -0500 Subject: [PATCH 22/56] handle exceptions more explicitly (#4971) --- openhands/controller/agent_controller.py | 4 ++-- openhands/core/logger.py | 9 ++++++-- openhands/runtime/base.py | 12 +++++++++-- .../impl/eventstream/eventstream_runtime.py | 20 ++++++++++++++++-- .../runtime/impl/remote/remote_runtime.py | 5 ++++- openhands/server/listen.py | 21 ++++++++++++++++--- openhands/server/session/agent_session.py | 6 +++--- openhands/server/session/manager.py | 7 ++++++- 8 files changed, 68 insertions(+), 16 deletions(-) diff --git a/openhands/controller/agent_controller.py b/openhands/controller/agent_controller.py index f9e4a8edb555..e0fa0dab0384 100644 --- a/openhands/controller/agent_controller.py +++ b/openhands/controller/agent_controller.py @@ -18,6 +18,7 @@ LLMNoActionError, LLMResponseError, ) +from openhands.core.logger import LOG_ALL_EVENTS from openhands.core.logger import openhands_logger as logger from openhands.core.schema import AgentState from openhands.events import EventSource, EventStream, EventStreamSubscriber @@ -528,8 +529,7 @@ async def _step(self) -> None: await self.update_state_after_step() - # Use info level if LOG_ALL_EVENTS is set - log_level = 'info' if os.getenv('LOG_ALL_EVENTS') in ('true', '1') else 'debug' + log_level = 'info' if LOG_ALL_EVENTS else 'debug' self.log(log_level, str(action), extra={'msg_type': 'ACTION'}) async def _delegate_step(self): diff --git a/openhands/core/logger.py b/openhands/core/logger.py index 1450e503515e..238b4c39435c 100644 --- a/openhands/core/logger.py +++ b/openhands/core/logger.py @@ -17,6 +17,8 @@ LOG_TO_FILE = os.getenv('LOG_TO_FILE', 'False').lower() in ['true', '1', 'yes'] DISABLE_COLOR_PRINTING = False +LOG_ALL_EVENTS = os.getenv('LOG_ALL_EVENTS', 'False').lower() in ['true', '1', 'yes'] + ColorType = Literal[ 'red', 'green', @@ -89,8 +91,11 @@ def format(self, record): return f'{time_str} - {name_str}:{level_str}: {record.filename}:{record.lineno}\n{msg_type_color}\n{msg}' return f'{time_str} - {msg_type_color}\n{msg}' elif msg_type == 'STEP': - msg = '\n\n==============\n' + record.msg + '\n' - return f'{msg}' + if LOG_ALL_EVENTS: + msg = '\n\n==============\n' + record.msg + '\n' + return f'{msg}' + else: + return record.msg return super().format(record) diff --git a/openhands/runtime/base.py b/openhands/runtime/base.py index b12c501c19f3..74891a7d52b0 100644 --- a/openhands/runtime/base.py +++ b/openhands/runtime/base.py @@ -47,11 +47,19 @@ } -class RuntimeNotReadyError(Exception): +class RuntimeUnavailableError(Exception): pass -class RuntimeDisconnectedError(Exception): +class RuntimeNotReadyError(RuntimeUnavailableError): + pass + + +class RuntimeDisconnectedError(RuntimeUnavailableError): + pass + + +class RuntimeNotFoundError(RuntimeUnavailableError): pass diff --git a/openhands/runtime/impl/eventstream/eventstream_runtime.py b/openhands/runtime/impl/eventstream/eventstream_runtime.py index e65c36cc67c6..fe8f67f29552 100644 --- a/openhands/runtime/impl/eventstream/eventstream_runtime.py +++ b/openhands/runtime/impl/eventstream/eventstream_runtime.py @@ -34,7 +34,11 @@ ) from openhands.events.serialization import event_to_dict, observation_from_dict from openhands.events.serialization.action import ACTION_TYPE_TO_CLASS -from openhands.runtime.base import Runtime +from openhands.runtime.base import ( + Runtime, + RuntimeDisconnectedError, + RuntimeNotFoundError, +) from openhands.runtime.builder import DockerRuntimeBuilder from openhands.runtime.impl.eventstream.containers import remove_all_containers from openhands.runtime.plugins import PluginRequirement @@ -424,10 +428,22 @@ def _refresh_logs(self): @tenacity.retry( stop=tenacity.stop_after_delay(120) | stop_if_should_exit(), - reraise=(ConnectionRefusedError,), + retry=tenacity.retry_if_exception_type( + (ConnectionError, requests.exceptions.ConnectionError) + ), + reraise=True, wait=tenacity.wait_fixed(2), ) def _wait_until_alive(self): + try: + container = self.docker_client.containers.get(self.container_name) + if container.status == 'exited': + raise RuntimeDisconnectedError( + f'Container {self.container_name} has exited.' + ) + except docker.errors.NotFound: + raise RuntimeNotFoundError(f'Container {self.container_name} not found.') + self._refresh_logs() if not self.log_buffer: raise RuntimeError('Runtime client is not ready.') diff --git a/openhands/runtime/impl/remote/remote_runtime.py b/openhands/runtime/impl/remote/remote_runtime.py index 4191a047b1c2..d996441b66c9 100644 --- a/openhands/runtime/impl/remote/remote_runtime.py +++ b/openhands/runtime/impl/remote/remote_runtime.py @@ -31,6 +31,7 @@ from openhands.runtime.base import ( Runtime, RuntimeDisconnectedError, + RuntimeNotFoundError, RuntimeNotReadyError, ) from openhands.runtime.builder.remote import RemoteRuntimeBuilder @@ -109,7 +110,9 @@ def _start_or_attach_to_runtime(self): if existing_runtime: self.log('debug', f'Using existing runtime with ID: {self.runtime_id}') elif self.attach_to_existing: - raise RuntimeError('Could not find existing runtime to attach to.') + raise RuntimeNotFoundError( + f'Could not find existing runtime for SID: {self.sid}' + ) else: self.send_status_message('STATUS$STARTING_CONTAINER') if self.config.sandbox.runtime_container_image is None: diff --git a/openhands/server/listen.py b/openhands/server/listen.py index 8724daf1905f..929a26ec987d 100644 --- a/openhands/server/listen.py +++ b/openhands/server/listen.py @@ -34,6 +34,7 @@ Request, UploadFile, WebSocket, + WebSocketDisconnect, status, ) from fastapi.responses import FileResponse, JSONResponse @@ -238,7 +239,8 @@ async def attach_session(request: Request, call_next): request.state.conversation = await session_manager.attach_to_conversation( request.state.sid ) - if request.state.conversation is None: + if not request.state.conversation: + logger.error(f'Runtime not found for session: {request.state.sid}') return JSONResponse( status_code=status.HTTP_404_NOT_FOUND, content={'error': 'Session not found'}, @@ -344,7 +346,13 @@ async def websocket_endpoint(websocket: WebSocket): latest_event_id = -1 if websocket.query_params.get('latest_event_id'): - latest_event_id = int(websocket.query_params.get('latest_event_id')) + try: + latest_event_id = int(websocket.query_params.get('latest_event_id')) + except ValueError: + logger.warning( + f'Invalid latest_event_id: {websocket.query_params.get("latest_event_id")}' + ) + pass async_stream = AsyncEventStreamWrapper( session.agent_session.event_stream, latest_event_id + 1 @@ -361,7 +369,14 @@ async def websocket_endpoint(websocket: WebSocket): ), ): continue - await websocket.send_json(event_to_dict(event)) + try: + await websocket.send_json(event_to_dict(event)) + except WebSocketDisconnect: + logger.warning( + 'Websocket disconnected while sending event history, before loop started' + ) + session.close() + return await session.loop_recv() diff --git a/openhands/server/session/agent_session.py b/openhands/server/session/agent_session.py index 8f9d20a5dc6e..76f6e2aa8bcb 100644 --- a/openhands/server/session/agent_session.py +++ b/openhands/server/session/agent_session.py @@ -11,7 +11,7 @@ from openhands.events.event import EventSource from openhands.events.stream import EventStream from openhands.runtime import get_runtime_cls -from openhands.runtime.base import Runtime +from openhands.runtime.base import Runtime, RuntimeUnavailableError from openhands.security import SecurityAnalyzer, options from openhands.storage.files import FileStore @@ -194,13 +194,13 @@ async def _create_runtime( try: await self.runtime.connect() - except Exception as e: + except RuntimeUnavailableError as e: logger.error(f'Runtime initialization failed: {e}', exc_info=True) if self._status_callback: self._status_callback( 'error', 'STATUS$ERROR_RUNTIME_DISCONNECTED', str(e) ) - raise + return if self.runtime is not None: logger.debug( diff --git a/openhands/server/session/manager.py b/openhands/server/session/manager.py index f746b3676e29..790b7c4bd1eb 100644 --- a/openhands/server/session/manager.py +++ b/openhands/server/session/manager.py @@ -6,6 +6,7 @@ from openhands.core.config import AppConfig from openhands.core.logger import openhands_logger as logger from openhands.events.stream import session_exists +from openhands.runtime.base import RuntimeUnavailableError from openhands.server.session.conversation import Conversation from openhands.server.session.session import Session from openhands.storage.files import FileStore @@ -26,7 +27,11 @@ async def attach_to_conversation(self, sid: str) -> Conversation | None: if not await session_exists(sid, self.file_store): return None c = Conversation(sid, file_store=self.file_store, config=self.config) - await c.connect() + try: + await c.connect() + except RuntimeUnavailableError as e: + logger.error(f'Error connecting to conversation {c.sid}: {e}') + return None end_time = time.time() logger.info( f'Conversation {c.sid} connected in {end_time - start_time} seconds' From 3c61a9521b5718754e34600f86a7397b4f8d1856 Mon Sep 17 00:00:00 2001 From: Robert Brennan Date: Tue, 19 Nov 2024 13:46:14 -0500 Subject: [PATCH 23/56] Simple initial rate limiting implementation (#4976) --- openhands/server/listen.py | 11 ++++++- openhands/server/middleware.py | 57 ++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/openhands/server/listen.py b/openhands/server/listen.py index 929a26ec987d..433b13bde208 100644 --- a/openhands/server/listen.py +++ b/openhands/server/listen.py @@ -64,7 +64,12 @@ from openhands.llm import bedrock from openhands.runtime.base import Runtime from openhands.server.auth.auth import get_sid_from_token, sign_token -from openhands.server.middleware import LocalhostCORSMiddleware, NoCacheMiddleware +from openhands.server.middleware import ( + InMemoryRateLimiter, + LocalhostCORSMiddleware, + NoCacheMiddleware, + RateLimitMiddleware, +) from openhands.server.session import SessionManager load_dotenv() @@ -84,6 +89,10 @@ app.add_middleware(NoCacheMiddleware) +app.add_middleware( + RateLimitMiddleware, rate_limiter=InMemoryRateLimiter(requests=2, seconds=1) +) + security_scheme = HTTPBearer() diff --git a/openhands/server/middleware.py b/openhands/server/middleware.py index 218a949fca58..872241fc865f 100644 --- a/openhands/server/middleware.py +++ b/openhands/server/middleware.py @@ -1,6 +1,11 @@ +import asyncio +from collections import defaultdict +from datetime import datetime, timedelta from urllib.parse import urlparse +from fastapi import Request from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import JSONResponse from starlette.middleware.base import BaseHTTPMiddleware from starlette.types import ASGIApp @@ -41,3 +46,55 @@ async def dispatch(self, request, call_next): response.headers['Pragma'] = 'no-cache' response.headers['Expires'] = '0' return response + + +class InMemoryRateLimiter: + history: dict + requests: int + seconds: int + sleep_seconds: int + + def __init__(self, requests: int = 2, seconds: int = 1, sleep_seconds: int = 1): + self.requests = requests + self.seconds = seconds + self.history = defaultdict(list) + + def _clean_old_requests(self, key: str) -> None: + now = datetime.now() + cutoff = now - timedelta(seconds=self.seconds) + self.history[key] = [ts for ts in self.history[key] if ts > cutoff] + + async def __call__(self, request: Request) -> bool: + key = request.client.host + now = datetime.now() + + self._clean_old_requests(key) + + self.history[key].append(now) + + if len(self.history[key]) > self.requests * 2: + return False + elif len(self.history[key]) > self.requests: + if self.sleep_seconds > 0: + await asyncio.sleep(self.sleep_seconds) + return True + else: + return False + + return True + + +class RateLimitMiddleware(BaseHTTPMiddleware): + def __init__(self, app: ASGIApp, rate_limiter: InMemoryRateLimiter): + super().__init__(app) + self.rate_limiter = rate_limiter + + async def dispatch(self, request, call_next): + ok = await self.rate_limiter(request) + if not ok: + return JSONResponse( + status_code=429, + content={'message': 'Too many requests'}, + headers={'Retry-After': '1'}, + ) + return await call_next(request) From 302e41d7bb3d5b2b319f1ce2d15e5925dda069a2 Mon Sep 17 00:00:00 2001 From: mamoodi Date: Tue, 19 Nov 2024 14:53:24 -0500 Subject: [PATCH 24/56] Release 0.14.1 (#5133) --- frontend/package-lock.json | 2 +- frontend/package.json | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 2d45a56c6c11..cfd8519e1adb 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,6 +1,6 @@ { "name": "openhands-frontend", - "version": "0.14.0", + "version": "0.14.1", "lockfileVersion": 3, "requires": true, "packages": { diff --git a/frontend/package.json b/frontend/package.json index c1415b02bd5a..1757adbe8ac3 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "openhands-frontend", - "version": "0.14.0", + "version": "0.14.1", "private": true, "type": "module", "engines": { diff --git a/pyproject.toml b/pyproject.toml index 1caad8bf9fc7..53648ae7d8e8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "openhands-ai" -version = "0.14.0" +version = "0.14.1" description = "OpenHands: Code Less, Make More" authors = ["OpenHands"] license = "MIT" From 018080aae0ea01f466f7f7297431a8d67c2f86c7 Mon Sep 17 00:00:00 2001 From: Robert Brennan Date: Tue, 19 Nov 2024 17:01:07 -0500 Subject: [PATCH 25/56] fix rate limiting (#5135) --- frontend/package-lock.json | 2 +- openhands/server/listen.py | 2 +- openhands/server/middleware.py | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index cfd8519e1adb..a5abe9cc6a58 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -6,7 +6,7 @@ "packages": { "": { "name": "openhands-frontend", - "version": "0.14.0", + "version": "0.14.1", "dependencies": { "@monaco-editor/react": "^4.6.0", "@nextui-org/react": "^2.4.8", diff --git a/openhands/server/listen.py b/openhands/server/listen.py index 433b13bde208..95d61f434a90 100644 --- a/openhands/server/listen.py +++ b/openhands/server/listen.py @@ -90,7 +90,7 @@ app.add_middleware(NoCacheMiddleware) app.add_middleware( - RateLimitMiddleware, rate_limiter=InMemoryRateLimiter(requests=2, seconds=1) + RateLimitMiddleware, rate_limiter=InMemoryRateLimiter(requests=10, seconds=1) ) diff --git a/openhands/server/middleware.py b/openhands/server/middleware.py index 872241fc865f..1938e253b68a 100644 --- a/openhands/server/middleware.py +++ b/openhands/server/middleware.py @@ -57,6 +57,7 @@ class InMemoryRateLimiter: def __init__(self, requests: int = 2, seconds: int = 1, sleep_seconds: int = 1): self.requests = requests self.seconds = seconds + self.sleep_seconds = sleep_seconds self.history = defaultdict(list) def _clean_old_requests(self, key: str) -> None: From a3977621ed25db336f71006e2d197c1644505e34 Mon Sep 17 00:00:00 2001 From: Robert Brennan Date: Tue, 19 Nov 2024 17:40:20 -0500 Subject: [PATCH 26/56] Add /health endpoint to server (#5136) Co-authored-by: openhands --- openhands/server/listen.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openhands/server/listen.py b/openhands/server/listen.py index 95d61f434a90..f5463a517306 100644 --- a/openhands/server/listen.py +++ b/openhands/server/listen.py @@ -94,6 +94,11 @@ ) +@app.get('/health') +async def health(): + return 'OK' + + security_scheme = HTTPBearer() From 2a78b3323b98b82195044e18c13d53cf1e2c8ac6 Mon Sep 17 00:00:00 2001 From: Rohit Malhotra Date: Tue, 19 Nov 2024 17:42:49 -0500 Subject: [PATCH 27/56] Adding experimental option for resolver macro (#5131) --- .github/workflows/openhands-resolver.yml | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/.github/workflows/openhands-resolver.yml b/.github/workflows/openhands-resolver.yml index 44875b177ec5..b517f4c46e27 100644 --- a/.github/workflows/openhands-resolver.yml +++ b/.github/workflows/openhands-resolver.yml @@ -40,7 +40,6 @@ permissions: issues: write jobs: - auto-fix: if: | github.event_name == 'workflow_call' || @@ -76,7 +75,18 @@ jobs: cat requirements.txt - name: Cache pip dependencies - if: github.event.label.name != 'fix-me-experimental' + if: | + !( + github.event.label.name == 'fix-me-experimental' || + ( + (github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment') && + startsWith(github.event.comment.body, inputs.macro || '@openhands-agent-exp') + ) || + ( + github.event_name == 'pull_request_review' && + startsWith(github.event.review.body, inputs.macro || '@openhands-agent-exp') + ) + ) uses: actions/cache@v3 with: path: ${{ env.pythonLocation }}/lib/python3.12/site-packages/* @@ -140,7 +150,11 @@ jobs: - name: Install OpenHands run: | - if [ "${{ github.event.label.name }}" == "fix-me-experimental" ]; then + if [[ "${{ github.event.label.name }}" == "fix-me-experimental" ]] || + ([[ "${{ github.event_name }}" == "issue_comment" || "${{ github.event_name }}" == "pull_request_review_comment" ]] && + [[ "${{ github.event.comment.body }}" == "@openhands-agent-exp"* ]]) || + ([[ "${{ github.event_name }}" == "pull_request_review" ]] && + [[ "${{ github.event.review.body }}" == "@openhands-agent-exp"* ]]); then python -m pip install --upgrade pip pip install git+https://github.com/all-hands-ai/openhands.git else From 24a83eb52d13db6a065b81c8d4e51c915eef7af0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 20 Nov 2024 14:48:40 +0000 Subject: [PATCH 28/56] Bump the docusaurus group in /docs with 7 updates (#5140) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- docs/package-lock.json | 2936 +++++++++++++++++++++++++++++++++------- docs/package.json | 10 +- docs/yarn.lock | 1059 ++++++++++++--- 3 files changed, 3302 insertions(+), 703 deletions(-) diff --git a/docs/package-lock.json b/docs/package-lock.json index dd2dbf1f3260..fae84142406a 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -8,10 +8,10 @@ "name": "docs", "version": "0.0.0", "dependencies": { - "@docusaurus/core": "^3.6.0", - "@docusaurus/plugin-content-pages": "^3.6.0", - "@docusaurus/preset-classic": "^3.6.0", - "@docusaurus/theme-mermaid": "^3.6.0", + "@docusaurus/core": "^3.6.2", + "@docusaurus/plugin-content-pages": "^3.6.2", + "@docusaurus/preset-classic": "^3.6.2", + "@docusaurus/theme-mermaid": "^3.6.2", "@mdx-js/react": "^3.1.0", "clsx": "^2.0.0", "prism-react-renderer": "^2.4.0", @@ -22,7 +22,7 @@ }, "devDependencies": { "@docusaurus/module-type-aliases": "^3.5.1", - "@docusaurus/tsconfig": "^3.6.0", + "@docusaurus/tsconfig": "^3.6.2", "@docusaurus/types": "^3.5.1", "typescript": "~5.6.3" }, @@ -31,31 +31,31 @@ } }, "node_modules/@algolia/autocomplete-core": { - "version": "1.17.6", - "resolved": "https://registry.npmjs.org/@algolia/autocomplete-core/-/autocomplete-core-1.17.6.tgz", - "integrity": "sha512-lkDoW4I7h2kKlIgf3pUt1LqvxyYKkVyiypoGLlUnhPSnCpmeOwudM6rNq6YYsCmdQtnDQoW5lUNNuj6ASg3qeg==", + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-core/-/autocomplete-core-1.17.7.tgz", + "integrity": "sha512-BjiPOW6ks90UKl7TwMv7oNQMnzU+t/wk9mgIDi6b1tXpUek7MW0lbNOUHpvam9pe3lVCf4xPFT+lK7s+e+fs7Q==", "dependencies": { - "@algolia/autocomplete-plugin-algolia-insights": "1.17.6", - "@algolia/autocomplete-shared": "1.17.6" + "@algolia/autocomplete-plugin-algolia-insights": "1.17.7", + "@algolia/autocomplete-shared": "1.17.7" } }, "node_modules/@algolia/autocomplete-plugin-algolia-insights": { - "version": "1.17.6", - "resolved": "https://registry.npmjs.org/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.17.6.tgz", - "integrity": "sha512-17NnaacuFzSWVuZu4NKzVeaFIe9Abpw8w+/gjc7xhZFtqj+GadufzodIdchwiB2eM2cDdiR3icW7gbNTB3K2YA==", + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.17.7.tgz", + "integrity": "sha512-Jca5Ude6yUOuyzjnz57og7Et3aXjbwCSDf/8onLHSQgw1qW3ALl9mrMWaXb5FmPVkV3EtkD2F/+NkT6VHyPu9A==", "dependencies": { - "@algolia/autocomplete-shared": "1.17.6" + "@algolia/autocomplete-shared": "1.17.7" }, "peerDependencies": { "search-insights": ">= 1 < 3" } }, "node_modules/@algolia/autocomplete-preset-algolia": { - "version": "1.17.6", - "resolved": "https://registry.npmjs.org/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.17.6.tgz", - "integrity": "sha512-Cvg5JENdSCMuClwhJ1ON1/jSuojaYMiUW2KePm18IkdCzPJj/NXojaOxw58RFtQFpJgfVW8h2E8mEoDtLlMdeA==", + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.17.7.tgz", + "integrity": "sha512-ggOQ950+nwbWROq2MOCIL71RE0DdQZsceqrg32UqnhDz8FlO9rL8ONHNsI2R1MH0tkgVIDKI/D0sMiUchsFdWA==", "dependencies": { - "@algolia/autocomplete-shared": "1.17.6" + "@algolia/autocomplete-shared": "1.17.7" }, "peerDependencies": { "@algolia/client-search": ">= 4.9.1 < 6", @@ -63,9 +63,9 @@ } }, "node_modules/@algolia/autocomplete-shared": { - "version": "1.17.6", - "resolved": "https://registry.npmjs.org/@algolia/autocomplete-shared/-/autocomplete-shared-1.17.6.tgz", - "integrity": "sha512-aq/3V9E00Tw2GC/PqgyPGXtqJUlVc17v4cn1EUhSc+O/4zd04Uwb3UmPm8KDaYQQOrkt1lwvCj2vG2wRE5IKhw==", + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-shared/-/autocomplete-shared-1.17.7.tgz", + "integrity": "sha512-o/1Vurr42U/qskRSuhBH+VKxMvkkUVTLU6WZQr+L5lGZZLYWyhdzWjW0iGXY7EkwRTjBqvN2EsR81yCTGV/kmg==", "peerDependencies": { "@algolia/client-search": ">= 4.9.1 < 6", "algoliasearch": ">= 4.9.1 < 6" @@ -93,14 +93,14 @@ } }, "node_modules/@algolia/client-abtesting": { - "version": "5.12.0", - "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.12.0.tgz", - "integrity": "sha512-hx4eVydkm3yrFCFxmcBtSzI/ykt0cZ6sDWch+v3JTgKpD2WtosMJU3Upv1AjQ4B6COSHCOWEX3vfFxW6OoH6aA==", + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.15.0.tgz", + "integrity": "sha512-FaEM40iuiv1mAipYyiptP4EyxkJ8qHfowCpEeusdHUC4C7spATJYArD2rX3AxkVeREkDIgYEOuXcwKUbDCr7Nw==", "dependencies": { - "@algolia/client-common": "5.12.0", - "@algolia/requester-browser-xhr": "5.12.0", - "@algolia/requester-fetch": "5.12.0", - "@algolia/requester-node-http": "5.12.0" + "@algolia/client-common": "5.15.0", + "@algolia/requester-browser-xhr": "5.15.0", + "@algolia/requester-fetch": "5.15.0", + "@algolia/requester-node-http": "5.15.0" }, "engines": { "node": ">= 14.0.0" @@ -166,22 +166,22 @@ } }, "node_modules/@algolia/client-common": { - "version": "5.12.0", - "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.12.0.tgz", - "integrity": "sha512-od3WmO8qxyfNhKc+K3D17tvun3IMs/xMNmxCG9MiElAkYVbPPTRUYMkRneCpmJyQI0hNx2/EA4kZgzVfQjO86Q==", + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.15.0.tgz", + "integrity": "sha512-IofrVh213VLsDkPoSKMeM9Dshrv28jhDlBDLRcVJQvlL8pzue7PEB1EZ4UoJFYS3NSn7JOcJ/V+olRQzXlJj1w==", "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/client-insights": { - "version": "5.12.0", - "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.12.0.tgz", - "integrity": "sha512-8alajmsYUd+7vfX5lpRNdxqv3Xx9clIHLUItyQK0Z6gwGMbVEFe6YYhgDtwslMAP0y6b0WeJEIZJMLgT7VYpRw==", + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.15.0.tgz", + "integrity": "sha512-bDDEQGfFidDi0UQUCbxXOCdphbVAgbVmxvaV75cypBTQkJ+ABx/Npw7LkFGw1FsoVrttlrrQbwjvUB6mLVKs/w==", "dependencies": { - "@algolia/client-common": "5.12.0", - "@algolia/requester-browser-xhr": "5.12.0", - "@algolia/requester-fetch": "5.12.0", - "@algolia/requester-node-http": "5.12.0" + "@algolia/client-common": "5.15.0", + "@algolia/requester-browser-xhr": "5.15.0", + "@algolia/requester-fetch": "5.15.0", + "@algolia/requester-node-http": "5.15.0" }, "engines": { "node": ">= 14.0.0" @@ -207,28 +207,28 @@ } }, "node_modules/@algolia/client-query-suggestions": { - "version": "5.12.0", - "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.12.0.tgz", - "integrity": "sha512-Q5CszzGWfxbIDs9DJ/QJsL7bP6h+lJMg27KxieEnI9KGCu0Jt5iFA3GkREkgRZxRdzlHbZKkrIzhtHVbSHw/rg==", + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.15.0.tgz", + "integrity": "sha512-wu8GVluiZ5+il8WIRsGKu8VxMK9dAlr225h878GGtpTL6VBvwyJvAyLdZsfFIpY0iN++jiNb31q2C1PlPL+n/A==", "dependencies": { - "@algolia/client-common": "5.12.0", - "@algolia/requester-browser-xhr": "5.12.0", - "@algolia/requester-fetch": "5.12.0", - "@algolia/requester-node-http": "5.12.0" + "@algolia/client-common": "5.15.0", + "@algolia/requester-browser-xhr": "5.15.0", + "@algolia/requester-fetch": "5.15.0", + "@algolia/requester-node-http": "5.15.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/client-search": { - "version": "5.12.0", - "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.12.0.tgz", - "integrity": "sha512-R3qzEytgVLHOGNri+bpta6NtTt7YtkvUe/QBcAmMDjW4Jk1P0eBYIPfvnzIPbINRsLxIq9fZs9uAYBgsrts4Zg==", + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.15.0.tgz", + "integrity": "sha512-Z32gEMrRRpEta5UqVQA612sLdoqY3AovvUPClDfMxYrbdDAebmGDVPtSogUba1FZ4pP5dx20D3OV3reogLKsRA==", "dependencies": { - "@algolia/client-common": "5.12.0", - "@algolia/requester-browser-xhr": "5.12.0", - "@algolia/requester-fetch": "5.12.0", - "@algolia/requester-node-http": "5.12.0" + "@algolia/client-common": "5.15.0", + "@algolia/requester-browser-xhr": "5.15.0", + "@algolia/requester-fetch": "5.15.0", + "@algolia/requester-node-http": "5.15.0" }, "engines": { "node": ">= 14.0.0" @@ -240,14 +240,14 @@ "integrity": "sha512-FQzvOCgoFXAbf5Y6mYozw2aj5KCJoA3m4heImceldzPSMbdyS4atVjJzXKMsfX3wnZTFYwkkt8/z8UesLHlSBQ==" }, "node_modules/@algolia/ingestion": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.12.0.tgz", - "integrity": "sha512-zpHo6qhR22tL8FsdSI4DvEraPDi/019HmMrCFB/TUX98yzh5ooAU7sNW0qPL1I7+S++VbBmNzJOEU9VI8tEC8A==", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.15.0.tgz", + "integrity": "sha512-MkqkAxBQxtQ5if/EX2IPqFA7LothghVyvPoRNA/meS2AW2qkHwcxjuiBxv4H6mnAVEPfJlhu9rkdVz9LgCBgJg==", "dependencies": { - "@algolia/client-common": "5.12.0", - "@algolia/requester-browser-xhr": "5.12.0", - "@algolia/requester-fetch": "5.12.0", - "@algolia/requester-node-http": "5.12.0" + "@algolia/client-common": "5.15.0", + "@algolia/requester-browser-xhr": "5.15.0", + "@algolia/requester-fetch": "5.15.0", + "@algolia/requester-node-http": "5.15.0" }, "engines": { "node": ">= 14.0.0" @@ -267,14 +267,14 @@ } }, "node_modules/@algolia/monitoring": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.12.0.tgz", - "integrity": "sha512-i2AJZED/zf4uhxezAJUhMKoL5QoepCBp2ynOYol0N76+TSoohaMADdPnWCqOULF4RzOwrG8wWynAwBlXsAI1RQ==", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.15.0.tgz", + "integrity": "sha512-QPrFnnGLMMdRa8t/4bs7XilPYnoUXDY8PMQJ1sf9ZFwhUysYYhQNX34/enoO0LBjpoOY6rLpha39YQEFbzgKyQ==", "dependencies": { - "@algolia/client-common": "5.12.0", - "@algolia/requester-browser-xhr": "5.12.0", - "@algolia/requester-fetch": "5.12.0", - "@algolia/requester-node-http": "5.12.0" + "@algolia/client-common": "5.15.0", + "@algolia/requester-browser-xhr": "5.15.0", + "@algolia/requester-fetch": "5.15.0", + "@algolia/requester-node-http": "5.15.0" }, "engines": { "node": ">= 14.0.0" @@ -334,11 +334,11 @@ } }, "node_modules/@algolia/requester-browser-xhr": { - "version": "5.12.0", - "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.12.0.tgz", - "integrity": "sha512-KxwleraFuVoEGCoeW6Y1RAEbgBMS7SavqeyzWdtkJc6mXeCOJXn1iZitb8Tyn2FcpMNUKlSm0adrUTt7G47+Ow==", + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.15.0.tgz", + "integrity": "sha512-Po/GNib6QKruC3XE+WKP1HwVSfCDaZcXu48kD+gwmtDlqHWKc7Bq9lrS0sNZ456rfCKhXksOmMfUs4wRM/Y96w==", "dependencies": { - "@algolia/client-common": "5.12.0" + "@algolia/client-common": "5.15.0" }, "engines": { "node": ">= 14.0.0" @@ -350,22 +350,22 @@ "integrity": "sha512-k3CXJ2OVnvgE3HMwcojpvY6d9kgKMPRxs/kVohrwF5WMr2fnqojnycZkxPoEg+bXm8fi5BBfFmOqgYztRtHsQA==" }, "node_modules/@algolia/requester-fetch": { - "version": "5.12.0", - "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.12.0.tgz", - "integrity": "sha512-FuDZXUGU1pAg2HCnrt8+q1VGHKChV/LhvjvZlLOT7e56GJie6p+EuLu4/hMKPOVuQQ8XXtrTHKIU3Lw+7O5/bQ==", + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.15.0.tgz", + "integrity": "sha512-rOZ+c0P7ajmccAvpeeNrUmEKoliYFL8aOR5qGW5pFq3oj3Iept7Y5mEtEsOBYsRt6qLnaXn4zUKf+N8nvJpcIw==", "dependencies": { - "@algolia/client-common": "5.12.0" + "@algolia/client-common": "5.15.0" }, "engines": { "node": ">= 14.0.0" } }, "node_modules/@algolia/requester-node-http": { - "version": "5.12.0", - "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.12.0.tgz", - "integrity": "sha512-ncDDY7CxZhMs6LIoPl+vHFQceIBhYPY5EfuGF1V7beO0U38xfsCYEyutEFB2kRzf4D9Gqppn3iWX71sNtrKcuw==", + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.15.0.tgz", + "integrity": "sha512-b1jTpbFf9LnQHEJP5ddDJKE2sAlhYd7EVSOWgzo/27n/SfCoHfqD0VWntnWYD83PnOKvfe8auZ2+xCb0TXotrQ==", "dependencies": { - "@algolia/client-common": "5.12.0" + "@algolia/client-common": "5.15.0" }, "engines": { "node": ">= 14.0.0" @@ -2002,208 +2002,1264 @@ "node": ">=0.1.90" } }, - "node_modules/@discoveryjs/json-ext": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", - "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "node_modules/@csstools/cascade-layer-name-parser": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@csstools/cascade-layer-name-parser/-/cascade-layer-name-parser-2.0.4.tgz", + "integrity": "sha512-7DFHlPuIxviKYZrOiwVU/PiHLm3lLUR23OMuEEtfEOQTOp9hzQ2JjdY6X5H18RVuUPJqSCI+qNnD5iOLMVE0bA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], "engines": { - "node": ">=10.0.0" + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3" } }, - "node_modules/@docsearch/css": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-3.7.0.tgz", - "integrity": "sha512-1OorbTwi1eeDmr0v5t+ckSRlt1zM5GHjm92iIl3kUu7im3GHuP+csf6E0WBg8pdXQczTWP9J9+o9n+Vg6DH5cQ==" + "node_modules/@csstools/color-helpers": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.0.1.tgz", + "integrity": "sha512-MKtmkA0BX87PKaO1NFRTFH+UnkgnmySQOvNxJubsadusqPEC2aJ9MOQiMceZJJ6oitUl/i0L6u0M1IrmAOmgBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": ">=18" + } }, - "node_modules/@docsearch/react": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@docsearch/react/-/react-3.7.0.tgz", - "integrity": "sha512-8e6tdDfkYoxafEEPuX5eE1h9cTkLvhe4KgoFkO5JCddXSQONnN1FHcDZRI4r8894eMpbYq6rdJF0dVYh8ikwNQ==", - "dependencies": { - "@algolia/autocomplete-core": "1.17.6", - "@algolia/autocomplete-preset-algolia": "1.17.6", - "@docsearch/css": "3.7.0", - "algoliasearch": "^5.12.0" + "node_modules/@csstools/css-calc": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.0.tgz", + "integrity": "sha512-X69PmFOrjTZfN5ijxtI8hZ9kRADFSLrmmQ6hgDJ272Il049WGKpDY64KhrFm/7rbWve0z81QepawzjkKlqkNGw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": ">=18" }, "peerDependencies": { - "@types/react": ">= 16.8.0 < 19.0.0", - "react": ">= 16.8.0 < 19.0.0", - "react-dom": ">= 16.8.0 < 19.0.0", - "search-insights": ">= 1 < 3" + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.0.6.tgz", + "integrity": "sha512-S/IjXqTHdpI4EtzGoNCHfqraXF37x12ZZHA1Lk7zoT5pm2lMjFuqhX/89L7dqX4CcMacKK+6ZCs5TmEGb/+wKw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "dependencies": { + "@csstools/color-helpers": "^5.0.1", + "@csstools/css-calc": "^2.1.0" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.4.tgz", + "integrity": "sha512-Up7rBoV77rv29d3uKHUIVubz1BTcgyUK72IvCQAbfbMv584xHcGKCKbWh7i8hPrRJ7qU4Y8IO3IY9m+iTB7P3A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" }, - "react": { - "optional": true + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.3" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.3.tgz", + "integrity": "sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" }, - "react-dom": { - "optional": true + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/media-query-list-parser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-4.0.2.tgz", + "integrity": "sha512-EUos465uvVvMJehckATTlNqGj4UJWkTmdWuDMjqvSUkjGpmOyFZBVwb4knxCm/k2GMTXY+c/5RkdndzFYWeX5A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" }, - "search-insights": { - "optional": true + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" } + ], + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3" } }, - "node_modules/@docsearch/react/node_modules/@algolia/client-analytics": { - "version": "5.12.0", - "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.12.0.tgz", - "integrity": "sha512-EpTsSv6IW8maCfXCDIptgT7+mQJj7pImEkcNUnxR8yUKAHzTogTXv9yGm2WXOZFVuwstd2i0sImhQ1Vz8RH/hA==", + "node_modules/@csstools/postcss-cascade-layers": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-cascade-layers/-/postcss-cascade-layers-5.0.1.tgz", + "integrity": "sha512-XOfhI7GShVcKiKwmPAnWSqd2tBR0uxt+runAxttbSp/LY2U16yAVPmAf7e9q4JJ0d+xMNmpwNDLBXnmRCl3HMQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], "dependencies": { - "@algolia/client-common": "5.12.0", - "@algolia/requester-browser-xhr": "5.12.0", - "@algolia/requester-fetch": "5.12.0", - "@algolia/requester-node-http": "5.12.0" + "@csstools/selector-specificity": "^5.0.0", + "postcss-selector-parser": "^7.0.0" }, "engines": { - "node": ">= 14.0.0" + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" } }, - "node_modules/@docsearch/react/node_modules/@algolia/client-personalization": { - "version": "5.12.0", - "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.12.0.tgz", - "integrity": "sha512-bUV9HtfkTBgpoVhxFrMkmVPG03ZN1Rtn51kiaEtukucdk3ggjR9Qu1YUfRSU2lFgxr9qJc8lTxwfvhjCeJRcqw==", + "node_modules/@csstools/postcss-cascade-layers/node_modules/@csstools/selector-specificity": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-5.0.0.tgz", + "integrity": "sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss-selector-parser": "^7.0.0" + } + }, + "node_modules/@csstools/postcss-cascade-layers/node_modules/postcss-selector-parser": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.0.0.tgz", + "integrity": "sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==", "dependencies": { - "@algolia/client-common": "5.12.0", - "@algolia/requester-browser-xhr": "5.12.0", - "@algolia/requester-fetch": "5.12.0", - "@algolia/requester-node-http": "5.12.0" + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" }, "engines": { - "node": ">= 14.0.0" + "node": ">=4" } }, - "node_modules/@docsearch/react/node_modules/@algolia/recommend": { - "version": "5.12.0", - "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.12.0.tgz", - "integrity": "sha512-0jmZyKvYnB/Bj5c7WKsKedOUjnr0UtXm0LVFUdQrxXfqOqvWv9n6Vpr65UjdYG4Q49kRQxhlwtal9WJYrYymXg==", + "node_modules/@csstools/postcss-color-function": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@csstools/postcss-color-function/-/postcss-color-function-4.0.6.tgz", + "integrity": "sha512-EcvXfC60cTIumzpsxWuvVjb7rsJEHPvqn3jeMEBUaE3JSc4FRuP7mEQ+1eicxWmIrs3FtzMH9gR3sgA5TH+ebQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], "dependencies": { - "@algolia/client-common": "5.12.0", - "@algolia/requester-browser-xhr": "5.12.0", - "@algolia/requester-fetch": "5.12.0", - "@algolia/requester-node-http": "5.12.0" + "@csstools/css-color-parser": "^3.0.6", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "@csstools/postcss-progressive-custom-properties": "^4.0.0", + "@csstools/utilities": "^2.0.0" }, "engines": { - "node": ">= 14.0.0" + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" } }, - "node_modules/@docsearch/react/node_modules/algoliasearch": { - "version": "5.12.0", - "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.12.0.tgz", - "integrity": "sha512-psGBRYdGgik8I6m28iAB8xpubvjEt7UQU+w5MAJUA2324WHiGoHap5BPkkjB14rMaXeRts6pmOsrVIglGyOVwg==", - "dependencies": { - "@algolia/client-abtesting": "5.12.0", - "@algolia/client-analytics": "5.12.0", - "@algolia/client-common": "5.12.0", - "@algolia/client-insights": "5.12.0", - "@algolia/client-personalization": "5.12.0", - "@algolia/client-query-suggestions": "5.12.0", - "@algolia/client-search": "5.12.0", - "@algolia/ingestion": "1.12.0", - "@algolia/monitoring": "1.12.0", - "@algolia/recommend": "5.12.0", - "@algolia/requester-browser-xhr": "5.12.0", - "@algolia/requester-fetch": "5.12.0", - "@algolia/requester-node-http": "5.12.0" + "node_modules/@csstools/postcss-color-mix-function": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@csstools/postcss-color-mix-function/-/postcss-color-mix-function-3.0.6.tgz", + "integrity": "sha512-jVKdJn4+JkASYGhyPO+Wa5WXSx1+oUgaXb3JsjJn/BlrtFh5zjocCY7pwWi0nuP24V1fY7glQsxEYcYNy0dMFg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "dependencies": { + "@csstools/css-color-parser": "^3.0.6", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "@csstools/postcss-progressive-custom-properties": "^4.0.0", + "@csstools/utilities": "^2.0.0" }, "engines": { - "node": ">= 14.0.0" + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" } }, - "node_modules/@docusaurus/babel": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/@docusaurus/babel/-/babel-3.6.0.tgz", - "integrity": "sha512-7CsoQFiadoq7AHSUIQNkI/lGfg9AQ2ZBzsf9BqfZGXkHwWDy6twuohEaG0PgQv1npSRSAB2dioVxhRSErnqKNA==", + "node_modules/@csstools/postcss-content-alt-text": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@csstools/postcss-content-alt-text/-/postcss-content-alt-text-2.0.4.tgz", + "integrity": "sha512-YItlZUOuZJCBlRaCf8Aucc1lgN41qYGALMly0qQllrxYJhiyzlI6RxOTMUvtWk+KhS8GphMDsDhKQ7KTPfEMSw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], "dependencies": { - "@babel/core": "^7.25.9", - "@babel/generator": "^7.25.9", - "@babel/plugin-syntax-dynamic-import": "^7.8.3", - "@babel/plugin-transform-runtime": "^7.25.9", - "@babel/preset-env": "^7.25.9", - "@babel/preset-react": "^7.25.9", - "@babel/preset-typescript": "^7.25.9", - "@babel/runtime": "^7.25.9", - "@babel/runtime-corejs3": "^7.25.9", - "@babel/traverse": "^7.25.9", - "@docusaurus/logger": "3.6.0", - "@docusaurus/utils": "3.6.0", - "babel-plugin-dynamic-import-node": "^2.3.3", - "fs-extra": "^11.1.1", - "tslib": "^2.6.0" + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "@csstools/postcss-progressive-custom-properties": "^4.0.0", + "@csstools/utilities": "^2.0.0" }, "engines": { - "node": ">=18.0" + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" } }, - "node_modules/@docusaurus/bundler": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/@docusaurus/bundler/-/bundler-3.6.0.tgz", - "integrity": "sha512-o5T9HXkPKH0OQAifTxEXaebcO8kaz3tU1+wlIShZ2DKJHlsyWX3N4rToWBHroWnV/ZCT2XN3kLRzXASqrnb9Tw==", + "node_modules/@csstools/postcss-exponential-functions": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@csstools/postcss-exponential-functions/-/postcss-exponential-functions-2.0.5.tgz", + "integrity": "sha512-mi8R6dVfA2nDoKM3wcEi64I8vOYEgQVtVKCfmLHXupeLpACfGAided5ddMt5f+CnEodNu4DifuVwb0I6fQDGGQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], "dependencies": { - "@babel/core": "^7.25.9", - "@docusaurus/babel": "3.6.0", - "@docusaurus/cssnano-preset": "3.6.0", - "@docusaurus/logger": "3.6.0", - "@docusaurus/types": "3.6.0", - "@docusaurus/utils": "3.6.0", - "autoprefixer": "^10.4.14", - "babel-loader": "^9.2.1", - "clean-css": "^5.3.2", - "copy-webpack-plugin": "^11.0.0", - "css-loader": "^6.8.1", - "css-minimizer-webpack-plugin": "^5.0.1", - "cssnano": "^6.1.2", - "file-loader": "^6.2.0", - "html-minifier-terser": "^7.2.0", - "mini-css-extract-plugin": "^2.9.1", - "null-loader": "^4.0.1", - "postcss": "^8.4.26", - "postcss-loader": "^7.3.3", - "react-dev-utils": "^12.0.1", - "terser-webpack-plugin": "^5.3.9", - "tslib": "^2.6.0", - "url-loader": "^4.1.1", - "webpack": "^5.95.0", - "webpackbar": "^6.0.1" + "@csstools/css-calc": "^2.1.0", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3" }, "engines": { - "node": ">=18.0" + "node": ">=18" }, "peerDependencies": { - "@docusaurus/faster": "3.5.2" - }, - "peerDependenciesMeta": { - "@docusaurus/faster": { - "optional": true - } + "postcss": "^8.4" } }, - "node_modules/@docusaurus/core": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/@docusaurus/core/-/core-3.6.0.tgz", - "integrity": "sha512-lvRgMoKJJSRDt9+HhAqFcICV4kp/mw1cJJrLxIw4Q2XZnFGM1XUuwcbuaqWmGog+NcOLZaPCcCtZbn60EMCtjQ==", - "dependencies": { - "@docusaurus/babel": "3.6.0", - "@docusaurus/bundler": "3.6.0", - "@docusaurus/logger": "3.6.0", - "@docusaurus/mdx-loader": "3.6.0", - "@docusaurus/utils": "3.6.0", - "@docusaurus/utils-common": "3.6.0", - "@docusaurus/utils-validation": "3.6.0", - "boxen": "^6.2.1", - "chalk": "^4.1.2", - "chokidar": "^3.5.3", - "cli-table3": "^0.6.3", - "combine-promises": "^1.1.0", - "commander": "^5.1.0", - "core-js": "^3.31.1", - "del": "^6.1.1", - "detect-port": "^1.5.1", - "escape-html": "^1.0.3", - "eta": "^2.2.0", - "eval": "^0.1.8", - "fs-extra": "^11.1.1", + "node_modules/@csstools/postcss-font-format-keywords": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-font-format-keywords/-/postcss-font-format-keywords-4.0.0.tgz", + "integrity": "sha512-usBzw9aCRDvchpok6C+4TXC57btc4bJtmKQWOHQxOVKen1ZfVqBUuCZ/wuqdX5GHsD0NRSr9XTP+5ID1ZZQBXw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "dependencies": { + "@csstools/utilities": "^2.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-gamut-mapping": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@csstools/postcss-gamut-mapping/-/postcss-gamut-mapping-2.0.6.tgz", + "integrity": "sha512-0ke7fmXfc8H+kysZz246yjirAH6JFhyX9GTlyRnM0exHO80XcA9zeJpy5pOp5zo/AZiC/q5Pf+Hw7Pd6/uAoYA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "dependencies": { + "@csstools/css-color-parser": "^3.0.6", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-gradients-interpolation-method": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@csstools/postcss-gradients-interpolation-method/-/postcss-gradients-interpolation-method-5.0.6.tgz", + "integrity": "sha512-Itrbx6SLUzsZ6Mz3VuOlxhbfuyLTogG5DwEF1V8dAi24iMuvQPIHd7Ti+pNDp7j6WixndJGZaoNR0f9VSzwuTg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "dependencies": { + "@csstools/css-color-parser": "^3.0.6", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "@csstools/postcss-progressive-custom-properties": "^4.0.0", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-hwb-function": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@csstools/postcss-hwb-function/-/postcss-hwb-function-4.0.6.tgz", + "integrity": "sha512-927Pqy3a1uBP7U8sTfaNdZVB0mNXzIrJO/GZ8us9219q9n06gOqCdfZ0E6d1P66Fm0fYHvxfDbfcUuwAn5UwhQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "dependencies": { + "@csstools/css-color-parser": "^3.0.6", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "@csstools/postcss-progressive-custom-properties": "^4.0.0", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-ic-unit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-ic-unit/-/postcss-ic-unit-4.0.0.tgz", + "integrity": "sha512-9QT5TDGgx7wD3EEMN3BSUG6ckb6Eh5gSPT5kZoVtUuAonfPmLDJyPhqR4ntPpMYhUKAMVKAg3I/AgzqHMSeLhA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "dependencies": { + "@csstools/postcss-progressive-custom-properties": "^4.0.0", + "@csstools/utilities": "^2.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-initial": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-initial/-/postcss-initial-2.0.0.tgz", + "integrity": "sha512-dv2lNUKR+JV+OOhZm9paWzYBXOCi+rJPqJ2cJuhh9xd8USVrd0cBEPczla81HNOyThMQWeCcdln3gZkQV2kYxA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-is-pseudo-class": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-is-pseudo-class/-/postcss-is-pseudo-class-5.0.1.tgz", + "integrity": "sha512-JLp3POui4S1auhDR0n8wHd/zTOWmMsmK3nQd3hhL6FhWPaox5W7j1se6zXOG/aP07wV2ww0lxbKYGwbBszOtfQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "dependencies": { + "@csstools/selector-specificity": "^5.0.0", + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-is-pseudo-class/node_modules/@csstools/selector-specificity": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-5.0.0.tgz", + "integrity": "sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss-selector-parser": "^7.0.0" + } + }, + "node_modules/@csstools/postcss-is-pseudo-class/node_modules/postcss-selector-parser": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.0.0.tgz", + "integrity": "sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@csstools/postcss-light-dark-function": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@csstools/postcss-light-dark-function/-/postcss-light-dark-function-2.0.7.tgz", + "integrity": "sha512-ZZ0rwlanYKOHekyIPaU+sVm3BEHCe+Ha0/px+bmHe62n0Uc1lL34vbwrLYn6ote8PHlsqzKeTQdIejQCJ05tfw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "dependencies": { + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "@csstools/postcss-progressive-custom-properties": "^4.0.0", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-logical-float-and-clear": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-float-and-clear/-/postcss-logical-float-and-clear-3.0.0.tgz", + "integrity": "sha512-SEmaHMszwakI2rqKRJgE+8rpotFfne1ZS6bZqBoQIicFyV+xT1UF42eORPxJkVJVrH9C0ctUgwMSn3BLOIZldQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-logical-overflow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-overflow/-/postcss-logical-overflow-2.0.0.tgz", + "integrity": "sha512-spzR1MInxPuXKEX2csMamshR4LRaSZ3UXVaRGjeQxl70ySxOhMpP2252RAFsg8QyyBXBzuVOOdx1+bVO5bPIzA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-logical-overscroll-behavior": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-overscroll-behavior/-/postcss-logical-overscroll-behavior-2.0.0.tgz", + "integrity": "sha512-e/webMjoGOSYfqLunyzByZj5KKe5oyVg/YSbie99VEaSDE2kimFm0q1f6t/6Jo+VVCQ/jbe2Xy+uX+C4xzWs4w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-logical-resize": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-resize/-/postcss-logical-resize-3.0.0.tgz", + "integrity": "sha512-DFbHQOFW/+I+MY4Ycd/QN6Dg4Hcbb50elIJCfnwkRTCX05G11SwViI5BbBlg9iHRl4ytB7pmY5ieAFk3ws7yyg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-logical-viewport-units": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-viewport-units/-/postcss-logical-viewport-units-3.0.3.tgz", + "integrity": "sha512-OC1IlG/yoGJdi0Y+7duz/kU/beCwO+Gua01sD6GtOtLi7ByQUpcIqs7UE/xuRPay4cHgOMatWdnDdsIDjnWpPw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "dependencies": { + "@csstools/css-tokenizer": "^3.0.3", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-media-minmax": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@csstools/postcss-media-minmax/-/postcss-media-minmax-2.0.5.tgz", + "integrity": "sha512-sdh5i5GToZOIAiwhdntRWv77QDtsxP2r2gXW/WbLSCoLr00KTq/yiF1qlQ5XX2+lmiFa8rATKMcbwl3oXDMNew==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "dependencies": { + "@csstools/css-calc": "^2.1.0", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "@csstools/media-query-list-parser": "^4.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-media-queries-aspect-ratio-number-values": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/postcss-media-queries-aspect-ratio-number-values/-/postcss-media-queries-aspect-ratio-number-values-3.0.4.tgz", + "integrity": "sha512-AnGjVslHMm5xw9keusQYvjVWvuS7KWK+OJagaG0+m9QnIjZsrysD2kJP/tr/UJIyYtMCtu8OkUd+Rajb4DqtIQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "dependencies": { + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "@csstools/media-query-list-parser": "^4.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-nested-calc": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-nested-calc/-/postcss-nested-calc-4.0.0.tgz", + "integrity": "sha512-jMYDdqrQQxE7k9+KjstC3NbsmC063n1FTPLCgCRS2/qHUbHM0mNy9pIn4QIiQGs9I/Bg98vMqw7mJXBxa0N88A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "dependencies": { + "@csstools/utilities": "^2.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-normalize-display-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-normalize-display-values/-/postcss-normalize-display-values-4.0.0.tgz", + "integrity": "sha512-HlEoG0IDRoHXzXnkV4in47dzsxdsjdz6+j7MLjaACABX2NfvjFS6XVAnpaDyGesz9gK2SC7MbNwdCHusObKJ9Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-oklab-function": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@csstools/postcss-oklab-function/-/postcss-oklab-function-4.0.6.tgz", + "integrity": "sha512-Hptoa0uX+XsNacFBCIQKTUBrFKDiplHan42X73EklG6XmQLG7/aIvxoNhvZ7PvOWMt67Pw3bIlUY2nD6p5vL8A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "dependencies": { + "@csstools/css-color-parser": "^3.0.6", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "@csstools/postcss-progressive-custom-properties": "^4.0.0", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-progressive-custom-properties": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-progressive-custom-properties/-/postcss-progressive-custom-properties-4.0.0.tgz", + "integrity": "sha512-XQPtROaQjomnvLUSy/bALTR5VCtTVUFwYs1SblvYgLSeTo2a/bMNwUwo2piXw5rTv/FEYiy5yPSXBqg9OKUx7Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-random-function": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-random-function/-/postcss-random-function-1.0.1.tgz", + "integrity": "sha512-Ab/tF8/RXktQlFwVhiC70UNfpFQRhtE5fQQoP2pO+KCPGLsLdWFiOuHgSRtBOqEshCVAzR4H6o38nhvRZq8deA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "dependencies": { + "@csstools/css-calc": "^2.1.0", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-relative-color-syntax": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@csstools/postcss-relative-color-syntax/-/postcss-relative-color-syntax-3.0.6.tgz", + "integrity": "sha512-yxP618Xb+ji1I624jILaYM62uEmZcmbdmFoZHoaThw896sq0vU39kqTTF+ZNic9XyPtPMvq0vyvbgmHaszq8xg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "dependencies": { + "@csstools/css-color-parser": "^3.0.6", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "@csstools/postcss-progressive-custom-properties": "^4.0.0", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-scope-pseudo-class": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-scope-pseudo-class/-/postcss-scope-pseudo-class-4.0.1.tgz", + "integrity": "sha512-IMi9FwtH6LMNuLea1bjVMQAsUhFxJnyLSgOp/cpv5hrzWmrUYU5fm0EguNDIIOHUqzXode8F/1qkC/tEo/qN8Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-scope-pseudo-class/node_modules/postcss-selector-parser": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.0.0.tgz", + "integrity": "sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@csstools/postcss-sign-functions": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-sign-functions/-/postcss-sign-functions-1.1.0.tgz", + "integrity": "sha512-SLcc20Nujx/kqbSwDmj6oaXgpy3UjFhBy1sfcqPgDkHfOIfUtUVH7OXO+j7BU4v/At5s61N5ZX6shvgPwluhsA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "dependencies": { + "@csstools/css-calc": "^2.1.0", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-stepped-value-functions": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@csstools/postcss-stepped-value-functions/-/postcss-stepped-value-functions-4.0.5.tgz", + "integrity": "sha512-G6SJ6hZJkhxo6UZojVlLo14MohH4J5J7z8CRBrxxUYy9JuZiIqUo5TBYyDGcE0PLdzpg63a7mHSJz3VD+gMwqw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "dependencies": { + "@csstools/css-calc": "^2.1.0", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-text-decoration-shorthand": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-text-decoration-shorthand/-/postcss-text-decoration-shorthand-4.0.1.tgz", + "integrity": "sha512-xPZIikbx6jyzWvhms27uugIc0I4ykH4keRvoa3rxX5K7lEhkbd54rjj/dv60qOCTisoS+3bmwJTeyV1VNBrXaw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "dependencies": { + "@csstools/color-helpers": "^5.0.1", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-trigonometric-functions": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@csstools/postcss-trigonometric-functions/-/postcss-trigonometric-functions-4.0.5.tgz", + "integrity": "sha512-/YQThYkt5MLvAmVu7zxjhceCYlKrYddK6LEmK5I4ojlS6BmO9u2yO4+xjXzu2+NPYmHSTtP4NFSamBCMmJ1NJA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "dependencies": { + "@csstools/css-calc": "^2.1.0", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-unset-value": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-unset-value/-/postcss-unset-value-4.0.0.tgz", + "integrity": "sha512-cBz3tOCI5Fw6NIFEwU3RiwK6mn3nKegjpJuzCndoGq3BZPkUjnsq7uQmIeMNeMbMk7YD2MfKcgCpZwX5jyXqCA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/utilities": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@csstools/utilities/-/utilities-2.0.0.tgz", + "integrity": "sha512-5VdOr0Z71u+Yp3ozOx8T11N703wIFGVRgOWbOZMKgglPJsWA54MRIoMNVMa7shUToIhx5J8vX4sOZgD2XiihiQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@docsearch/css": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-3.8.0.tgz", + "integrity": "sha512-pieeipSOW4sQ0+bE5UFC51AOZp9NGxg89wAlZ1BAQFaiRAGK1IKUaPQ0UGZeNctJXyqZ1UvBtOQh2HH+U5GtmA==" + }, + "node_modules/@docsearch/react": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@docsearch/react/-/react-3.8.0.tgz", + "integrity": "sha512-WnFK720+iwTVt94CxY3u+FgX6exb3BfN5kE9xUY6uuAH/9W/UFboBZFLlrw/zxFRHoHZCOXRtOylsXF+6LHI+Q==", + "dependencies": { + "@algolia/autocomplete-core": "1.17.7", + "@algolia/autocomplete-preset-algolia": "1.17.7", + "@docsearch/css": "3.8.0", + "algoliasearch": "^5.12.0" + }, + "peerDependencies": { + "@types/react": ">= 16.8.0 < 19.0.0", + "react": ">= 16.8.0 < 19.0.0", + "react-dom": ">= 16.8.0 < 19.0.0", + "search-insights": ">= 1 < 3" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "search-insights": { + "optional": true + } + } + }, + "node_modules/@docsearch/react/node_modules/@algolia/client-analytics": { + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.15.0.tgz", + "integrity": "sha512-lho0gTFsQDIdCwyUKTtMuf9nCLwq9jOGlLGIeQGKDxXF7HbiAysFIu5QW/iQr1LzMgDyM9NH7K98KY+BiIFriQ==", + "dependencies": { + "@algolia/client-common": "5.15.0", + "@algolia/requester-browser-xhr": "5.15.0", + "@algolia/requester-fetch": "5.15.0", + "@algolia/requester-node-http": "5.15.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@docsearch/react/node_modules/@algolia/client-personalization": { + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.15.0.tgz", + "integrity": "sha512-LfaZqLUWxdYFq44QrasCDED5bSYOswpQjSiIL7Q5fYlefAAUO95PzBPKCfUhSwhb4rKxigHfDkd81AvEicIEoA==", + "dependencies": { + "@algolia/client-common": "5.15.0", + "@algolia/requester-browser-xhr": "5.15.0", + "@algolia/requester-fetch": "5.15.0", + "@algolia/requester-node-http": "5.15.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@docsearch/react/node_modules/@algolia/recommend": { + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.15.0.tgz", + "integrity": "sha512-5eupMwSqMLDObgSMF0XG958zR6GJP3f7jHDQ3/WlzCM9/YIJiWIUoJFGsko9GYsA5xbLDHE/PhWtq4chcCdaGQ==", + "dependencies": { + "@algolia/client-common": "5.15.0", + "@algolia/requester-browser-xhr": "5.15.0", + "@algolia/requester-fetch": "5.15.0", + "@algolia/requester-node-http": "5.15.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@docsearch/react/node_modules/algoliasearch": { + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.15.0.tgz", + "integrity": "sha512-Yf3Swz1s63hjvBVZ/9f2P1Uu48GjmjCN+Esxb6MAONMGtZB1fRX8/S1AhUTtsuTlcGovbYLxpHgc7wEzstDZBw==", + "dependencies": { + "@algolia/client-abtesting": "5.15.0", + "@algolia/client-analytics": "5.15.0", + "@algolia/client-common": "5.15.0", + "@algolia/client-insights": "5.15.0", + "@algolia/client-personalization": "5.15.0", + "@algolia/client-query-suggestions": "5.15.0", + "@algolia/client-search": "5.15.0", + "@algolia/ingestion": "1.15.0", + "@algolia/monitoring": "1.15.0", + "@algolia/recommend": "5.15.0", + "@algolia/requester-browser-xhr": "5.15.0", + "@algolia/requester-fetch": "5.15.0", + "@algolia/requester-node-http": "5.15.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@docusaurus/babel": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/@docusaurus/babel/-/babel-3.6.2.tgz", + "integrity": "sha512-v8N8TWGXDsb5sxQC3Rcqb1CZr0LlU1OgqqVBUchN6cpIUr7EJuVJs5eHcIu5Ag8mwO/hWN3f7FE9uaHTMapAbg==", + "dependencies": { + "@babel/core": "^7.25.9", + "@babel/generator": "^7.25.9", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-transform-runtime": "^7.25.9", + "@babel/preset-env": "^7.25.9", + "@babel/preset-react": "^7.25.9", + "@babel/preset-typescript": "^7.25.9", + "@babel/runtime": "^7.25.9", + "@babel/runtime-corejs3": "^7.25.9", + "@babel/traverse": "^7.25.9", + "@docusaurus/logger": "3.6.2", + "@docusaurus/utils": "3.6.2", + "babel-plugin-dynamic-import-node": "^2.3.3", + "fs-extra": "^11.1.1", + "tslib": "^2.6.0" + }, + "engines": { + "node": ">=18.0" + } + }, + "node_modules/@docusaurus/bundler": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/@docusaurus/bundler/-/bundler-3.6.2.tgz", + "integrity": "sha512-YkEifEVs4lV931SrHBB4n6WqRowMw+aM/QPH3z8aU+5t1dWa+1p2OPqARS+tSbh3la9ns+L1zIfSbd8RHi2/PQ==", + "dependencies": { + "@babel/core": "^7.25.9", + "@docusaurus/babel": "3.6.2", + "@docusaurus/cssnano-preset": "3.6.2", + "@docusaurus/logger": "3.6.2", + "@docusaurus/types": "3.6.2", + "@docusaurus/utils": "3.6.2", + "babel-loader": "^9.2.1", + "clean-css": "^5.3.2", + "copy-webpack-plugin": "^11.0.0", + "css-loader": "^6.8.1", + "css-minimizer-webpack-plugin": "^5.0.1", + "cssnano": "^6.1.2", + "file-loader": "^6.2.0", + "html-minifier-terser": "^7.2.0", + "mini-css-extract-plugin": "^2.9.1", + "null-loader": "^4.0.1", + "postcss": "^8.4.26", + "postcss-loader": "^7.3.3", + "postcss-preset-env": "^10.1.0", + "react-dev-utils": "^12.0.1", + "terser-webpack-plugin": "^5.3.9", + "tslib": "^2.6.0", + "url-loader": "^4.1.1", + "webpack": "^5.95.0", + "webpackbar": "^6.0.1" + }, + "engines": { + "node": ">=18.0" + }, + "peerDependencies": { + "@docusaurus/faster": "*" + }, + "peerDependenciesMeta": { + "@docusaurus/faster": { + "optional": true + } + } + }, + "node_modules/@docusaurus/core": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/@docusaurus/core/-/core-3.6.2.tgz", + "integrity": "sha512-irMts/mGLZv8dWcy0WUtbY/U6b5qIfHgQd1/kXMyAxUJo99fL0wFSqhMI+tcxjk0HYy427MXerLMqFJj+Arg1w==", + "dependencies": { + "@docusaurus/babel": "3.6.2", + "@docusaurus/bundler": "3.6.2", + "@docusaurus/logger": "3.6.2", + "@docusaurus/mdx-loader": "3.6.2", + "@docusaurus/utils": "3.6.2", + "@docusaurus/utils-common": "3.6.2", + "@docusaurus/utils-validation": "3.6.2", + "boxen": "^6.2.1", + "chalk": "^4.1.2", + "chokidar": "^3.5.3", + "cli-table3": "^0.6.3", + "combine-promises": "^1.1.0", + "commander": "^5.1.0", + "core-js": "^3.31.1", + "del": "^6.1.1", + "detect-port": "^1.5.1", + "escape-html": "^1.0.3", + "eta": "^2.2.0", + "eval": "^0.1.8", + "fs-extra": "^11.1.1", "html-tags": "^3.3.1", "html-webpack-plugin": "^5.6.0", "leven": "^3.1.0", @@ -2254,9 +3310,9 @@ } }, "node_modules/@docusaurus/cssnano-preset": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/@docusaurus/cssnano-preset/-/cssnano-preset-3.6.0.tgz", - "integrity": "sha512-h3jlOXqqzNSoU+C4CZLNpFtD+v2xr1UBf4idZpwMgqid9r6lb5GS7tWKnQnauio6OipacbHbDXEX3JyT1PlDkg==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/@docusaurus/cssnano-preset/-/cssnano-preset-3.6.2.tgz", + "integrity": "sha512-mBkVa4QMHRwCFCVLYdBlOZuAT1iVVsS7GGSgliSVAeTOagP/AbtlBsCVrBs+keEuDuRF1w/6QEcqDoZe9fa5pw==", "dependencies": { "cssnano-preset-advanced": "^6.1.2", "postcss": "^8.4.38", @@ -2268,9 +3324,9 @@ } }, "node_modules/@docusaurus/logger": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/@docusaurus/logger/-/logger-3.6.0.tgz", - "integrity": "sha512-BcQhoXilXW0607cH/kO6P5Gt5KxCGfoJ+QDKNf3yO2S09/RsITlW+0QljXPbI3DklTrHrhRDmgGk1yX4nUhWTA==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/@docusaurus/logger/-/logger-3.6.2.tgz", + "integrity": "sha512-1p4IQhhgLyIfsey4UAdAIW69aUE1Ei6O91Nsw30ryZeDWSG5dh4o3zaRGOLxfAX69Ac/yDm6YCwJOafUxL6Vxg==", "dependencies": { "chalk": "^4.1.2", "tslib": "^2.6.0" @@ -2280,13 +3336,13 @@ } }, "node_modules/@docusaurus/mdx-loader": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/@docusaurus/mdx-loader/-/mdx-loader-3.6.0.tgz", - "integrity": "sha512-GhRzL1Af/AdSSrGesSPOU/iP/aXadTGmVKuysCxZDrQR2RtBtubQZ9aw+KvdFVV7R4K/CsbgD6J5oqrXlEPk3Q==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/@docusaurus/mdx-loader/-/mdx-loader-3.6.2.tgz", + "integrity": "sha512-7fbRmNgF3CR96Ja82Ya0/Cdu1OL9UJ/22llNMY8lr5gAbw718Y5ryXMVRIYn0JNLTiSxzgtvW4DIsUWEB8NMpw==", "dependencies": { - "@docusaurus/logger": "3.6.0", - "@docusaurus/utils": "3.6.0", - "@docusaurus/utils-validation": "3.6.0", + "@docusaurus/logger": "3.6.2", + "@docusaurus/utils": "3.6.2", + "@docusaurus/utils-validation": "3.6.2", "@mdx-js/mdx": "^3.0.0", "@slorber/remark-comment": "^1.0.0", "escape-html": "^1.0.3", @@ -2318,11 +3374,11 @@ } }, "node_modules/@docusaurus/module-type-aliases": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/@docusaurus/module-type-aliases/-/module-type-aliases-3.6.0.tgz", - "integrity": "sha512-szTrIN/6/fuk0xkf3XbRfdTFJzRQ8d1s3sQj5++58wltrT7v3yn1149oc9ryYjMpRcbsarGloQwMu7ofPe4XPg==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/@docusaurus/module-type-aliases/-/module-type-aliases-3.6.2.tgz", + "integrity": "sha512-NrJkL2rLTCjHtWOqUvWzwqvJrsKLj0gVJeV6q5yeKdKKgItietcTf2fTRkM9LHKSUN8CBDXxwHABeQvTahvmXQ==", "dependencies": { - "@docusaurus/types": "3.6.0", + "@docusaurus/types": "3.6.2", "@types/history": "^4.7.11", "@types/react": "*", "@types/react-router-config": "*", @@ -2336,18 +3392,18 @@ } }, "node_modules/@docusaurus/plugin-content-blog": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-blog/-/plugin-content-blog-3.6.0.tgz", - "integrity": "sha512-o4aT1/E0Ldpzs/hQff5uyoSriAhS/yqBhqSn+fvSw465AaqRsva6O7CZSYleuBq6x2bewyE3QJq2PcTiHhAd8g==", - "dependencies": { - "@docusaurus/core": "3.6.0", - "@docusaurus/logger": "3.6.0", - "@docusaurus/mdx-loader": "3.6.0", - "@docusaurus/theme-common": "3.6.0", - "@docusaurus/types": "3.6.0", - "@docusaurus/utils": "3.6.0", - "@docusaurus/utils-common": "3.6.0", - "@docusaurus/utils-validation": "3.6.0", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-blog/-/plugin-content-blog-3.6.2.tgz", + "integrity": "sha512-6bJxr6Or4NslEVH3BJuPH30kUWiqUjDRdGPhvxpHmt9W/RY2/6u72WICG3bW3dLFxJ/2uDLBU92lHnatpvo7Ew==", + "dependencies": { + "@docusaurus/core": "3.6.2", + "@docusaurus/logger": "3.6.2", + "@docusaurus/mdx-loader": "3.6.2", + "@docusaurus/theme-common": "3.6.2", + "@docusaurus/types": "3.6.2", + "@docusaurus/utils": "3.6.2", + "@docusaurus/utils-common": "3.6.2", + "@docusaurus/utils-validation": "3.6.2", "cheerio": "1.0.0-rc.12", "feed": "^4.2.2", "fs-extra": "^11.1.1", @@ -2369,19 +3425,19 @@ } }, "node_modules/@docusaurus/plugin-content-docs": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.6.0.tgz", - "integrity": "sha512-c5gZOxocJKO/Zev2MEZInli+b+VNswDGuKHE6QtFgidhAJonwjh2kwj967RvWFaMMk62HlLJLZ+IGK2XsVy4Aw==", - "dependencies": { - "@docusaurus/core": "3.6.0", - "@docusaurus/logger": "3.6.0", - "@docusaurus/mdx-loader": "3.6.0", - "@docusaurus/module-type-aliases": "3.6.0", - "@docusaurus/theme-common": "3.6.0", - "@docusaurus/types": "3.6.0", - "@docusaurus/utils": "3.6.0", - "@docusaurus/utils-common": "3.6.0", - "@docusaurus/utils-validation": "3.6.0", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.6.2.tgz", + "integrity": "sha512-e6WW1g10RIXXLN/rrtqTi/FyJ1Hj3X9Mmgz4V11/0pDCxIGGI8m4ocbAglUlLtgvbLD5viNLefl/NwbOW3JXiQ==", + "dependencies": { + "@docusaurus/core": "3.6.2", + "@docusaurus/logger": "3.6.2", + "@docusaurus/mdx-loader": "3.6.2", + "@docusaurus/module-type-aliases": "3.6.2", + "@docusaurus/theme-common": "3.6.2", + "@docusaurus/types": "3.6.2", + "@docusaurus/utils": "3.6.2", + "@docusaurus/utils-common": "3.6.2", + "@docusaurus/utils-validation": "3.6.2", "@types/react-router-config": "^5.0.7", "combine-promises": "^1.1.0", "fs-extra": "^11.1.1", @@ -2400,15 +3456,15 @@ } }, "node_modules/@docusaurus/plugin-content-pages": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-pages/-/plugin-content-pages-3.6.0.tgz", - "integrity": "sha512-RKHhJrfkadHc7+tt1cP48NWifOrhkSRMPdXNYytzhoQrXlP6Ph+3tfQ4/n+nT0S3Y9+wwRxYqRqA380ZLt+QtQ==", - "dependencies": { - "@docusaurus/core": "3.6.0", - "@docusaurus/mdx-loader": "3.6.0", - "@docusaurus/types": "3.6.0", - "@docusaurus/utils": "3.6.0", - "@docusaurus/utils-validation": "3.6.0", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-pages/-/plugin-content-pages-3.6.2.tgz", + "integrity": "sha512-fo4NyGkw10lYHyHaTxE6TZLYnxNtCfRHeZkNK1N9pBYqe7TT2dBUNAEeVW2U3ed9m6YuB7JKSQsa++GGmcP+6g==", + "dependencies": { + "@docusaurus/core": "3.6.2", + "@docusaurus/mdx-loader": "3.6.2", + "@docusaurus/types": "3.6.2", + "@docusaurus/utils": "3.6.2", + "@docusaurus/utils-validation": "3.6.2", "fs-extra": "^11.1.1", "tslib": "^2.6.0", "webpack": "^5.88.1" @@ -2422,13 +3478,13 @@ } }, "node_modules/@docusaurus/plugin-debug": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-debug/-/plugin-debug-3.6.0.tgz", - "integrity": "sha512-o8T1Rl94COLdSlKvjYLQpRJQRU8WWZ8EX1B0yV0dQLNN8reyH7MQW+6z1ig4sQFfH3pnjPWVGHfuEjcib5m7Eg==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-debug/-/plugin-debug-3.6.2.tgz", + "integrity": "sha512-T/eS3VvHElpeV5S8uwp7Si4ujEynmgFtJLvA2CSa5pzQuOF1EEghF9nekAIj0cWtDHsqNUDZNr8hK1brivFXSg==", "dependencies": { - "@docusaurus/core": "3.6.0", - "@docusaurus/types": "3.6.0", - "@docusaurus/utils": "3.6.0", + "@docusaurus/core": "3.6.2", + "@docusaurus/types": "3.6.2", + "@docusaurus/utils": "3.6.2", "fs-extra": "^11.1.1", "react-json-view-lite": "^1.2.0", "tslib": "^2.6.0" @@ -2442,13 +3498,13 @@ } }, "node_modules/@docusaurus/plugin-google-analytics": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-3.6.0.tgz", - "integrity": "sha512-kgRFbfpi6Hshj75YUztKyEMtI/kw0trPRwoTN4g+W1NK99R/vh8phTvhBTIMnDbetU79795LkwfG0rZ/ce6zWQ==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-3.6.2.tgz", + "integrity": "sha512-B7ihrr3wz8e4XqW+dIAtq844u3Z83u5CeiL1xrCqzFH+vDCjUZHTamS3zKXNcgi6YVVe6hUQXPG15ltaqQaVPQ==", "dependencies": { - "@docusaurus/core": "3.6.0", - "@docusaurus/types": "3.6.0", - "@docusaurus/utils-validation": "3.6.0", + "@docusaurus/core": "3.6.2", + "@docusaurus/types": "3.6.2", + "@docusaurus/utils-validation": "3.6.2", "tslib": "^2.6.0" }, "engines": { @@ -2460,13 +3516,13 @@ } }, "node_modules/@docusaurus/plugin-google-gtag": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-3.6.0.tgz", - "integrity": "sha512-nqu4IfjaO4UX+dojHL2BxHRS+sKj31CIMWYo49huQ3wTET0Oc3u/WGTaKd3ShTPDhkgiRhTOSTPUwJWrU55nHg==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-3.6.2.tgz", + "integrity": "sha512-V8ijI6qddAAkJ0vd8sjZ7S/apRTLJn9dAwvj/rSMd93witGdKINemL+9TyfLkhcXKTxyqRT8zKdu8ewjPXqKHg==", "dependencies": { - "@docusaurus/core": "3.6.0", - "@docusaurus/types": "3.6.0", - "@docusaurus/utils-validation": "3.6.0", + "@docusaurus/core": "3.6.2", + "@docusaurus/types": "3.6.2", + "@docusaurus/utils-validation": "3.6.2", "@types/gtag.js": "^0.0.12", "tslib": "^2.6.0" }, @@ -2479,13 +3535,13 @@ } }, "node_modules/@docusaurus/plugin-google-tag-manager": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-tag-manager/-/plugin-google-tag-manager-3.6.0.tgz", - "integrity": "sha512-OU6c5xI0nOVbEc9eImGvvsgNWe4vGm97t/W3aLHjWsHyNk3uwFNBQMHRvBUwAi9k/K3kyC5E7DWnc67REhdLOw==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-tag-manager/-/plugin-google-tag-manager-3.6.2.tgz", + "integrity": "sha512-fnWQ5FdN9f8c8VTgjaQ98208Y+d/JjHhD506rWIIL9rt1cJOf29XElxvOeKpMJadfkgY5KLZSAiHkGt+4qgN4g==", "dependencies": { - "@docusaurus/core": "3.6.0", - "@docusaurus/types": "3.6.0", - "@docusaurus/utils-validation": "3.6.0", + "@docusaurus/core": "3.6.2", + "@docusaurus/types": "3.6.2", + "@docusaurus/utils-validation": "3.6.2", "tslib": "^2.6.0" }, "engines": { @@ -2497,16 +3553,16 @@ } }, "node_modules/@docusaurus/plugin-sitemap": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-sitemap/-/plugin-sitemap-3.6.0.tgz", - "integrity": "sha512-YB5XMdf9FjLhgbHY/cDbYhVxsgcpPIjxY9769HUgFOB7GVzItTLOR71W035R1BiR2CA5QAn3XOSg36WLRxlhQQ==", - "dependencies": { - "@docusaurus/core": "3.6.0", - "@docusaurus/logger": "3.6.0", - "@docusaurus/types": "3.6.0", - "@docusaurus/utils": "3.6.0", - "@docusaurus/utils-common": "3.6.0", - "@docusaurus/utils-validation": "3.6.0", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-sitemap/-/plugin-sitemap-3.6.2.tgz", + "integrity": "sha512-qcAQAP1Ot0dZpeRoJ0L/Zck5FVDkll2IleVZQLzxeRVDZIw1P9/TK7/Aw1w2pmH7dmw/Cwk/cLSVRvLAmp9k7A==", + "dependencies": { + "@docusaurus/core": "3.6.2", + "@docusaurus/logger": "3.6.2", + "@docusaurus/types": "3.6.2", + "@docusaurus/utils": "3.6.2", + "@docusaurus/utils-common": "3.6.2", + "@docusaurus/utils-validation": "3.6.2", "fs-extra": "^11.1.1", "sitemap": "^7.1.1", "tslib": "^2.6.0" @@ -2520,23 +3576,23 @@ } }, "node_modules/@docusaurus/preset-classic": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/@docusaurus/preset-classic/-/preset-classic-3.6.0.tgz", - "integrity": "sha512-kpGNdQzr/Dpm7o3b1iaQrz4DMDx3WIeBbl4V4P4maa2zAQkTdlaP4CMgA5oKrRrpqPLnQFsUM/b+qf2glhl2Tw==", - "dependencies": { - "@docusaurus/core": "3.6.0", - "@docusaurus/plugin-content-blog": "3.6.0", - "@docusaurus/plugin-content-docs": "3.6.0", - "@docusaurus/plugin-content-pages": "3.6.0", - "@docusaurus/plugin-debug": "3.6.0", - "@docusaurus/plugin-google-analytics": "3.6.0", - "@docusaurus/plugin-google-gtag": "3.6.0", - "@docusaurus/plugin-google-tag-manager": "3.6.0", - "@docusaurus/plugin-sitemap": "3.6.0", - "@docusaurus/theme-classic": "3.6.0", - "@docusaurus/theme-common": "3.6.0", - "@docusaurus/theme-search-algolia": "3.6.0", - "@docusaurus/types": "3.6.0" + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/@docusaurus/preset-classic/-/preset-classic-3.6.2.tgz", + "integrity": "sha512-r2n5eHdhiNSrJGsrrYcw+WsyStmXxe0ZG3RdA9LVyK5+jBHM8blrUWJEDugnzCNbyhUzhdtcmgCC9fhdAvKuQw==", + "dependencies": { + "@docusaurus/core": "3.6.2", + "@docusaurus/plugin-content-blog": "3.6.2", + "@docusaurus/plugin-content-docs": "3.6.2", + "@docusaurus/plugin-content-pages": "3.6.2", + "@docusaurus/plugin-debug": "3.6.2", + "@docusaurus/plugin-google-analytics": "3.6.2", + "@docusaurus/plugin-google-gtag": "3.6.2", + "@docusaurus/plugin-google-tag-manager": "3.6.2", + "@docusaurus/plugin-sitemap": "3.6.2", + "@docusaurus/theme-classic": "3.6.2", + "@docusaurus/theme-common": "3.6.2", + "@docusaurus/theme-search-algolia": "3.6.2", + "@docusaurus/types": "3.6.2" }, "engines": { "node": ">=18.0" @@ -2547,23 +3603,23 @@ } }, "node_modules/@docusaurus/theme-classic": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/@docusaurus/theme-classic/-/theme-classic-3.6.0.tgz", - "integrity": "sha512-sAXNfwPL6uRD+BuHuKXZfAXud7SS7IK/JdrPuzyQxdO1gJKzI5GFfe1ED1QoJDNWJWJ01JHE5rSnwYLEADc2rQ==", - "dependencies": { - "@docusaurus/core": "3.6.0", - "@docusaurus/logger": "3.6.0", - "@docusaurus/mdx-loader": "3.6.0", - "@docusaurus/module-type-aliases": "3.6.0", - "@docusaurus/plugin-content-blog": "3.6.0", - "@docusaurus/plugin-content-docs": "3.6.0", - "@docusaurus/plugin-content-pages": "3.6.0", - "@docusaurus/theme-common": "3.6.0", - "@docusaurus/theme-translations": "3.6.0", - "@docusaurus/types": "3.6.0", - "@docusaurus/utils": "3.6.0", - "@docusaurus/utils-common": "3.6.0", - "@docusaurus/utils-validation": "3.6.0", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-classic/-/theme-classic-3.6.2.tgz", + "integrity": "sha512-bCdOPqPNezhLx+hgNVO2Cf+8/1AHa9uHDOqTx/CKAx2I0J/jV9G+6JiMtpSRKGNfBoLT1O+56/7+WtkOf54xTw==", + "dependencies": { + "@docusaurus/core": "3.6.2", + "@docusaurus/logger": "3.6.2", + "@docusaurus/mdx-loader": "3.6.2", + "@docusaurus/module-type-aliases": "3.6.2", + "@docusaurus/plugin-content-blog": "3.6.2", + "@docusaurus/plugin-content-docs": "3.6.2", + "@docusaurus/plugin-content-pages": "3.6.2", + "@docusaurus/theme-common": "3.6.2", + "@docusaurus/theme-translations": "3.6.2", + "@docusaurus/types": "3.6.2", + "@docusaurus/utils": "3.6.2", + "@docusaurus/utils-common": "3.6.2", + "@docusaurus/utils-validation": "3.6.2", "@mdx-js/react": "^3.0.0", "clsx": "^2.0.0", "copy-text-to-clipboard": "^3.2.0", @@ -2587,14 +3643,14 @@ } }, "node_modules/@docusaurus/theme-common": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/@docusaurus/theme-common/-/theme-common-3.6.0.tgz", - "integrity": "sha512-frjlYE5sRs+GuPs4XXlp9aMLI2O4H5FPpznDAXBrCm+8EpWRiIb443ePMxM3IyMCQ5bwFlki0PI9C+r4apstnw==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-common/-/theme-common-3.6.2.tgz", + "integrity": "sha512-lfgsL064KEHpCkgGUc0OYoUPCpYfzggp6Hof8sz59UuKiLvb/Z7raewE9/NfocrJ2HZI17rLgMX3SQlRDh/5gg==", "dependencies": { - "@docusaurus/mdx-loader": "3.6.0", - "@docusaurus/module-type-aliases": "3.6.0", - "@docusaurus/utils": "3.6.0", - "@docusaurus/utils-common": "3.6.0", + "@docusaurus/mdx-loader": "3.6.2", + "@docusaurus/module-type-aliases": "3.6.2", + "@docusaurus/utils": "3.6.2", + "@docusaurus/utils-common": "3.6.2", "@types/history": "^4.7.11", "@types/react": "*", "@types/react-router-config": "*", @@ -2614,15 +3670,15 @@ } }, "node_modules/@docusaurus/theme-mermaid": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/@docusaurus/theme-mermaid/-/theme-mermaid-3.6.0.tgz", - "integrity": "sha512-5t7zzBnnJa4BBcGo9bEfTM48DxD/+CVbFkfiRnFXheWjMrMm5a+IP10igEQ4zyDC+QgatbzLAxkj4GRYpYTauA==", - "dependencies": { - "@docusaurus/core": "3.6.0", - "@docusaurus/module-type-aliases": "3.6.0", - "@docusaurus/theme-common": "3.6.0", - "@docusaurus/types": "3.6.0", - "@docusaurus/utils-validation": "3.6.0", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-mermaid/-/theme-mermaid-3.6.2.tgz", + "integrity": "sha512-Ui+rBtqMPKj3RCOxNlY04i1tEjNg+fZg4URTvkHmYR07hcKaJw+vkw+wlaYjd0HFZk+3Er9vUAcwsCWuea4cVQ==", + "dependencies": { + "@docusaurus/core": "3.6.2", + "@docusaurus/module-type-aliases": "3.6.2", + "@docusaurus/theme-common": "3.6.2", + "@docusaurus/types": "3.6.2", + "@docusaurus/utils-validation": "3.6.2", "mermaid": ">=10.4", "tslib": "^2.6.0" }, @@ -2635,18 +3691,18 @@ } }, "node_modules/@docusaurus/theme-search-algolia": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/@docusaurus/theme-search-algolia/-/theme-search-algolia-3.6.0.tgz", - "integrity": "sha512-4IwRUkxjrisR8LXBHeE4d2btraWdMficbgiVL3UHvJURmyvgzMBZQP8KrK8rjdXeu8SuRxSmeV6NSVomRvdbEg==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-search-algolia/-/theme-search-algolia-3.6.2.tgz", + "integrity": "sha512-SFLS+Rq8Cg2yepnHucA9sRpIR97yHvZWlCgMzBLunV3KHbB6hD2h5HPhFV39wYHYCjJUAOH1lX9poJ1qKYuSvg==", "dependencies": { "@docsearch/react": "^3.5.2", - "@docusaurus/core": "3.6.0", - "@docusaurus/logger": "3.6.0", - "@docusaurus/plugin-content-docs": "3.6.0", - "@docusaurus/theme-common": "3.6.0", - "@docusaurus/theme-translations": "3.6.0", - "@docusaurus/utils": "3.6.0", - "@docusaurus/utils-validation": "3.6.0", + "@docusaurus/core": "3.6.2", + "@docusaurus/logger": "3.6.2", + "@docusaurus/plugin-content-docs": "3.6.2", + "@docusaurus/theme-common": "3.6.2", + "@docusaurus/theme-translations": "3.6.2", + "@docusaurus/utils": "3.6.2", + "@docusaurus/utils-validation": "3.6.2", "algoliasearch": "^4.18.0", "algoliasearch-helper": "^3.13.3", "clsx": "^2.0.0", @@ -2665,9 +3721,9 @@ } }, "node_modules/@docusaurus/theme-translations": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/@docusaurus/theme-translations/-/theme-translations-3.6.0.tgz", - "integrity": "sha512-L555X8lWE3fv8VaF0Bc1VnAgi10UvRKFcvADHiYR7Gj37ItaWP5i7xLHsSw7fi/SHTXe5wfIeCFNqUYHyCOHAQ==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-translations/-/theme-translations-3.6.2.tgz", + "integrity": "sha512-LIWrYoDUsOTKmb0c7IQzawiPUTAaczBs5IOx6srxOWoTHVUMLzJCkl5Y6whfuRrnul8G05qv2vk238bN5Ko62g==", "dependencies": { "fs-extra": "^11.1.1", "tslib": "^2.6.0" @@ -2677,15 +3733,15 @@ } }, "node_modules/@docusaurus/tsconfig": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/@docusaurus/tsconfig/-/tsconfig-3.6.0.tgz", - "integrity": "sha512-1nHsSMlNgEifnvsL4ql9wx7I1xXhrrNZl65IKD11pdo/749oI9fMcvm47dDwgS57x1WEteIAxJjzidffa5J9TQ==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/@docusaurus/tsconfig/-/tsconfig-3.6.2.tgz", + "integrity": "sha512-TWLkyYHBYhIJNcXCEc3D1M9R8UFV4IZ82rGef5U9mE1ZrcgDUlZxYaYdoSuHrPrzPRIl3orjmpscO2FAk2gdZw==", "dev": true }, "node_modules/@docusaurus/types": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-3.6.0.tgz", - "integrity": "sha512-jADLgoZGWhAzThr+mRiyuFD4OUzt6jHnb7NRArRKorgxckqUBaPyFOau9hhbcSTHtU6ceyeWjN7FDt7uG2Hplw==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-3.6.2.tgz", + "integrity": "sha512-117Wsk6xXrWEAsCYCXS3TGJv5tkdIZDcd7T/V0UJvKYmY0gyVPPcEQChy8yTdjbIkbB2q4fa7Jpox72Qv86mqQ==", "dependencies": { "@mdx-js/mdx": "^3.0.0", "@types/history": "^4.7.11", @@ -2703,12 +3759,13 @@ } }, "node_modules/@docusaurus/utils": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/@docusaurus/utils/-/utils-3.6.0.tgz", - "integrity": "sha512-VKczAutI4mptiAw/WcYEu5WeVhQ6Q1zdIUl64SGw9K++9lziH+Kt10Ee8l2dMpRkiUk6zzK20kMNlX2WCUwXYQ==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/@docusaurus/utils/-/utils-3.6.2.tgz", + "integrity": "sha512-oxnpUcFZGE3uPCDoXr8GJriB3VWM9sFjPedFidX3Fsz87l1NZNc1wtbKPfQ7GYFDMYo2IGlAv5+47Me9RkM6lg==", "dependencies": { - "@docusaurus/logger": "3.6.0", - "@docusaurus/utils-common": "3.6.0", + "@docusaurus/logger": "3.6.2", + "@docusaurus/types": "3.6.2", + "@docusaurus/utils-common": "3.6.2", "@svgr/webpack": "^8.1.0", "escape-string-regexp": "^4.0.0", "file-loader": "^6.2.0", @@ -2730,43 +3787,28 @@ }, "engines": { "node": ">=18.0" - }, - "peerDependencies": { - "@docusaurus/types": "*" - }, - "peerDependenciesMeta": { - "@docusaurus/types": { - "optional": true - } } }, "node_modules/@docusaurus/utils-common": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/@docusaurus/utils-common/-/utils-common-3.6.0.tgz", - "integrity": "sha512-diUDNfbw33GaZMmKwdTckT2IBfVouXLXRD+zphH9ywswuaEIKqixvuf5g41H7MBBrlMsxhna3uTMoB4B/OPDcA==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/@docusaurus/utils-common/-/utils-common-3.6.2.tgz", + "integrity": "sha512-dr5wK+OyU2QAWxG7S5siD2bPgS7+ZeqWHfgLNHZ5yalaZf8TbeNNLqydfngukAY56BGZN0NbMkX6jGIr7ZF0sA==", "dependencies": { + "@docusaurus/types": "3.6.2", "tslib": "^2.6.0" }, "engines": { "node": ">=18.0" - }, - "peerDependencies": { - "@docusaurus/types": "*" - }, - "peerDependenciesMeta": { - "@docusaurus/types": { - "optional": true - } } }, "node_modules/@docusaurus/utils-validation": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/@docusaurus/utils-validation/-/utils-validation-3.6.0.tgz", - "integrity": "sha512-CRHiKKJEKA0GFlfOf71JWHl7PtwOyX0+Zg9ep9NFEZv6Lcx3RJ9nhl7p8HRjPL6deyYceavM//BsfW4pCI4BtA==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/@docusaurus/utils-validation/-/utils-validation-3.6.2.tgz", + "integrity": "sha512-Y3EwblDz72KOcobb5t2zlhHSmrfE8EaHusPJ96Kx2JYtNXL2omqCoOb6FpaXWhES75wvjUpkFLYfiNqAqEov8g==", "dependencies": { - "@docusaurus/logger": "3.6.0", - "@docusaurus/utils": "3.6.0", - "@docusaurus/utils-common": "3.6.0", + "@docusaurus/logger": "3.6.2", + "@docusaurus/utils": "3.6.2", + "@docusaurus/utils-common": "3.6.2", "fs-extra": "^11.2.0", "joi": "^17.9.2", "js-yaml": "^4.1.0", @@ -5175,6 +6217,42 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/css-blank-pseudo": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-7.0.1.tgz", + "integrity": "sha512-jf+twWGDf6LDoXDUode+nc7ZlrqfaNphrBIBrcmeP3D8yw1uPaix1gCC8LUQUGQ6CycuK2opkbFFWFuq/a94ag==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/css-blank-pseudo/node_modules/postcss-selector-parser": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.0.0.tgz", + "integrity": "sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/css-declaration-sorter": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-7.2.0.tgz", @@ -5186,6 +6264,65 @@ "postcss": "^8.0.9" } }, + "node_modules/css-has-pseudo": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/css-has-pseudo/-/css-has-pseudo-7.0.1.tgz", + "integrity": "sha512-EOcoyJt+OsuKfCADgLT7gADZI5jMzIe/AeI6MeAYKiFBDmNmM7kk46DtSfMj5AohUJisqVzopBpnQTlvbyaBWg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "dependencies": { + "@csstools/selector-specificity": "^5.0.0", + "postcss-selector-parser": "^7.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/css-has-pseudo/node_modules/@csstools/selector-specificity": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-5.0.0.tgz", + "integrity": "sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss-selector-parser": "^7.0.0" + } + }, + "node_modules/css-has-pseudo/node_modules/postcss-selector-parser": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.0.0.tgz", + "integrity": "sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/css-in-js-utils": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/css-in-js-utils/-/css-in-js-utils-3.1.0.tgz", @@ -5271,6 +6408,27 @@ } } }, + "node_modules/css-prefers-color-scheme": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/css-prefers-color-scheme/-/css-prefers-color-scheme-10.0.0.tgz", + "integrity": "sha512-VCtXZAWivRglTZditUfB4StnsWr6YVZ2PRtuxQLKTNRdtAf8tpzaVPE9zXIF3VaSc7O70iK/j1+NXxyQCqdPjQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, "node_modules/css-select": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", @@ -5317,6 +6475,21 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/cssdb": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-8.2.1.tgz", + "integrity": "sha512-KwEPys7lNsC8OjASI8RrmwOYYDcm0JOW9zQhcV83ejYcQkirTEyeAGui8aO2F5PiS6SLpxuTzl6qlMElIdsgIg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + } + ] + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -12210,49 +13383,177 @@ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", "engines": { - "node": ">=4" + "node": ">=4" + } + }, + "node_modules/postcss": { + "version": "8.4.38", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", + "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.0", + "source-map-js": "^1.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-attribute-case-insensitive": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-7.0.1.tgz", + "integrity": "sha512-Uai+SupNSqzlschRyNx3kbCTWgY/2hcwtHEI/ej2LJWc9JJ77qKgGptd8DHwY1mXtZ7Aoh4z4yxfwMBue9eNgw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-attribute-case-insensitive/node_modules/postcss-selector-parser": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.0.0.tgz", + "integrity": "sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-calc": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-9.0.1.tgz", + "integrity": "sha512-TipgjGyzP5QzEhsOZUaIkeO5mKeMFpebWzRogWG/ysonUlnHcq5aJe0jOjpfzUU8PeSaBQnrE8ehR0QA5vs8PQ==", + "dependencies": { + "postcss-selector-parser": "^6.0.11", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.2.2" + } + }, + "node_modules/postcss-clamp": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-clamp/-/postcss-clamp-4.1.0.tgz", + "integrity": "sha512-ry4b1Llo/9zz+PKC+030KUnPITTJAHeOwjfAyyB60eT0AorGLdzp52s31OsPRHRf8NchkgFoG2y6fCfn1IV1Ow==", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=7.6.0" + }, + "peerDependencies": { + "postcss": "^8.4.6" + } + }, + "node_modules/postcss-color-functional-notation": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/postcss-color-functional-notation/-/postcss-color-functional-notation-7.0.6.tgz", + "integrity": "sha512-wLXvm8RmLs14Z2nVpB4CWlnvaWPRcOZFltJSlcbYwSJ1EDZKsKDhPKIMecCnuU054KSmlmubkqczmm6qBPCBhA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "dependencies": { + "@csstools/css-color-parser": "^3.0.6", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "@csstools/postcss-progressive-custom-properties": "^4.0.0", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" } }, - "node_modules/postcss": { - "version": "8.4.38", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", - "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", + "node_modules/postcss-color-hex-alpha": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/postcss-color-hex-alpha/-/postcss-color-hex-alpha-10.0.0.tgz", + "integrity": "sha512-1kervM2cnlgPs2a8Vt/Qbe5cQ++N7rkYo/2rz2BkqJZIHQwaVuJgQH38REHrAi4uM0b1fqxMkWYmese94iMp3w==", "funding": [ { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" + "type": "github", + "url": "https://github.com/sponsors/csstools" }, { - "type": "github", - "url": "https://github.com/sponsors/ai" + "type": "opencollective", + "url": "https://opencollective.com/csstools" } ], "dependencies": { - "nanoid": "^3.3.7", - "picocolors": "^1.0.0", - "source-map-js": "^1.2.0" + "@csstools/utilities": "^2.0.0", + "postcss-value-parser": "^4.2.0" }, "engines": { - "node": "^10 || ^12 || >=14" + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" } }, - "node_modules/postcss-calc": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-9.0.1.tgz", - "integrity": "sha512-TipgjGyzP5QzEhsOZUaIkeO5mKeMFpebWzRogWG/ysonUlnHcq5aJe0jOjpfzUU8PeSaBQnrE8ehR0QA5vs8PQ==", + "node_modules/postcss-color-rebeccapurple": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-10.0.0.tgz", + "integrity": "sha512-JFta737jSP+hdAIEhk1Vs0q0YF5P8fFcj+09pweS8ktuGuZ8pPlykHsk6mPxZ8awDl4TrcxUqJo9l1IhVr/OjQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], "dependencies": { - "postcss-selector-parser": "^6.0.11", + "@csstools/utilities": "^2.0.0", "postcss-value-parser": "^4.2.0" }, "engines": { - "node": "^14 || ^16 || >=18.0" + "node": ">=18" }, "peerDependencies": { - "postcss": "^8.2.2" + "postcss": "^8.4" } }, "node_modules/postcss-colormin": { @@ -12287,6 +13588,136 @@ "postcss": "^8.4.31" } }, + "node_modules/postcss-custom-media": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-11.0.5.tgz", + "integrity": "sha512-SQHhayVNgDvSAdX9NQ/ygcDQGEY+aSF4b/96z7QUX6mqL5yl/JgG/DywcF6fW9XbnCRE+aVYk+9/nqGuzOPWeQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "dependencies": { + "@csstools/cascade-layer-name-parser": "^2.0.4", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "@csstools/media-query-list-parser": "^4.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-custom-properties": { + "version": "14.0.4", + "resolved": "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-14.0.4.tgz", + "integrity": "sha512-QnW8FCCK6q+4ierwjnmXF9Y9KF8q0JkbgVfvQEMa93x1GT8FvOiUevWCN2YLaOWyByeDX8S6VFbZEeWoAoXs2A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "dependencies": { + "@csstools/cascade-layer-name-parser": "^2.0.4", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "@csstools/utilities": "^2.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-custom-selectors": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/postcss-custom-selectors/-/postcss-custom-selectors-8.0.4.tgz", + "integrity": "sha512-ASOXqNvDCE0dAJ/5qixxPeL1aOVGHGW2JwSy7HyjWNbnWTQCl+fDc968HY1jCmZI0+BaYT5CxsOiUhavpG/7eg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "dependencies": { + "@csstools/cascade-layer-name-parser": "^2.0.4", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-custom-selectors/node_modules/postcss-selector-parser": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.0.0.tgz", + "integrity": "sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-dir-pseudo-class": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-9.0.1.tgz", + "integrity": "sha512-tRBEK0MHYvcMUrAuYMEOa0zg9APqirBcgzi6P21OhxtJyJADo/SWBwY1CAwEohQ/6HDaa9jCjLRG7K3PVQYHEA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-dir-pseudo-class/node_modules/postcss-selector-parser": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.0.0.tgz", + "integrity": "sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/postcss-discard-comments": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-6.0.2.tgz", @@ -12316,33 +13747,213 @@ "engines": { "node": "^14 || ^16 || >=18.0" }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-discard-overridden": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-6.0.2.tgz", - "integrity": "sha512-j87xzI4LUggC5zND7KdjsI25APtyMuynXZSujByMaav2roV6OZX+8AaCUcZSWqckZpjAjRyFDdpqybgjFO0HJQ==", + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-discard-overridden": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-6.0.2.tgz", + "integrity": "sha512-j87xzI4LUggC5zND7KdjsI25APtyMuynXZSujByMaav2roV6OZX+8AaCUcZSWqckZpjAjRyFDdpqybgjFO0HJQ==", + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-discard-unused": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/postcss-discard-unused/-/postcss-discard-unused-6.0.5.tgz", + "integrity": "sha512-wHalBlRHkaNnNwfC8z+ppX57VhvS+HWgjW508esjdaEYr3Mx7Gnn2xA4R/CKf5+Z9S5qsqC+Uzh4ueENWwCVUA==", + "dependencies": { + "postcss-selector-parser": "^6.0.16" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-double-position-gradients": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-double-position-gradients/-/postcss-double-position-gradients-6.0.0.tgz", + "integrity": "sha512-JkIGah3RVbdSEIrcobqj4Gzq0h53GG4uqDPsho88SgY84WnpkTpI0k50MFK/sX7XqVisZ6OqUfFnoUO6m1WWdg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "dependencies": { + "@csstools/postcss-progressive-custom-properties": "^4.0.0", + "@csstools/utilities": "^2.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-focus-visible": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/postcss-focus-visible/-/postcss-focus-visible-10.0.1.tgz", + "integrity": "sha512-U58wyjS/I1GZgjRok33aE8juW9qQgQUNwTSdxQGuShHzwuYdcklnvK/+qOWX1Q9kr7ysbraQ6ht6r+udansalA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-focus-visible/node_modules/postcss-selector-parser": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.0.0.tgz", + "integrity": "sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-focus-within": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/postcss-focus-within/-/postcss-focus-within-9.0.1.tgz", + "integrity": "sha512-fzNUyS1yOYa7mOjpci/bR+u+ESvdar6hk8XNK/TRR0fiGTp2QT5N+ducP0n3rfH/m9I7H/EQU6lsa2BrgxkEjw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-focus-within/node_modules/postcss-selector-parser": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.0.0.tgz", + "integrity": "sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-font-variant": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-font-variant/-/postcss-font-variant-5.0.0.tgz", + "integrity": "sha512-1fmkBaCALD72CK2a9i468mA/+tr9/1cBxRRMXOUaZqO43oWPR5imcyPjXwuv7PXbCid4ndlP5zWhidQVVa3hmA==", + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-gap-properties": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-gap-properties/-/postcss-gap-properties-6.0.0.tgz", + "integrity": "sha512-Om0WPjEwiM9Ru+VhfEDPZJAKWUd0mV1HmNXqp2C29z80aQ2uP9UVhLc7e3aYMIor/S5cVhoPgYQ7RtfeZpYTRw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-image-set-function": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-image-set-function/-/postcss-image-set-function-7.0.0.tgz", + "integrity": "sha512-QL7W7QNlZuzOwBTeXEmbVckNt1FSmhQtbMRvGGqqU4Nf4xk6KUEQhAoWuMzwbSv5jxiRiSZ5Tv7eiDB9U87znA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "dependencies": { + "@csstools/utilities": "^2.0.0", + "postcss-value-parser": "^4.2.0" + }, "engines": { - "node": "^14 || ^16 || >=18.0" + "node": ">=18" }, "peerDependencies": { - "postcss": "^8.4.31" + "postcss": "^8.4" } }, - "node_modules/postcss-discard-unused": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/postcss-discard-unused/-/postcss-discard-unused-6.0.5.tgz", - "integrity": "sha512-wHalBlRHkaNnNwfC8z+ppX57VhvS+HWgjW508esjdaEYr3Mx7Gnn2xA4R/CKf5+Z9S5qsqC+Uzh4ueENWwCVUA==", + "node_modules/postcss-lab-function": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/postcss-lab-function/-/postcss-lab-function-7.0.6.tgz", + "integrity": "sha512-HPwvsoK7C949vBZ+eMyvH2cQeMr3UREoHvbtra76/UhDuiViZH6pir+z71UaJQohd7VDSVUdR6TkWYKExEc9aQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], "dependencies": { - "postcss-selector-parser": "^6.0.16" + "@csstools/css-color-parser": "^3.0.6", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "@csstools/postcss-progressive-custom-properties": "^4.0.0", + "@csstools/utilities": "^2.0.0" }, "engines": { - "node": "^14 || ^16 || >=18.0" + "node": ">=18" }, "peerDependencies": { - "postcss": "^8.4.31" + "postcss": "^8.4" } }, "node_modules/postcss-loader": { @@ -12366,6 +13977,30 @@ "webpack": "^5.0.0" } }, + "node_modules/postcss-logical": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/postcss-logical/-/postcss-logical-8.0.0.tgz", + "integrity": "sha512-HpIdsdieClTjXLOyYdUPAX/XQASNIwdKt5hoZW08ZOAiI+tbV0ta1oclkpVkW5ANU+xJvk3KkA0FejkjGLXUkg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, "node_modules/postcss-merge-idents": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/postcss-merge-idents/-/postcss-merge-idents-6.0.3.tgz", @@ -12485,12 +14120,12 @@ } }, "node_modules/postcss-modules-local-by-default": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.5.tgz", - "integrity": "sha512-6MieY7sIfTK0hYfafw1OMEG+2bg8Q1ocHCpoWLqOKj3JXlKu4G7btkmM/B7lFubYkYWmRSPLZi5chid63ZaZYw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.1.0.tgz", + "integrity": "sha512-rm0bdSv4jC3BDma3s9H19ZddW0aHX6EoqwDYU2IfZhRN+53QrufTRo2IdkAbRqLx4R2IYbZnbjKKxg4VN5oU9Q==", "dependencies": { "icss-utils": "^5.0.0", - "postcss-selector-parser": "^6.0.2", + "postcss-selector-parser": "^7.0.0", "postcss-value-parser": "^4.1.0" }, "engines": { @@ -12500,12 +14135,24 @@ "postcss": "^8.1.0" } }, + "node_modules/postcss-modules-local-by-default/node_modules/postcss-selector-parser": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.0.0.tgz", + "integrity": "sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/postcss-modules-scope": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.0.tgz", - "integrity": "sha512-oq+g1ssrsZOsx9M96c5w8laRmvEu9C3adDSjI8oTcbfkrTE8hx/zfyobUoWIxaKPO8bt6S62kxpw5GqypEw1QQ==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz", + "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==", "dependencies": { - "postcss-selector-parser": "^6.0.4" + "postcss-selector-parser": "^7.0.0" }, "engines": { "node": "^10 || ^12 || >= 14" @@ -12514,6 +14161,18 @@ "postcss": "^8.1.0" } }, + "node_modules/postcss-modules-scope/node_modules/postcss-selector-parser": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.0.0.tgz", + "integrity": "sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/postcss-modules-values": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", @@ -12528,6 +14187,86 @@ "postcss": "^8.1.0" } }, + "node_modules/postcss-nesting": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-13.0.1.tgz", + "integrity": "sha512-VbqqHkOBOt4Uu3G8Dm8n6lU5+9cJFxiuty9+4rcoyRPO9zZS1JIs6td49VIoix3qYqELHlJIn46Oih9SAKo+yQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "dependencies": { + "@csstools/selector-resolve-nested": "^3.0.0", + "@csstools/selector-specificity": "^5.0.0", + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-nesting/node_modules/@csstools/selector-resolve-nested": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-resolve-nested/-/selector-resolve-nested-3.0.0.tgz", + "integrity": "sha512-ZoK24Yku6VJU1gS79a5PFmC8yn3wIapiKmPgun0hZgEI5AOqgH2kiPRsPz1qkGv4HL+wuDLH83yQyk6inMYrJQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss-selector-parser": "^7.0.0" + } + }, + "node_modules/postcss-nesting/node_modules/@csstools/selector-specificity": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-5.0.0.tgz", + "integrity": "sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss-selector-parser": "^7.0.0" + } + }, + "node_modules/postcss-nesting/node_modules/postcss-selector-parser": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.0.0.tgz", + "integrity": "sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/postcss-normalize-charset": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-6.0.2.tgz", @@ -12652,6 +14391,27 @@ "postcss": "^8.4.31" } }, + "node_modules/postcss-opacity-percentage": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-opacity-percentage/-/postcss-opacity-percentage-3.0.0.tgz", + "integrity": "sha512-K6HGVzyxUxd/VgZdX04DCtdwWJ4NGLG212US4/LA1TLAbHgmAsTWVR86o+gGIbFtnTkfOpb9sCRBx8K7HO66qQ==", + "funding": [ + { + "type": "kofi", + "url": "https://ko-fi.com/mrcgrtz" + }, + { + "type": "liberapay", + "url": "https://liberapay.com/mrcgrtz" + } + ], + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, "node_modules/postcss-ordered-values": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-6.0.2.tgz", @@ -12667,6 +14427,184 @@ "postcss": "^8.4.31" } }, + "node_modules/postcss-overflow-shorthand": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-overflow-shorthand/-/postcss-overflow-shorthand-6.0.0.tgz", + "integrity": "sha512-BdDl/AbVkDjoTofzDQnwDdm/Ym6oS9KgmO7Gr+LHYjNWJ6ExORe4+3pcLQsLA9gIROMkiGVjjwZNoL/mpXHd5Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-page-break": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/postcss-page-break/-/postcss-page-break-3.0.4.tgz", + "integrity": "sha512-1JGu8oCjVXLa9q9rFTo4MbeeA5FMe00/9C7lN4va606Rdb+HkxXtXsmEDrIraQ11fGz/WvKWa8gMuCKkrXpTsQ==", + "peerDependencies": { + "postcss": "^8" + } + }, + "node_modules/postcss-place": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/postcss-place/-/postcss-place-10.0.0.tgz", + "integrity": "sha512-5EBrMzat2pPAxQNWYavwAfoKfYcTADJ8AXGVPcUZ2UkNloUTWzJQExgrzrDkh3EKzmAx1evfTAzF9I8NGcc+qw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-preset-env": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-10.1.1.tgz", + "integrity": "sha512-wqqsnBFD6VIwcHHRbhjTOcOi4qRVlB26RwSr0ordPj7OubRRxdWebv/aLjKLRR8zkZrbxZyuus03nOIgC5elMQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "dependencies": { + "@csstools/postcss-cascade-layers": "^5.0.1", + "@csstools/postcss-color-function": "^4.0.6", + "@csstools/postcss-color-mix-function": "^3.0.6", + "@csstools/postcss-content-alt-text": "^2.0.4", + "@csstools/postcss-exponential-functions": "^2.0.5", + "@csstools/postcss-font-format-keywords": "^4.0.0", + "@csstools/postcss-gamut-mapping": "^2.0.6", + "@csstools/postcss-gradients-interpolation-method": "^5.0.6", + "@csstools/postcss-hwb-function": "^4.0.6", + "@csstools/postcss-ic-unit": "^4.0.0", + "@csstools/postcss-initial": "^2.0.0", + "@csstools/postcss-is-pseudo-class": "^5.0.1", + "@csstools/postcss-light-dark-function": "^2.0.7", + "@csstools/postcss-logical-float-and-clear": "^3.0.0", + "@csstools/postcss-logical-overflow": "^2.0.0", + "@csstools/postcss-logical-overscroll-behavior": "^2.0.0", + "@csstools/postcss-logical-resize": "^3.0.0", + "@csstools/postcss-logical-viewport-units": "^3.0.3", + "@csstools/postcss-media-minmax": "^2.0.5", + "@csstools/postcss-media-queries-aspect-ratio-number-values": "^3.0.4", + "@csstools/postcss-nested-calc": "^4.0.0", + "@csstools/postcss-normalize-display-values": "^4.0.0", + "@csstools/postcss-oklab-function": "^4.0.6", + "@csstools/postcss-progressive-custom-properties": "^4.0.0", + "@csstools/postcss-random-function": "^1.0.1", + "@csstools/postcss-relative-color-syntax": "^3.0.6", + "@csstools/postcss-scope-pseudo-class": "^4.0.1", + "@csstools/postcss-sign-functions": "^1.1.0", + "@csstools/postcss-stepped-value-functions": "^4.0.5", + "@csstools/postcss-text-decoration-shorthand": "^4.0.1", + "@csstools/postcss-trigonometric-functions": "^4.0.5", + "@csstools/postcss-unset-value": "^4.0.0", + "autoprefixer": "^10.4.19", + "browserslist": "^4.23.1", + "css-blank-pseudo": "^7.0.1", + "css-has-pseudo": "^7.0.1", + "css-prefers-color-scheme": "^10.0.0", + "cssdb": "^8.2.1", + "postcss-attribute-case-insensitive": "^7.0.1", + "postcss-clamp": "^4.1.0", + "postcss-color-functional-notation": "^7.0.6", + "postcss-color-hex-alpha": "^10.0.0", + "postcss-color-rebeccapurple": "^10.0.0", + "postcss-custom-media": "^11.0.5", + "postcss-custom-properties": "^14.0.4", + "postcss-custom-selectors": "^8.0.4", + "postcss-dir-pseudo-class": "^9.0.1", + "postcss-double-position-gradients": "^6.0.0", + "postcss-focus-visible": "^10.0.1", + "postcss-focus-within": "^9.0.1", + "postcss-font-variant": "^5.0.0", + "postcss-gap-properties": "^6.0.0", + "postcss-image-set-function": "^7.0.0", + "postcss-lab-function": "^7.0.6", + "postcss-logical": "^8.0.0", + "postcss-nesting": "^13.0.1", + "postcss-opacity-percentage": "^3.0.0", + "postcss-overflow-shorthand": "^6.0.0", + "postcss-page-break": "^3.0.4", + "postcss-place": "^10.0.0", + "postcss-pseudo-class-any-link": "^10.0.1", + "postcss-replace-overflow-wrap": "^4.0.0", + "postcss-selector-not": "^8.0.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-pseudo-class-any-link": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-10.0.1.tgz", + "integrity": "sha512-3el9rXlBOqTFaMFkWDOkHUTQekFIYnaQY55Rsp8As8QQkpiSgIYEcF/6Ond93oHiDsGb4kad8zjt+NPlOC1H0Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-pseudo-class-any-link/node_modules/postcss-selector-parser": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.0.0.tgz", + "integrity": "sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/postcss-reduce-idents": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/postcss-reduce-idents/-/postcss-reduce-idents-6.0.3.tgz", @@ -12710,6 +14648,50 @@ "postcss": "^8.4.31" } }, + "node_modules/postcss-replace-overflow-wrap": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-4.0.0.tgz", + "integrity": "sha512-KmF7SBPphT4gPPcKZc7aDkweHiKEEO8cla/GjcBK+ckKxiZslIu3C4GCRW3DNfL0o7yW7kMQu9xlZ1kXRXLXtw==", + "peerDependencies": { + "postcss": "^8.0.3" + } + }, + "node_modules/postcss-selector-not": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/postcss-selector-not/-/postcss-selector-not-8.0.1.tgz", + "integrity": "sha512-kmVy/5PYVb2UOhy0+LqUYAhKj7DUGDpSWa5LZqlkWJaaAV+dxxsOG3+St0yNLu6vsKD7Dmqx+nWQt0iil89+WA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-selector-not/node_modules/postcss-selector-parser": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.0.0.tgz", + "integrity": "sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/postcss-selector-parser": { "version": "6.1.2", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", @@ -13066,9 +15048,9 @@ } }, "node_modules/react-dev-utils/node_modules/loader-utils": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.1.tgz", - "integrity": "sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.3.1.tgz", + "integrity": "sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg==", "engines": { "node": ">= 12.13.0" } @@ -13930,9 +15912,9 @@ } }, "node_modules/search-insights": { - "version": "2.17.2", - "resolved": "https://registry.npmjs.org/search-insights/-/search-insights-2.17.2.tgz", - "integrity": "sha512-zFNpOpUO+tY2D85KrxJ+aqwnIfdEGi06UH2+xEb+Bp9Mwznmauqc9djbnBibJO5mpfUPPa8st6Sx65+vbeO45g==", + "version": "2.17.3", + "resolved": "https://registry.npmjs.org/search-insights/-/search-insights-2.17.3.tgz", + "integrity": "sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ==", "peer": true }, "node_modules/section-matter": { @@ -14500,9 +16482,9 @@ } }, "node_modules/std-env": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.7.0.tgz", - "integrity": "sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==" + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.8.0.tgz", + "integrity": "sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w==" }, "node_modules/string_decoder": { "version": "1.3.0", diff --git a/docs/package.json b/docs/package.json index 599c15b29df5..d20a9faf8ada 100644 --- a/docs/package.json +++ b/docs/package.json @@ -15,10 +15,10 @@ "typecheck": "tsc" }, "dependencies": { - "@docusaurus/core": "^3.6.0", - "@docusaurus/plugin-content-pages": "^3.6.0", - "@docusaurus/preset-classic": "^3.6.0", - "@docusaurus/theme-mermaid": "^3.6.0", + "@docusaurus/core": "^3.6.2", + "@docusaurus/plugin-content-pages": "^3.6.2", + "@docusaurus/preset-classic": "^3.6.2", + "@docusaurus/theme-mermaid": "^3.6.2", "@mdx-js/react": "^3.1.0", "clsx": "^2.0.0", "prism-react-renderer": "^2.4.0", @@ -29,7 +29,7 @@ }, "devDependencies": { "@docusaurus/module-type-aliases": "^3.5.1", - "@docusaurus/tsconfig": "^3.6.0", + "@docusaurus/tsconfig": "^3.6.2", "@docusaurus/types": "^3.5.1", "typescript": "~5.6.3" }, diff --git a/docs/yarn.lock b/docs/yarn.lock index 0ec8d2f49906..a4f770f428bc 100644 --- a/docs/yarn.lock +++ b/docs/yarn.lock @@ -1208,6 +1208,328 @@ resolved "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz" integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== +"@csstools/cascade-layer-name-parser@^2.0.4": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@csstools/cascade-layer-name-parser/-/cascade-layer-name-parser-2.0.4.tgz#64d128529397aa1e1c986f685713363b262b81b1" + integrity sha512-7DFHlPuIxviKYZrOiwVU/PiHLm3lLUR23OMuEEtfEOQTOp9hzQ2JjdY6X5H18RVuUPJqSCI+qNnD5iOLMVE0bA== + +"@csstools/color-helpers@^5.0.1": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@csstools/color-helpers/-/color-helpers-5.0.1.tgz#829f1c76f5800b79c51c709e2f36821b728e0e10" + integrity sha512-MKtmkA0BX87PKaO1NFRTFH+UnkgnmySQOvNxJubsadusqPEC2aJ9MOQiMceZJJ6oitUl/i0L6u0M1IrmAOmgBA== + +"@csstools/css-calc@^2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@csstools/css-calc/-/css-calc-2.1.0.tgz#3f28b8f8f736b8f78abbc75eebd55c756207e773" + integrity sha512-X69PmFOrjTZfN5ijxtI8hZ9kRADFSLrmmQ6hgDJ272Il049WGKpDY64KhrFm/7rbWve0z81QepawzjkKlqkNGw== + +"@csstools/css-color-parser@^3.0.6": + version "3.0.6" + resolved "https://registry.yarnpkg.com/@csstools/css-color-parser/-/css-color-parser-3.0.6.tgz#e646838f6aab4618aeea7ba0c4921a254e180276" + integrity sha512-S/IjXqTHdpI4EtzGoNCHfqraXF37x12ZZHA1Lk7zoT5pm2lMjFuqhX/89L7dqX4CcMacKK+6ZCs5TmEGb/+wKw== + dependencies: + "@csstools/color-helpers" "^5.0.1" + "@csstools/css-calc" "^2.1.0" + +"@csstools/css-parser-algorithms@^3.0.4": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.4.tgz#74426e93bd1c4dcab3e441f5cc7ba4fb35d94356" + integrity sha512-Up7rBoV77rv29d3uKHUIVubz1BTcgyUK72IvCQAbfbMv584xHcGKCKbWh7i8hPrRJ7qU4Y8IO3IY9m+iTB7P3A== + +"@csstools/css-tokenizer@^3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@csstools/css-tokenizer/-/css-tokenizer-3.0.3.tgz#a5502c8539265fecbd873c1e395a890339f119c2" + integrity sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw== + +"@csstools/media-query-list-parser@^4.0.2": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@csstools/media-query-list-parser/-/media-query-list-parser-4.0.2.tgz#e80e17eba1693fceafb8d6f2cfc68c0e7a9ab78a" + integrity sha512-EUos465uvVvMJehckATTlNqGj4UJWkTmdWuDMjqvSUkjGpmOyFZBVwb4knxCm/k2GMTXY+c/5RkdndzFYWeX5A== + +"@csstools/postcss-cascade-layers@^5.0.1": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@csstools/postcss-cascade-layers/-/postcss-cascade-layers-5.0.1.tgz#9640313e64b5e39133de7e38a5aa7f40dc259597" + integrity sha512-XOfhI7GShVcKiKwmPAnWSqd2tBR0uxt+runAxttbSp/LY2U16yAVPmAf7e9q4JJ0d+xMNmpwNDLBXnmRCl3HMQ== + dependencies: + "@csstools/selector-specificity" "^5.0.0" + postcss-selector-parser "^7.0.0" + +"@csstools/postcss-color-function@^4.0.6": + version "4.0.6" + resolved "https://registry.yarnpkg.com/@csstools/postcss-color-function/-/postcss-color-function-4.0.6.tgz#dabd1e516ccd4c7bd5803e37075a503b5f7f0ac4" + integrity sha512-EcvXfC60cTIumzpsxWuvVjb7rsJEHPvqn3jeMEBUaE3JSc4FRuP7mEQ+1eicxWmIrs3FtzMH9gR3sgA5TH+ebQ== + dependencies: + "@csstools/css-color-parser" "^3.0.6" + "@csstools/css-parser-algorithms" "^3.0.4" + "@csstools/css-tokenizer" "^3.0.3" + "@csstools/postcss-progressive-custom-properties" "^4.0.0" + "@csstools/utilities" "^2.0.0" + +"@csstools/postcss-color-mix-function@^3.0.6": + version "3.0.6" + resolved "https://registry.yarnpkg.com/@csstools/postcss-color-mix-function/-/postcss-color-mix-function-3.0.6.tgz#d971832ec30b3b60363bceddfeb4b90c7cc0f4b8" + integrity sha512-jVKdJn4+JkASYGhyPO+Wa5WXSx1+oUgaXb3JsjJn/BlrtFh5zjocCY7pwWi0nuP24V1fY7glQsxEYcYNy0dMFg== + dependencies: + "@csstools/css-color-parser" "^3.0.6" + "@csstools/css-parser-algorithms" "^3.0.4" + "@csstools/css-tokenizer" "^3.0.3" + "@csstools/postcss-progressive-custom-properties" "^4.0.0" + "@csstools/utilities" "^2.0.0" + +"@csstools/postcss-content-alt-text@^2.0.4": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@csstools/postcss-content-alt-text/-/postcss-content-alt-text-2.0.4.tgz#76f4687fb15ed45bc1139bb71e5775779762897a" + integrity sha512-YItlZUOuZJCBlRaCf8Aucc1lgN41qYGALMly0qQllrxYJhiyzlI6RxOTMUvtWk+KhS8GphMDsDhKQ7KTPfEMSw== + dependencies: + "@csstools/css-parser-algorithms" "^3.0.4" + "@csstools/css-tokenizer" "^3.0.3" + "@csstools/postcss-progressive-custom-properties" "^4.0.0" + "@csstools/utilities" "^2.0.0" + +"@csstools/postcss-exponential-functions@^2.0.5": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@csstools/postcss-exponential-functions/-/postcss-exponential-functions-2.0.5.tgz#0c39f75df3357ee1e444b0aa0ede4e12aafea0e9" + integrity sha512-mi8R6dVfA2nDoKM3wcEi64I8vOYEgQVtVKCfmLHXupeLpACfGAided5ddMt5f+CnEodNu4DifuVwb0I6fQDGGQ== + dependencies: + "@csstools/css-calc" "^2.1.0" + "@csstools/css-parser-algorithms" "^3.0.4" + "@csstools/css-tokenizer" "^3.0.3" + +"@csstools/postcss-font-format-keywords@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-font-format-keywords/-/postcss-font-format-keywords-4.0.0.tgz#6730836eb0153ff4f3840416cc2322f129c086e6" + integrity sha512-usBzw9aCRDvchpok6C+4TXC57btc4bJtmKQWOHQxOVKen1ZfVqBUuCZ/wuqdX5GHsD0NRSr9XTP+5ID1ZZQBXw== + dependencies: + "@csstools/utilities" "^2.0.0" + postcss-value-parser "^4.2.0" + +"@csstools/postcss-gamut-mapping@^2.0.6": + version "2.0.6" + resolved "https://registry.yarnpkg.com/@csstools/postcss-gamut-mapping/-/postcss-gamut-mapping-2.0.6.tgz#04ec6a50fdbca2a30dec56e6bb780c79621e47a7" + integrity sha512-0ke7fmXfc8H+kysZz246yjirAH6JFhyX9GTlyRnM0exHO80XcA9zeJpy5pOp5zo/AZiC/q5Pf+Hw7Pd6/uAoYA== + dependencies: + "@csstools/css-color-parser" "^3.0.6" + "@csstools/css-parser-algorithms" "^3.0.4" + "@csstools/css-tokenizer" "^3.0.3" + +"@csstools/postcss-gradients-interpolation-method@^5.0.6": + version "5.0.6" + resolved "https://registry.yarnpkg.com/@csstools/postcss-gradients-interpolation-method/-/postcss-gradients-interpolation-method-5.0.6.tgz#67fa61ada95e4534687fa76cd2d15ac74386560e" + integrity sha512-Itrbx6SLUzsZ6Mz3VuOlxhbfuyLTogG5DwEF1V8dAi24iMuvQPIHd7Ti+pNDp7j6WixndJGZaoNR0f9VSzwuTg== + dependencies: + "@csstools/css-color-parser" "^3.0.6" + "@csstools/css-parser-algorithms" "^3.0.4" + "@csstools/css-tokenizer" "^3.0.3" + "@csstools/postcss-progressive-custom-properties" "^4.0.0" + "@csstools/utilities" "^2.0.0" + +"@csstools/postcss-hwb-function@^4.0.6": + version "4.0.6" + resolved "https://registry.yarnpkg.com/@csstools/postcss-hwb-function/-/postcss-hwb-function-4.0.6.tgz#c40f557a54ed45e75c601a9ba7a08d315f64dbd7" + integrity sha512-927Pqy3a1uBP7U8sTfaNdZVB0mNXzIrJO/GZ8us9219q9n06gOqCdfZ0E6d1P66Fm0fYHvxfDbfcUuwAn5UwhQ== + dependencies: + "@csstools/css-color-parser" "^3.0.6" + "@csstools/css-parser-algorithms" "^3.0.4" + "@csstools/css-tokenizer" "^3.0.3" + "@csstools/postcss-progressive-custom-properties" "^4.0.0" + "@csstools/utilities" "^2.0.0" + +"@csstools/postcss-ic-unit@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-ic-unit/-/postcss-ic-unit-4.0.0.tgz#b60ec06500717c337447c39ae7fe7952eeb9d48f" + integrity sha512-9QT5TDGgx7wD3EEMN3BSUG6ckb6Eh5gSPT5kZoVtUuAonfPmLDJyPhqR4ntPpMYhUKAMVKAg3I/AgzqHMSeLhA== + dependencies: + "@csstools/postcss-progressive-custom-properties" "^4.0.0" + "@csstools/utilities" "^2.0.0" + postcss-value-parser "^4.2.0" + +"@csstools/postcss-initial@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-initial/-/postcss-initial-2.0.0.tgz#a86f5fc59ab9f16f1422dade4c58bd941af5df22" + integrity sha512-dv2lNUKR+JV+OOhZm9paWzYBXOCi+rJPqJ2cJuhh9xd8USVrd0cBEPczla81HNOyThMQWeCcdln3gZkQV2kYxA== + +"@csstools/postcss-is-pseudo-class@^5.0.1": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@csstools/postcss-is-pseudo-class/-/postcss-is-pseudo-class-5.0.1.tgz#12041448fedf01090dd4626022c28b7f7623f58e" + integrity sha512-JLp3POui4S1auhDR0n8wHd/zTOWmMsmK3nQd3hhL6FhWPaox5W7j1se6zXOG/aP07wV2ww0lxbKYGwbBszOtfQ== + dependencies: + "@csstools/selector-specificity" "^5.0.0" + postcss-selector-parser "^7.0.0" + +"@csstools/postcss-light-dark-function@^2.0.7": + version "2.0.7" + resolved "https://registry.yarnpkg.com/@csstools/postcss-light-dark-function/-/postcss-light-dark-function-2.0.7.tgz#807c170cd28eebb0c00e64dfc6ab0bf418f19209" + integrity sha512-ZZ0rwlanYKOHekyIPaU+sVm3BEHCe+Ha0/px+bmHe62n0Uc1lL34vbwrLYn6ote8PHlsqzKeTQdIejQCJ05tfw== + dependencies: + "@csstools/css-parser-algorithms" "^3.0.4" + "@csstools/css-tokenizer" "^3.0.3" + "@csstools/postcss-progressive-custom-properties" "^4.0.0" + "@csstools/utilities" "^2.0.0" + +"@csstools/postcss-logical-float-and-clear@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-logical-float-and-clear/-/postcss-logical-float-and-clear-3.0.0.tgz#62617564182cf86ab5d4e7485433ad91e4c58571" + integrity sha512-SEmaHMszwakI2rqKRJgE+8rpotFfne1ZS6bZqBoQIicFyV+xT1UF42eORPxJkVJVrH9C0ctUgwMSn3BLOIZldQ== + +"@csstools/postcss-logical-overflow@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-logical-overflow/-/postcss-logical-overflow-2.0.0.tgz#c6de7c5f04e3d4233731a847f6c62819bcbcfa1d" + integrity sha512-spzR1MInxPuXKEX2csMamshR4LRaSZ3UXVaRGjeQxl70ySxOhMpP2252RAFsg8QyyBXBzuVOOdx1+bVO5bPIzA== + +"@csstools/postcss-logical-overscroll-behavior@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-logical-overscroll-behavior/-/postcss-logical-overscroll-behavior-2.0.0.tgz#43c03eaecdf34055ef53bfab691db6dc97a53d37" + integrity sha512-e/webMjoGOSYfqLunyzByZj5KKe5oyVg/YSbie99VEaSDE2kimFm0q1f6t/6Jo+VVCQ/jbe2Xy+uX+C4xzWs4w== + +"@csstools/postcss-logical-resize@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-logical-resize/-/postcss-logical-resize-3.0.0.tgz#4df0eeb1a61d7bd85395e56a5cce350b5dbfdca6" + integrity sha512-DFbHQOFW/+I+MY4Ycd/QN6Dg4Hcbb50elIJCfnwkRTCX05G11SwViI5BbBlg9iHRl4ytB7pmY5ieAFk3ws7yyg== + dependencies: + postcss-value-parser "^4.2.0" + +"@csstools/postcss-logical-viewport-units@^3.0.3": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@csstools/postcss-logical-viewport-units/-/postcss-logical-viewport-units-3.0.3.tgz#f6cc63520ca2a6eb76b9cd946070c38dda66d733" + integrity sha512-OC1IlG/yoGJdi0Y+7duz/kU/beCwO+Gua01sD6GtOtLi7ByQUpcIqs7UE/xuRPay4cHgOMatWdnDdsIDjnWpPw== + dependencies: + "@csstools/css-tokenizer" "^3.0.3" + "@csstools/utilities" "^2.0.0" + +"@csstools/postcss-media-minmax@^2.0.5": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@csstools/postcss-media-minmax/-/postcss-media-minmax-2.0.5.tgz#66970aa8d8057f84b88aff21f385194fbe03eb11" + integrity sha512-sdh5i5GToZOIAiwhdntRWv77QDtsxP2r2gXW/WbLSCoLr00KTq/yiF1qlQ5XX2+lmiFa8rATKMcbwl3oXDMNew== + dependencies: + "@csstools/css-calc" "^2.1.0" + "@csstools/css-parser-algorithms" "^3.0.4" + "@csstools/css-tokenizer" "^3.0.3" + "@csstools/media-query-list-parser" "^4.0.2" + +"@csstools/postcss-media-queries-aspect-ratio-number-values@^3.0.4": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@csstools/postcss-media-queries-aspect-ratio-number-values/-/postcss-media-queries-aspect-ratio-number-values-3.0.4.tgz#d71102172c74baf3f892fac88cf1ea46a961600d" + integrity sha512-AnGjVslHMm5xw9keusQYvjVWvuS7KWK+OJagaG0+m9QnIjZsrysD2kJP/tr/UJIyYtMCtu8OkUd+Rajb4DqtIQ== + dependencies: + "@csstools/css-parser-algorithms" "^3.0.4" + "@csstools/css-tokenizer" "^3.0.3" + "@csstools/media-query-list-parser" "^4.0.2" + +"@csstools/postcss-nested-calc@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-nested-calc/-/postcss-nested-calc-4.0.0.tgz#754e10edc6958d664c11cde917f44ba144141c62" + integrity sha512-jMYDdqrQQxE7k9+KjstC3NbsmC063n1FTPLCgCRS2/qHUbHM0mNy9pIn4QIiQGs9I/Bg98vMqw7mJXBxa0N88A== + dependencies: + "@csstools/utilities" "^2.0.0" + postcss-value-parser "^4.2.0" + +"@csstools/postcss-normalize-display-values@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-normalize-display-values/-/postcss-normalize-display-values-4.0.0.tgz#ecdde2daf4e192e5da0c6fd933b6d8aff32f2a36" + integrity sha512-HlEoG0IDRoHXzXnkV4in47dzsxdsjdz6+j7MLjaACABX2NfvjFS6XVAnpaDyGesz9gK2SC7MbNwdCHusObKJ9Q== + dependencies: + postcss-value-parser "^4.2.0" + +"@csstools/postcss-oklab-function@^4.0.6": + version "4.0.6" + resolved "https://registry.yarnpkg.com/@csstools/postcss-oklab-function/-/postcss-oklab-function-4.0.6.tgz#17e8dfb6422dfd8d77256def5d5be8335ea7af34" + integrity sha512-Hptoa0uX+XsNacFBCIQKTUBrFKDiplHan42X73EklG6XmQLG7/aIvxoNhvZ7PvOWMt67Pw3bIlUY2nD6p5vL8A== + dependencies: + "@csstools/css-color-parser" "^3.0.6" + "@csstools/css-parser-algorithms" "^3.0.4" + "@csstools/css-tokenizer" "^3.0.3" + "@csstools/postcss-progressive-custom-properties" "^4.0.0" + "@csstools/utilities" "^2.0.0" + +"@csstools/postcss-progressive-custom-properties@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-progressive-custom-properties/-/postcss-progressive-custom-properties-4.0.0.tgz#ecdb85bcdb1852d73970a214a376684a91f82bdc" + integrity sha512-XQPtROaQjomnvLUSy/bALTR5VCtTVUFwYs1SblvYgLSeTo2a/bMNwUwo2piXw5rTv/FEYiy5yPSXBqg9OKUx7Q== + dependencies: + postcss-value-parser "^4.2.0" + +"@csstools/postcss-random-function@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@csstools/postcss-random-function/-/postcss-random-function-1.0.1.tgz#73a0b62b5dbbc03c25a28f085235eb61b09a2fb0" + integrity sha512-Ab/tF8/RXktQlFwVhiC70UNfpFQRhtE5fQQoP2pO+KCPGLsLdWFiOuHgSRtBOqEshCVAzR4H6o38nhvRZq8deA== + dependencies: + "@csstools/css-calc" "^2.1.0" + "@csstools/css-parser-algorithms" "^3.0.4" + "@csstools/css-tokenizer" "^3.0.3" + +"@csstools/postcss-relative-color-syntax@^3.0.6": + version "3.0.6" + resolved "https://registry.yarnpkg.com/@csstools/postcss-relative-color-syntax/-/postcss-relative-color-syntax-3.0.6.tgz#4b8bc219b34b16f5abdbbcf09ac13e65bff6ef16" + integrity sha512-yxP618Xb+ji1I624jILaYM62uEmZcmbdmFoZHoaThw896sq0vU39kqTTF+ZNic9XyPtPMvq0vyvbgmHaszq8xg== + dependencies: + "@csstools/css-color-parser" "^3.0.6" + "@csstools/css-parser-algorithms" "^3.0.4" + "@csstools/css-tokenizer" "^3.0.3" + "@csstools/postcss-progressive-custom-properties" "^4.0.0" + "@csstools/utilities" "^2.0.0" + +"@csstools/postcss-scope-pseudo-class@^4.0.1": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@csstools/postcss-scope-pseudo-class/-/postcss-scope-pseudo-class-4.0.1.tgz#9fe60e9d6d91d58fb5fc6c768a40f6e47e89a235" + integrity sha512-IMi9FwtH6LMNuLea1bjVMQAsUhFxJnyLSgOp/cpv5hrzWmrUYU5fm0EguNDIIOHUqzXode8F/1qkC/tEo/qN8Q== + dependencies: + postcss-selector-parser "^7.0.0" + +"@csstools/postcss-sign-functions@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-sign-functions/-/postcss-sign-functions-1.1.0.tgz#a524fae1374b0e167729f612ca875d7b1b334262" + integrity sha512-SLcc20Nujx/kqbSwDmj6oaXgpy3UjFhBy1sfcqPgDkHfOIfUtUVH7OXO+j7BU4v/At5s61N5ZX6shvgPwluhsA== + dependencies: + "@csstools/css-calc" "^2.1.0" + "@csstools/css-parser-algorithms" "^3.0.4" + "@csstools/css-tokenizer" "^3.0.3" + +"@csstools/postcss-stepped-value-functions@^4.0.5": + version "4.0.5" + resolved "https://registry.yarnpkg.com/@csstools/postcss-stepped-value-functions/-/postcss-stepped-value-functions-4.0.5.tgz#4d68633d502fbe2b6ef3898e368e3540488a0d8a" + integrity sha512-G6SJ6hZJkhxo6UZojVlLo14MohH4J5J7z8CRBrxxUYy9JuZiIqUo5TBYyDGcE0PLdzpg63a7mHSJz3VD+gMwqw== + dependencies: + "@csstools/css-calc" "^2.1.0" + "@csstools/css-parser-algorithms" "^3.0.4" + "@csstools/css-tokenizer" "^3.0.3" + +"@csstools/postcss-text-decoration-shorthand@^4.0.1": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@csstools/postcss-text-decoration-shorthand/-/postcss-text-decoration-shorthand-4.0.1.tgz#251fab0939d50c6fd73bb2b830b2574188efa087" + integrity sha512-xPZIikbx6jyzWvhms27uugIc0I4ykH4keRvoa3rxX5K7lEhkbd54rjj/dv60qOCTisoS+3bmwJTeyV1VNBrXaw== + dependencies: + "@csstools/color-helpers" "^5.0.1" + postcss-value-parser "^4.2.0" + +"@csstools/postcss-trigonometric-functions@^4.0.5": + version "4.0.5" + resolved "https://registry.yarnpkg.com/@csstools/postcss-trigonometric-functions/-/postcss-trigonometric-functions-4.0.5.tgz#267b95a8bd45536e0360596b6da660a9eb6aac83" + integrity sha512-/YQThYkt5MLvAmVu7zxjhceCYlKrYddK6LEmK5I4ojlS6BmO9u2yO4+xjXzu2+NPYmHSTtP4NFSamBCMmJ1NJA== + dependencies: + "@csstools/css-calc" "^2.1.0" + "@csstools/css-parser-algorithms" "^3.0.4" + "@csstools/css-tokenizer" "^3.0.3" + +"@csstools/postcss-unset-value@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@csstools/postcss-unset-value/-/postcss-unset-value-4.0.0.tgz#7caa981a34196d06a737754864baf77d64de4bba" + integrity sha512-cBz3tOCI5Fw6NIFEwU3RiwK6mn3nKegjpJuzCndoGq3BZPkUjnsq7uQmIeMNeMbMk7YD2MfKcgCpZwX5jyXqCA== + +"@csstools/selector-resolve-nested@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@csstools/selector-resolve-nested/-/selector-resolve-nested-3.0.0.tgz#704a9b637975680e025e069a4c58b3beb3e2752a" + integrity sha512-ZoK24Yku6VJU1gS79a5PFmC8yn3wIapiKmPgun0hZgEI5AOqgH2kiPRsPz1qkGv4HL+wuDLH83yQyk6inMYrJQ== + +"@csstools/selector-specificity@^5.0.0": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@csstools/selector-specificity/-/selector-specificity-5.0.0.tgz#037817b574262134cabd68fc4ec1a454f168407b" + integrity sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw== + +"@csstools/utilities@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@csstools/utilities/-/utilities-2.0.0.tgz#f7ff0fee38c9ffb5646d47b6906e0bc8868bde60" + integrity sha512-5VdOr0Z71u+Yp3ozOx8T11N703wIFGVRgOWbOZMKgglPJsWA54MRIoMNVMa7shUToIhx5J8vX4sOZgD2XiihiQ== + "@discoveryjs/json-ext@0.5.7": version "0.5.7" resolved "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz" @@ -1228,10 +1550,10 @@ "@docsearch/css" "3.6.1" algoliasearch "^4.19.1" -"@docusaurus/babel@3.6.0": - version "3.6.0" - resolved "https://registry.yarnpkg.com/@docusaurus/babel/-/babel-3.6.0.tgz#735a003207925bd782dd08ffa5d8b3503c1f8d72" - integrity sha512-7CsoQFiadoq7AHSUIQNkI/lGfg9AQ2ZBzsf9BqfZGXkHwWDy6twuohEaG0PgQv1npSRSAB2dioVxhRSErnqKNA== +"@docusaurus/babel@3.6.2": + version "3.6.2" + resolved "https://registry.yarnpkg.com/@docusaurus/babel/-/babel-3.6.2.tgz#c63dd2d9d7861189fe51950b3b6550477057bcee" + integrity sha512-v8N8TWGXDsb5sxQC3Rcqb1CZr0LlU1OgqqVBUchN6cpIUr7EJuVJs5eHcIu5Ag8mwO/hWN3f7FE9uaHTMapAbg== dependencies: "@babel/core" "^7.25.9" "@babel/generator" "^7.25.9" @@ -1243,24 +1565,23 @@ "@babel/runtime" "^7.25.9" "@babel/runtime-corejs3" "^7.25.9" "@babel/traverse" "^7.25.9" - "@docusaurus/logger" "3.6.0" - "@docusaurus/utils" "3.6.0" + "@docusaurus/logger" "3.6.2" + "@docusaurus/utils" "3.6.2" babel-plugin-dynamic-import-node "^2.3.3" fs-extra "^11.1.1" tslib "^2.6.0" -"@docusaurus/bundler@3.6.0": - version "3.6.0" - resolved "https://registry.yarnpkg.com/@docusaurus/bundler/-/bundler-3.6.0.tgz#bdd060ba4d009211348e4e973a3bf4861cf0996b" - integrity sha512-o5T9HXkPKH0OQAifTxEXaebcO8kaz3tU1+wlIShZ2DKJHlsyWX3N4rToWBHroWnV/ZCT2XN3kLRzXASqrnb9Tw== +"@docusaurus/bundler@3.6.2": + version "3.6.2" + resolved "https://registry.yarnpkg.com/@docusaurus/bundler/-/bundler-3.6.2.tgz#5bdd46862b40f1eea93f14714b858d07c2dd8c2f" + integrity sha512-YkEifEVs4lV931SrHBB4n6WqRowMw+aM/QPH3z8aU+5t1dWa+1p2OPqARS+tSbh3la9ns+L1zIfSbd8RHi2/PQ== dependencies: "@babel/core" "^7.25.9" - "@docusaurus/babel" "3.6.0" - "@docusaurus/cssnano-preset" "3.6.0" - "@docusaurus/logger" "3.6.0" - "@docusaurus/types" "3.6.0" - "@docusaurus/utils" "3.6.0" - autoprefixer "^10.4.14" + "@docusaurus/babel" "3.6.2" + "@docusaurus/cssnano-preset" "3.6.2" + "@docusaurus/logger" "3.6.2" + "@docusaurus/types" "3.6.2" + "@docusaurus/utils" "3.6.2" babel-loader "^9.2.1" clean-css "^5.3.2" copy-webpack-plugin "^11.0.0" @@ -1273,6 +1594,7 @@ null-loader "^4.0.1" postcss "^8.4.26" postcss-loader "^7.3.3" + postcss-preset-env "^10.1.0" react-dev-utils "^12.0.1" terser-webpack-plugin "^5.3.9" tslib "^2.6.0" @@ -1280,18 +1602,18 @@ webpack "^5.95.0" webpackbar "^6.0.1" -"@docusaurus/core@3.6.0", "@docusaurus/core@^3.6.0": - version "3.6.0" - resolved "https://registry.yarnpkg.com/@docusaurus/core/-/core-3.6.0.tgz#b23fc7e253a49cc3e5ac9e091354f497cc0b101b" - integrity sha512-lvRgMoKJJSRDt9+HhAqFcICV4kp/mw1cJJrLxIw4Q2XZnFGM1XUuwcbuaqWmGog+NcOLZaPCcCtZbn60EMCtjQ== - dependencies: - "@docusaurus/babel" "3.6.0" - "@docusaurus/bundler" "3.6.0" - "@docusaurus/logger" "3.6.0" - "@docusaurus/mdx-loader" "3.6.0" - "@docusaurus/utils" "3.6.0" - "@docusaurus/utils-common" "3.6.0" - "@docusaurus/utils-validation" "3.6.0" +"@docusaurus/core@3.6.2", "@docusaurus/core@^3.6.2": + version "3.6.2" + resolved "https://registry.yarnpkg.com/@docusaurus/core/-/core-3.6.2.tgz#78628790f255555bb4c81e5952d16ea1412c4548" + integrity sha512-irMts/mGLZv8dWcy0WUtbY/U6b5qIfHgQd1/kXMyAxUJo99fL0wFSqhMI+tcxjk0HYy427MXerLMqFJj+Arg1w== + dependencies: + "@docusaurus/babel" "3.6.2" + "@docusaurus/bundler" "3.6.2" + "@docusaurus/logger" "3.6.2" + "@docusaurus/mdx-loader" "3.6.2" + "@docusaurus/utils" "3.6.2" + "@docusaurus/utils-common" "3.6.2" + "@docusaurus/utils-validation" "3.6.2" boxen "^6.2.1" chalk "^4.1.2" chokidar "^3.5.3" @@ -1329,32 +1651,32 @@ webpack-dev-server "^4.15.2" webpack-merge "^6.0.1" -"@docusaurus/cssnano-preset@3.6.0": - version "3.6.0" - resolved "https://registry.yarnpkg.com/@docusaurus/cssnano-preset/-/cssnano-preset-3.6.0.tgz#02378e53e9568ed5fc8871d4fc158ea96fd7421c" - integrity sha512-h3jlOXqqzNSoU+C4CZLNpFtD+v2xr1UBf4idZpwMgqid9r6lb5GS7tWKnQnauio6OipacbHbDXEX3JyT1PlDkg== +"@docusaurus/cssnano-preset@3.6.2": + version "3.6.2" + resolved "https://registry.yarnpkg.com/@docusaurus/cssnano-preset/-/cssnano-preset-3.6.2.tgz#007e7150b099ea2e9e874dd48a809614c628a335" + integrity sha512-mBkVa4QMHRwCFCVLYdBlOZuAT1iVVsS7GGSgliSVAeTOagP/AbtlBsCVrBs+keEuDuRF1w/6QEcqDoZe9fa5pw== dependencies: cssnano-preset-advanced "^6.1.2" postcss "^8.4.38" postcss-sort-media-queries "^5.2.0" tslib "^2.6.0" -"@docusaurus/logger@3.6.0": - version "3.6.0" - resolved "https://registry.yarnpkg.com/@docusaurus/logger/-/logger-3.6.0.tgz#c7349c2636087f55f573a60a3c7f69b87d59974d" - integrity sha512-BcQhoXilXW0607cH/kO6P5Gt5KxCGfoJ+QDKNf3yO2S09/RsITlW+0QljXPbI3DklTrHrhRDmgGk1yX4nUhWTA== +"@docusaurus/logger@3.6.2": + version "3.6.2" + resolved "https://registry.yarnpkg.com/@docusaurus/logger/-/logger-3.6.2.tgz#4f73f82b33e1d432f3940fc208b3c0646ca5549c" + integrity sha512-1p4IQhhgLyIfsey4UAdAIW69aUE1Ei6O91Nsw30ryZeDWSG5dh4o3zaRGOLxfAX69Ac/yDm6YCwJOafUxL6Vxg== dependencies: chalk "^4.1.2" tslib "^2.6.0" -"@docusaurus/mdx-loader@3.6.0": - version "3.6.0" - resolved "https://registry.yarnpkg.com/@docusaurus/mdx-loader/-/mdx-loader-3.6.0.tgz#f8ba7af9d59473a7182f6a9307e0432f8dce905b" - integrity sha512-GhRzL1Af/AdSSrGesSPOU/iP/aXadTGmVKuysCxZDrQR2RtBtubQZ9aw+KvdFVV7R4K/CsbgD6J5oqrXlEPk3Q== +"@docusaurus/mdx-loader@3.6.2": + version "3.6.2" + resolved "https://registry.yarnpkg.com/@docusaurus/mdx-loader/-/mdx-loader-3.6.2.tgz#d240974b0e754d5a5d8eb3f9d0a00a2055fabc68" + integrity sha512-7fbRmNgF3CR96Ja82Ya0/Cdu1OL9UJ/22llNMY8lr5gAbw718Y5ryXMVRIYn0JNLTiSxzgtvW4DIsUWEB8NMpw== dependencies: - "@docusaurus/logger" "3.6.0" - "@docusaurus/utils" "3.6.0" - "@docusaurus/utils-validation" "3.6.0" + "@docusaurus/logger" "3.6.2" + "@docusaurus/utils" "3.6.2" + "@docusaurus/utils-validation" "3.6.2" "@mdx-js/mdx" "^3.0.0" "@slorber/remark-comment" "^1.0.0" escape-html "^1.0.3" @@ -1377,12 +1699,12 @@ vfile "^6.0.1" webpack "^5.88.1" -"@docusaurus/module-type-aliases@3.6.0", "@docusaurus/module-type-aliases@^3.5.1": - version "3.6.0" - resolved "https://registry.yarnpkg.com/@docusaurus/module-type-aliases/-/module-type-aliases-3.6.0.tgz#44083c34a53db1dde06364b4e7f2d144fa2d5394" - integrity sha512-szTrIN/6/fuk0xkf3XbRfdTFJzRQ8d1s3sQj5++58wltrT7v3yn1149oc9ryYjMpRcbsarGloQwMu7ofPe4XPg== +"@docusaurus/module-type-aliases@3.6.2", "@docusaurus/module-type-aliases@^3.5.1": + version "3.6.2" + resolved "https://registry.yarnpkg.com/@docusaurus/module-type-aliases/-/module-type-aliases-3.6.2.tgz#7432618696668acc9a7cfb47de66c6987cd57680" + integrity sha512-NrJkL2rLTCjHtWOqUvWzwqvJrsKLj0gVJeV6q5yeKdKKgItietcTf2fTRkM9LHKSUN8CBDXxwHABeQvTahvmXQ== dependencies: - "@docusaurus/types" "3.6.0" + "@docusaurus/types" "3.6.2" "@types/history" "^4.7.11" "@types/react" "*" "@types/react-router-config" "*" @@ -1390,19 +1712,19 @@ react-helmet-async "*" react-loadable "npm:@docusaurus/react-loadable@6.0.0" -"@docusaurus/plugin-content-blog@3.6.0": - version "3.6.0" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-blog/-/plugin-content-blog-3.6.0.tgz#9128175b4c3ce885d9090183d74c60813844ea8d" - integrity sha512-o4aT1/E0Ldpzs/hQff5uyoSriAhS/yqBhqSn+fvSw465AaqRsva6O7CZSYleuBq6x2bewyE3QJq2PcTiHhAd8g== - dependencies: - "@docusaurus/core" "3.6.0" - "@docusaurus/logger" "3.6.0" - "@docusaurus/mdx-loader" "3.6.0" - "@docusaurus/theme-common" "3.6.0" - "@docusaurus/types" "3.6.0" - "@docusaurus/utils" "3.6.0" - "@docusaurus/utils-common" "3.6.0" - "@docusaurus/utils-validation" "3.6.0" +"@docusaurus/plugin-content-blog@3.6.2": + version "3.6.2" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-blog/-/plugin-content-blog-3.6.2.tgz#b197dd920e380bf1394995215ba7fee8019baa82" + integrity sha512-6bJxr6Or4NslEVH3BJuPH30kUWiqUjDRdGPhvxpHmt9W/RY2/6u72WICG3bW3dLFxJ/2uDLBU92lHnatpvo7Ew== + dependencies: + "@docusaurus/core" "3.6.2" + "@docusaurus/logger" "3.6.2" + "@docusaurus/mdx-loader" "3.6.2" + "@docusaurus/theme-common" "3.6.2" + "@docusaurus/types" "3.6.2" + "@docusaurus/utils" "3.6.2" + "@docusaurus/utils-common" "3.6.2" + "@docusaurus/utils-validation" "3.6.2" cheerio "1.0.0-rc.12" feed "^4.2.2" fs-extra "^11.1.1" @@ -1414,20 +1736,20 @@ utility-types "^3.10.0" webpack "^5.88.1" -"@docusaurus/plugin-content-docs@3.6.0": - version "3.6.0" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.6.0.tgz#15cae4bf81da0b0ddce09d53b10b7209116ea9c2" - integrity sha512-c5gZOxocJKO/Zev2MEZInli+b+VNswDGuKHE6QtFgidhAJonwjh2kwj967RvWFaMMk62HlLJLZ+IGK2XsVy4Aw== - dependencies: - "@docusaurus/core" "3.6.0" - "@docusaurus/logger" "3.6.0" - "@docusaurus/mdx-loader" "3.6.0" - "@docusaurus/module-type-aliases" "3.6.0" - "@docusaurus/theme-common" "3.6.0" - "@docusaurus/types" "3.6.0" - "@docusaurus/utils" "3.6.0" - "@docusaurus/utils-common" "3.6.0" - "@docusaurus/utils-validation" "3.6.0" +"@docusaurus/plugin-content-docs@3.6.2": + version "3.6.2" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.6.2.tgz#3a8b4b162a2688e5855c04ed6c4ec0b6951619a0" + integrity sha512-e6WW1g10RIXXLN/rrtqTi/FyJ1Hj3X9Mmgz4V11/0pDCxIGGI8m4ocbAglUlLtgvbLD5viNLefl/NwbOW3JXiQ== + dependencies: + "@docusaurus/core" "3.6.2" + "@docusaurus/logger" "3.6.2" + "@docusaurus/mdx-loader" "3.6.2" + "@docusaurus/module-type-aliases" "3.6.2" + "@docusaurus/theme-common" "3.6.2" + "@docusaurus/types" "3.6.2" + "@docusaurus/utils" "3.6.2" + "@docusaurus/utils-common" "3.6.2" + "@docusaurus/utils-validation" "3.6.2" "@types/react-router-config" "^5.0.7" combine-promises "^1.1.0" fs-extra "^11.1.1" @@ -1437,115 +1759,115 @@ utility-types "^3.10.0" webpack "^5.88.1" -"@docusaurus/plugin-content-pages@3.6.0", "@docusaurus/plugin-content-pages@^3.6.0": - version "3.6.0" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-pages/-/plugin-content-pages-3.6.0.tgz#5dd284bf063baaba1e0305c90b1dd0d5acc7e466" - integrity sha512-RKHhJrfkadHc7+tt1cP48NWifOrhkSRMPdXNYytzhoQrXlP6Ph+3tfQ4/n+nT0S3Y9+wwRxYqRqA380ZLt+QtQ== - dependencies: - "@docusaurus/core" "3.6.0" - "@docusaurus/mdx-loader" "3.6.0" - "@docusaurus/types" "3.6.0" - "@docusaurus/utils" "3.6.0" - "@docusaurus/utils-validation" "3.6.0" +"@docusaurus/plugin-content-pages@3.6.2", "@docusaurus/plugin-content-pages@^3.6.2": + version "3.6.2" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-content-pages/-/plugin-content-pages-3.6.2.tgz#49b1a033d41841f7a8bcbbe67511609b402cc80f" + integrity sha512-fo4NyGkw10lYHyHaTxE6TZLYnxNtCfRHeZkNK1N9pBYqe7TT2dBUNAEeVW2U3ed9m6YuB7JKSQsa++GGmcP+6g== + dependencies: + "@docusaurus/core" "3.6.2" + "@docusaurus/mdx-loader" "3.6.2" + "@docusaurus/types" "3.6.2" + "@docusaurus/utils" "3.6.2" + "@docusaurus/utils-validation" "3.6.2" fs-extra "^11.1.1" tslib "^2.6.0" webpack "^5.88.1" -"@docusaurus/plugin-debug@3.6.0": - version "3.6.0" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-debug/-/plugin-debug-3.6.0.tgz#0a6da9ba31a0acb176ae2762b4d6b96b1906c826" - integrity sha512-o8T1Rl94COLdSlKvjYLQpRJQRU8WWZ8EX1B0yV0dQLNN8reyH7MQW+6z1ig4sQFfH3pnjPWVGHfuEjcib5m7Eg== +"@docusaurus/plugin-debug@3.6.2": + version "3.6.2" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-debug/-/plugin-debug-3.6.2.tgz#6983f64954fe69a51b65b2d9431bdf0b5ccf1884" + integrity sha512-T/eS3VvHElpeV5S8uwp7Si4ujEynmgFtJLvA2CSa5pzQuOF1EEghF9nekAIj0cWtDHsqNUDZNr8hK1brivFXSg== dependencies: - "@docusaurus/core" "3.6.0" - "@docusaurus/types" "3.6.0" - "@docusaurus/utils" "3.6.0" + "@docusaurus/core" "3.6.2" + "@docusaurus/types" "3.6.2" + "@docusaurus/utils" "3.6.2" fs-extra "^11.1.1" react-json-view-lite "^1.2.0" tslib "^2.6.0" -"@docusaurus/plugin-google-analytics@3.6.0": - version "3.6.0" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-3.6.0.tgz#9e8245eef1bee95e44ef2af92ce3e844a8e93e64" - integrity sha512-kgRFbfpi6Hshj75YUztKyEMtI/kw0trPRwoTN4g+W1NK99R/vh8phTvhBTIMnDbetU79795LkwfG0rZ/ce6zWQ== +"@docusaurus/plugin-google-analytics@3.6.2": + version "3.6.2" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-3.6.2.tgz#4266b4b2273998e87fa733d932d5b464c2a10b21" + integrity sha512-B7ihrr3wz8e4XqW+dIAtq844u3Z83u5CeiL1xrCqzFH+vDCjUZHTamS3zKXNcgi6YVVe6hUQXPG15ltaqQaVPQ== dependencies: - "@docusaurus/core" "3.6.0" - "@docusaurus/types" "3.6.0" - "@docusaurus/utils-validation" "3.6.0" + "@docusaurus/core" "3.6.2" + "@docusaurus/types" "3.6.2" + "@docusaurus/utils-validation" "3.6.2" tslib "^2.6.0" -"@docusaurus/plugin-google-gtag@3.6.0": - version "3.6.0" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-3.6.0.tgz#bed8381fe3ab357d56a565f657e38d8ea6272703" - integrity sha512-nqu4IfjaO4UX+dojHL2BxHRS+sKj31CIMWYo49huQ3wTET0Oc3u/WGTaKd3ShTPDhkgiRhTOSTPUwJWrU55nHg== +"@docusaurus/plugin-google-gtag@3.6.2": + version "3.6.2" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-3.6.2.tgz#23f70f7a05e61cfb9d9d7ee18dbff3ef2b129f2c" + integrity sha512-V8ijI6qddAAkJ0vd8sjZ7S/apRTLJn9dAwvj/rSMd93witGdKINemL+9TyfLkhcXKTxyqRT8zKdu8ewjPXqKHg== dependencies: - "@docusaurus/core" "3.6.0" - "@docusaurus/types" "3.6.0" - "@docusaurus/utils-validation" "3.6.0" + "@docusaurus/core" "3.6.2" + "@docusaurus/types" "3.6.2" + "@docusaurus/utils-validation" "3.6.2" "@types/gtag.js" "^0.0.12" tslib "^2.6.0" -"@docusaurus/plugin-google-tag-manager@3.6.0": - version "3.6.0" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-tag-manager/-/plugin-google-tag-manager-3.6.0.tgz#326382de05888ea4317837be736eabd635adbc71" - integrity sha512-OU6c5xI0nOVbEc9eImGvvsgNWe4vGm97t/W3aLHjWsHyNk3uwFNBQMHRvBUwAi9k/K3kyC5E7DWnc67REhdLOw== +"@docusaurus/plugin-google-tag-manager@3.6.2": + version "3.6.2" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-google-tag-manager/-/plugin-google-tag-manager-3.6.2.tgz#36ab95fcd5c1bf96fd18c0cf9b208bb428b81242" + integrity sha512-fnWQ5FdN9f8c8VTgjaQ98208Y+d/JjHhD506rWIIL9rt1cJOf29XElxvOeKpMJadfkgY5KLZSAiHkGt+4qgN4g== dependencies: - "@docusaurus/core" "3.6.0" - "@docusaurus/types" "3.6.0" - "@docusaurus/utils-validation" "3.6.0" + "@docusaurus/core" "3.6.2" + "@docusaurus/types" "3.6.2" + "@docusaurus/utils-validation" "3.6.2" tslib "^2.6.0" -"@docusaurus/plugin-sitemap@3.6.0": - version "3.6.0" - resolved "https://registry.yarnpkg.com/@docusaurus/plugin-sitemap/-/plugin-sitemap-3.6.0.tgz#c7c93f75f03391ca9071da48563fc4faa84966bc" - integrity sha512-YB5XMdf9FjLhgbHY/cDbYhVxsgcpPIjxY9769HUgFOB7GVzItTLOR71W035R1BiR2CA5QAn3XOSg36WLRxlhQQ== - dependencies: - "@docusaurus/core" "3.6.0" - "@docusaurus/logger" "3.6.0" - "@docusaurus/types" "3.6.0" - "@docusaurus/utils" "3.6.0" - "@docusaurus/utils-common" "3.6.0" - "@docusaurus/utils-validation" "3.6.0" +"@docusaurus/plugin-sitemap@3.6.2": + version "3.6.2" + resolved "https://registry.yarnpkg.com/@docusaurus/plugin-sitemap/-/plugin-sitemap-3.6.2.tgz#c8ff7cf82bd5d943a13bb8d0ae690080a025029e" + integrity sha512-qcAQAP1Ot0dZpeRoJ0L/Zck5FVDkll2IleVZQLzxeRVDZIw1P9/TK7/Aw1w2pmH7dmw/Cwk/cLSVRvLAmp9k7A== + dependencies: + "@docusaurus/core" "3.6.2" + "@docusaurus/logger" "3.6.2" + "@docusaurus/types" "3.6.2" + "@docusaurus/utils" "3.6.2" + "@docusaurus/utils-common" "3.6.2" + "@docusaurus/utils-validation" "3.6.2" fs-extra "^11.1.1" sitemap "^7.1.1" tslib "^2.6.0" -"@docusaurus/preset-classic@^3.6.0": - version "3.6.0" - resolved "https://registry.yarnpkg.com/@docusaurus/preset-classic/-/preset-classic-3.6.0.tgz#71561f366a266be571022764eb8b9e5618f573eb" - integrity sha512-kpGNdQzr/Dpm7o3b1iaQrz4DMDx3WIeBbl4V4P4maa2zAQkTdlaP4CMgA5oKrRrpqPLnQFsUM/b+qf2glhl2Tw== - dependencies: - "@docusaurus/core" "3.6.0" - "@docusaurus/plugin-content-blog" "3.6.0" - "@docusaurus/plugin-content-docs" "3.6.0" - "@docusaurus/plugin-content-pages" "3.6.0" - "@docusaurus/plugin-debug" "3.6.0" - "@docusaurus/plugin-google-analytics" "3.6.0" - "@docusaurus/plugin-google-gtag" "3.6.0" - "@docusaurus/plugin-google-tag-manager" "3.6.0" - "@docusaurus/plugin-sitemap" "3.6.0" - "@docusaurus/theme-classic" "3.6.0" - "@docusaurus/theme-common" "3.6.0" - "@docusaurus/theme-search-algolia" "3.6.0" - "@docusaurus/types" "3.6.0" - -"@docusaurus/theme-classic@3.6.0": - version "3.6.0" - resolved "https://registry.yarnpkg.com/@docusaurus/theme-classic/-/theme-classic-3.6.0.tgz#8f34b65c85f5082deb3633a893974d2eee309121" - integrity sha512-sAXNfwPL6uRD+BuHuKXZfAXud7SS7IK/JdrPuzyQxdO1gJKzI5GFfe1ED1QoJDNWJWJ01JHE5rSnwYLEADc2rQ== - dependencies: - "@docusaurus/core" "3.6.0" - "@docusaurus/logger" "3.6.0" - "@docusaurus/mdx-loader" "3.6.0" - "@docusaurus/module-type-aliases" "3.6.0" - "@docusaurus/plugin-content-blog" "3.6.0" - "@docusaurus/plugin-content-docs" "3.6.0" - "@docusaurus/plugin-content-pages" "3.6.0" - "@docusaurus/theme-common" "3.6.0" - "@docusaurus/theme-translations" "3.6.0" - "@docusaurus/types" "3.6.0" - "@docusaurus/utils" "3.6.0" - "@docusaurus/utils-common" "3.6.0" - "@docusaurus/utils-validation" "3.6.0" +"@docusaurus/preset-classic@^3.6.2": + version "3.6.2" + resolved "https://registry.yarnpkg.com/@docusaurus/preset-classic/-/preset-classic-3.6.2.tgz#5ec801fa317123ba8458af3105eca8eac78a49bc" + integrity sha512-r2n5eHdhiNSrJGsrrYcw+WsyStmXxe0ZG3RdA9LVyK5+jBHM8blrUWJEDugnzCNbyhUzhdtcmgCC9fhdAvKuQw== + dependencies: + "@docusaurus/core" "3.6.2" + "@docusaurus/plugin-content-blog" "3.6.2" + "@docusaurus/plugin-content-docs" "3.6.2" + "@docusaurus/plugin-content-pages" "3.6.2" + "@docusaurus/plugin-debug" "3.6.2" + "@docusaurus/plugin-google-analytics" "3.6.2" + "@docusaurus/plugin-google-gtag" "3.6.2" + "@docusaurus/plugin-google-tag-manager" "3.6.2" + "@docusaurus/plugin-sitemap" "3.6.2" + "@docusaurus/theme-classic" "3.6.2" + "@docusaurus/theme-common" "3.6.2" + "@docusaurus/theme-search-algolia" "3.6.2" + "@docusaurus/types" "3.6.2" + +"@docusaurus/theme-classic@3.6.2": + version "3.6.2" + resolved "https://registry.yarnpkg.com/@docusaurus/theme-classic/-/theme-classic-3.6.2.tgz#4c2770d3609176dd2462dfb0cb4d0b3d3010404b" + integrity sha512-bCdOPqPNezhLx+hgNVO2Cf+8/1AHa9uHDOqTx/CKAx2I0J/jV9G+6JiMtpSRKGNfBoLT1O+56/7+WtkOf54xTw== + dependencies: + "@docusaurus/core" "3.6.2" + "@docusaurus/logger" "3.6.2" + "@docusaurus/mdx-loader" "3.6.2" + "@docusaurus/module-type-aliases" "3.6.2" + "@docusaurus/plugin-content-blog" "3.6.2" + "@docusaurus/plugin-content-docs" "3.6.2" + "@docusaurus/plugin-content-pages" "3.6.2" + "@docusaurus/theme-common" "3.6.2" + "@docusaurus/theme-translations" "3.6.2" + "@docusaurus/types" "3.6.2" + "@docusaurus/utils" "3.6.2" + "@docusaurus/utils-common" "3.6.2" + "@docusaurus/utils-validation" "3.6.2" "@mdx-js/react" "^3.0.0" clsx "^2.0.0" copy-text-to-clipboard "^3.2.0" @@ -1560,15 +1882,15 @@ tslib "^2.6.0" utility-types "^3.10.0" -"@docusaurus/theme-common@3.6.0": - version "3.6.0" - resolved "https://registry.yarnpkg.com/@docusaurus/theme-common/-/theme-common-3.6.0.tgz#9a061d278df76da0f70a9465cd0b7299c14d03d3" - integrity sha512-frjlYE5sRs+GuPs4XXlp9aMLI2O4H5FPpznDAXBrCm+8EpWRiIb443ePMxM3IyMCQ5bwFlki0PI9C+r4apstnw== +"@docusaurus/theme-common@3.6.2": + version "3.6.2" + resolved "https://registry.yarnpkg.com/@docusaurus/theme-common/-/theme-common-3.6.2.tgz#a520d9053b6ea0fa913d42898d35f73ed5ca3b9b" + integrity sha512-lfgsL064KEHpCkgGUc0OYoUPCpYfzggp6Hof8sz59UuKiLvb/Z7raewE9/NfocrJ2HZI17rLgMX3SQlRDh/5gg== dependencies: - "@docusaurus/mdx-loader" "3.6.0" - "@docusaurus/module-type-aliases" "3.6.0" - "@docusaurus/utils" "3.6.0" - "@docusaurus/utils-common" "3.6.0" + "@docusaurus/mdx-loader" "3.6.2" + "@docusaurus/module-type-aliases" "3.6.2" + "@docusaurus/utils" "3.6.2" + "@docusaurus/utils-common" "3.6.2" "@types/history" "^4.7.11" "@types/react" "*" "@types/react-router-config" "*" @@ -1578,32 +1900,32 @@ tslib "^2.6.0" utility-types "^3.10.0" -"@docusaurus/theme-mermaid@^3.6.0": - version "3.6.0" - resolved "https://registry.yarnpkg.com/@docusaurus/theme-mermaid/-/theme-mermaid-3.6.0.tgz#0a79b76950aee8e2856a3e39f1c1050eb237c1c9" - integrity sha512-5t7zzBnnJa4BBcGo9bEfTM48DxD/+CVbFkfiRnFXheWjMrMm5a+IP10igEQ4zyDC+QgatbzLAxkj4GRYpYTauA== - dependencies: - "@docusaurus/core" "3.6.0" - "@docusaurus/module-type-aliases" "3.6.0" - "@docusaurus/theme-common" "3.6.0" - "@docusaurus/types" "3.6.0" - "@docusaurus/utils-validation" "3.6.0" +"@docusaurus/theme-mermaid@^3.6.2": + version "3.6.2" + resolved "https://registry.yarnpkg.com/@docusaurus/theme-mermaid/-/theme-mermaid-3.6.2.tgz#f7ad64ae5b9510ddbf77bc4264ada059c8e815b1" + integrity sha512-Ui+rBtqMPKj3RCOxNlY04i1tEjNg+fZg4URTvkHmYR07hcKaJw+vkw+wlaYjd0HFZk+3Er9vUAcwsCWuea4cVQ== + dependencies: + "@docusaurus/core" "3.6.2" + "@docusaurus/module-type-aliases" "3.6.2" + "@docusaurus/theme-common" "3.6.2" + "@docusaurus/types" "3.6.2" + "@docusaurus/utils-validation" "3.6.2" mermaid ">=10.4" tslib "^2.6.0" -"@docusaurus/theme-search-algolia@3.6.0": - version "3.6.0" - resolved "https://registry.yarnpkg.com/@docusaurus/theme-search-algolia/-/theme-search-algolia-3.6.0.tgz#47dcfca68f50163abce411dd9b181855a9ec9c83" - integrity sha512-4IwRUkxjrisR8LXBHeE4d2btraWdMficbgiVL3UHvJURmyvgzMBZQP8KrK8rjdXeu8SuRxSmeV6NSVomRvdbEg== +"@docusaurus/theme-search-algolia@3.6.2": + version "3.6.2" + resolved "https://registry.yarnpkg.com/@docusaurus/theme-search-algolia/-/theme-search-algolia-3.6.2.tgz#b03b7d35a385004d089d000be764abdfb3fa5721" + integrity sha512-SFLS+Rq8Cg2yepnHucA9sRpIR97yHvZWlCgMzBLunV3KHbB6hD2h5HPhFV39wYHYCjJUAOH1lX9poJ1qKYuSvg== dependencies: "@docsearch/react" "^3.5.2" - "@docusaurus/core" "3.6.0" - "@docusaurus/logger" "3.6.0" - "@docusaurus/plugin-content-docs" "3.6.0" - "@docusaurus/theme-common" "3.6.0" - "@docusaurus/theme-translations" "3.6.0" - "@docusaurus/utils" "3.6.0" - "@docusaurus/utils-validation" "3.6.0" + "@docusaurus/core" "3.6.2" + "@docusaurus/logger" "3.6.2" + "@docusaurus/plugin-content-docs" "3.6.2" + "@docusaurus/theme-common" "3.6.2" + "@docusaurus/theme-translations" "3.6.2" + "@docusaurus/utils" "3.6.2" + "@docusaurus/utils-validation" "3.6.2" algoliasearch "^4.18.0" algoliasearch-helper "^3.13.3" clsx "^2.0.0" @@ -1613,23 +1935,23 @@ tslib "^2.6.0" utility-types "^3.10.0" -"@docusaurus/theme-translations@3.6.0": - version "3.6.0" - resolved "https://registry.yarnpkg.com/@docusaurus/theme-translations/-/theme-translations-3.6.0.tgz#93994e931f340c1712c81ac80dbab5750c24634f" - integrity sha512-L555X8lWE3fv8VaF0Bc1VnAgi10UvRKFcvADHiYR7Gj37ItaWP5i7xLHsSw7fi/SHTXe5wfIeCFNqUYHyCOHAQ== +"@docusaurus/theme-translations@3.6.2": + version "3.6.2" + resolved "https://registry.yarnpkg.com/@docusaurus/theme-translations/-/theme-translations-3.6.2.tgz#ff6d2588aa9bf9fb1e07465def067529d5668665" + integrity sha512-LIWrYoDUsOTKmb0c7IQzawiPUTAaczBs5IOx6srxOWoTHVUMLzJCkl5Y6whfuRrnul8G05qv2vk238bN5Ko62g== dependencies: fs-extra "^11.1.1" tslib "^2.6.0" -"@docusaurus/tsconfig@^3.6.0": - version "3.6.0" - resolved "https://registry.yarnpkg.com/@docusaurus/tsconfig/-/tsconfig-3.6.0.tgz#4be9d0469e5f3683cd6e2d33bc8e963d9878f751" - integrity sha512-1nHsSMlNgEifnvsL4ql9wx7I1xXhrrNZl65IKD11pdo/749oI9fMcvm47dDwgS57x1WEteIAxJjzidffa5J9TQ== +"@docusaurus/tsconfig@^3.6.2": + version "3.6.2" + resolved "https://registry.yarnpkg.com/@docusaurus/tsconfig/-/tsconfig-3.6.2.tgz#a6d3fec19ae45a67da678b54c019b0e3839b6711" + integrity sha512-TWLkyYHBYhIJNcXCEc3D1M9R8UFV4IZ82rGef5U9mE1ZrcgDUlZxYaYdoSuHrPrzPRIl3orjmpscO2FAk2gdZw== -"@docusaurus/types@3.6.0", "@docusaurus/types@^3.5.1": - version "3.6.0" - resolved "https://registry.yarnpkg.com/@docusaurus/types/-/types-3.6.0.tgz#8fa82332a7c7b8093b5c55e1115f5854ce484978" - integrity sha512-jADLgoZGWhAzThr+mRiyuFD4OUzt6jHnb7NRArRKorgxckqUBaPyFOau9hhbcSTHtU6ceyeWjN7FDt7uG2Hplw== +"@docusaurus/types@3.6.2", "@docusaurus/types@^3.5.1": + version "3.6.2" + resolved "https://registry.yarnpkg.com/@docusaurus/types/-/types-3.6.2.tgz#bd69c4c99b535b67f01276dc186622e0b1fc1305" + integrity sha512-117Wsk6xXrWEAsCYCXS3TGJv5tkdIZDcd7T/V0UJvKYmY0gyVPPcEQChy8yTdjbIkbB2q4fa7Jpox72Qv86mqQ== dependencies: "@mdx-js/mdx" "^3.0.0" "@types/history" "^4.7.11" @@ -1641,34 +1963,36 @@ webpack "^5.95.0" webpack-merge "^5.9.0" -"@docusaurus/utils-common@3.6.0": - version "3.6.0" - resolved "https://registry.yarnpkg.com/@docusaurus/utils-common/-/utils-common-3.6.0.tgz#11855ea503132bbcaba6ca4d351293ff10a75d34" - integrity sha512-diUDNfbw33GaZMmKwdTckT2IBfVouXLXRD+zphH9ywswuaEIKqixvuf5g41H7MBBrlMsxhna3uTMoB4B/OPDcA== +"@docusaurus/utils-common@3.6.2": + version "3.6.2" + resolved "https://registry.yarnpkg.com/@docusaurus/utils-common/-/utils-common-3.6.2.tgz#3367572d72090b7f17e721af7f020f8e39931662" + integrity sha512-dr5wK+OyU2QAWxG7S5siD2bPgS7+ZeqWHfgLNHZ5yalaZf8TbeNNLqydfngukAY56BGZN0NbMkX6jGIr7ZF0sA== dependencies: + "@docusaurus/types" "3.6.2" tslib "^2.6.0" -"@docusaurus/utils-validation@3.6.0": - version "3.6.0" - resolved "https://registry.yarnpkg.com/@docusaurus/utils-validation/-/utils-validation-3.6.0.tgz#5557ca14fa64ac29e6f70e61006be721395ecde5" - integrity sha512-CRHiKKJEKA0GFlfOf71JWHl7PtwOyX0+Zg9ep9NFEZv6Lcx3RJ9nhl7p8HRjPL6deyYceavM//BsfW4pCI4BtA== +"@docusaurus/utils-validation@3.6.2": + version "3.6.2" + resolved "https://registry.yarnpkg.com/@docusaurus/utils-validation/-/utils-validation-3.6.2.tgz#62b97a0d72694c85fa63928c494dd238a84c991f" + integrity sha512-Y3EwblDz72KOcobb5t2zlhHSmrfE8EaHusPJ96Kx2JYtNXL2omqCoOb6FpaXWhES75wvjUpkFLYfiNqAqEov8g== dependencies: - "@docusaurus/logger" "3.6.0" - "@docusaurus/utils" "3.6.0" - "@docusaurus/utils-common" "3.6.0" + "@docusaurus/logger" "3.6.2" + "@docusaurus/utils" "3.6.2" + "@docusaurus/utils-common" "3.6.2" fs-extra "^11.2.0" joi "^17.9.2" js-yaml "^4.1.0" lodash "^4.17.21" tslib "^2.6.0" -"@docusaurus/utils@3.6.0": - version "3.6.0" - resolved "https://registry.yarnpkg.com/@docusaurus/utils/-/utils-3.6.0.tgz#192785da6fd62dfd83d6f1879c3aa45547f5df23" - integrity sha512-VKczAutI4mptiAw/WcYEu5WeVhQ6Q1zdIUl64SGw9K++9lziH+Kt10Ee8l2dMpRkiUk6zzK20kMNlX2WCUwXYQ== +"@docusaurus/utils@3.6.2": + version "3.6.2" + resolved "https://registry.yarnpkg.com/@docusaurus/utils/-/utils-3.6.2.tgz#727299c2051eee04c1b431bc6ccd55fd4e5a0d52" + integrity sha512-oxnpUcFZGE3uPCDoXr8GJriB3VWM9sFjPedFidX3Fsz87l1NZNc1wtbKPfQ7GYFDMYo2IGlAv5+47Me9RkM6lg== dependencies: - "@docusaurus/logger" "3.6.0" - "@docusaurus/utils-common" "3.6.0" + "@docusaurus/logger" "3.6.2" + "@docusaurus/types" "3.6.2" + "@docusaurus/utils-common" "3.6.2" "@svgr/webpack" "^8.1.0" escape-string-regexp "^4.0.0" file-loader "^6.2.0" @@ -2937,7 +3261,7 @@ at-least-node@^1.0.0: resolved "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz" integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== -autoprefixer@^10.4.14, autoprefixer@^10.4.19: +autoprefixer@^10.4.19: version "10.4.20" resolved "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz" integrity sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g== @@ -3097,7 +3421,7 @@ browserslist@^4.0.0, browserslist@^4.18.1, browserslist@^4.22.2, browserslist@^4 node-releases "^2.0.18" update-browserslist-db "^1.1.0" -browserslist@^4.24.0, browserslist@^4.24.2: +browserslist@^4.23.1, browserslist@^4.24.0, browserslist@^4.24.2: version "4.24.2" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.24.2.tgz#f5845bc91069dbd55ee89faf9822e1d885d16580" integrity sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg== @@ -3622,11 +3946,27 @@ crypto-random-string@^4.0.0: dependencies: type-fest "^1.0.1" +css-blank-pseudo@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/css-blank-pseudo/-/css-blank-pseudo-7.0.1.tgz#32020bff20a209a53ad71b8675852b49e8d57e46" + integrity sha512-jf+twWGDf6LDoXDUode+nc7ZlrqfaNphrBIBrcmeP3D8yw1uPaix1gCC8LUQUGQ6CycuK2opkbFFWFuq/a94ag== + dependencies: + postcss-selector-parser "^7.0.0" + css-declaration-sorter@^7.2.0: version "7.2.0" resolved "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-7.2.0.tgz" integrity sha512-h70rUM+3PNFuaBDTLe8wF/cdWu+dOZmb7pJt8Z2sedYbAcQVQV/tEchueg3GWxwqS0cxtbxmaHEdkNACqcvsow== +css-has-pseudo@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/css-has-pseudo/-/css-has-pseudo-7.0.1.tgz#adbb51821e51f7a7c1d2df4d12827870cc311137" + integrity sha512-EOcoyJt+OsuKfCADgLT7gADZI5jMzIe/AeI6MeAYKiFBDmNmM7kk46DtSfMj5AohUJisqVzopBpnQTlvbyaBWg== + dependencies: + "@csstools/selector-specificity" "^5.0.0" + postcss-selector-parser "^7.0.0" + postcss-value-parser "^4.2.0" + css-in-js-utils@^3.1.0: version "3.1.0" resolved "https://registry.npmjs.org/css-in-js-utils/-/css-in-js-utils-3.1.0.tgz" @@ -3660,6 +4000,11 @@ css-minimizer-webpack-plugin@^5.0.1: schema-utils "^4.0.1" serialize-javascript "^6.0.1" +css-prefers-color-scheme@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/css-prefers-color-scheme/-/css-prefers-color-scheme-10.0.0.tgz#ba001b99b8105b8896ca26fc38309ddb2278bd3c" + integrity sha512-VCtXZAWivRglTZditUfB4StnsWr6YVZ2PRtuxQLKTNRdtAf8tpzaVPE9zXIF3VaSc7O70iK/j1+NXxyQCqdPjQ== + css-select@^4.1.3: version "4.3.0" resolved "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz" @@ -3711,6 +4056,11 @@ css-what@^6.0.1, css-what@^6.1.0: resolved "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz" integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== +cssdb@^8.2.1: + version "8.2.1" + resolved "https://registry.yarnpkg.com/cssdb/-/cssdb-8.2.1.tgz#62a5d9a41e2c86f1d7c35981098fc5ce47c5766c" + integrity sha512-KwEPys7lNsC8OjASI8RrmwOYYDcm0JOW9zQhcV83ejYcQkirTEyeAGui8aO2F5PiS6SLpxuTzl6qlMElIdsgIg== + cssesc@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz" @@ -7336,6 +7686,13 @@ points-on-path@^0.2.1: path-data-parser "0.1.0" points-on-curve "0.2.0" +postcss-attribute-case-insensitive@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-7.0.1.tgz#0c4500e3bcb2141848e89382c05b5a31c23033a3" + integrity sha512-Uai+SupNSqzlschRyNx3kbCTWgY/2hcwtHEI/ej2LJWc9JJ77qKgGptd8DHwY1mXtZ7Aoh4z4yxfwMBue9eNgw== + dependencies: + postcss-selector-parser "^7.0.0" + postcss-calc@^9.0.1: version "9.0.1" resolved "https://registry.npmjs.org/postcss-calc/-/postcss-calc-9.0.1.tgz" @@ -7344,6 +7701,40 @@ postcss-calc@^9.0.1: postcss-selector-parser "^6.0.11" postcss-value-parser "^4.2.0" +postcss-clamp@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/postcss-clamp/-/postcss-clamp-4.1.0.tgz#7263e95abadd8c2ba1bd911b0b5a5c9c93e02363" + integrity sha512-ry4b1Llo/9zz+PKC+030KUnPITTJAHeOwjfAyyB60eT0AorGLdzp52s31OsPRHRf8NchkgFoG2y6fCfn1IV1Ow== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-color-functional-notation@^7.0.6: + version "7.0.6" + resolved "https://registry.yarnpkg.com/postcss-color-functional-notation/-/postcss-color-functional-notation-7.0.6.tgz#d74c1e2294b72287eb9af079c04b7ddeff7ec5b3" + integrity sha512-wLXvm8RmLs14Z2nVpB4CWlnvaWPRcOZFltJSlcbYwSJ1EDZKsKDhPKIMecCnuU054KSmlmubkqczmm6qBPCBhA== + dependencies: + "@csstools/css-color-parser" "^3.0.6" + "@csstools/css-parser-algorithms" "^3.0.4" + "@csstools/css-tokenizer" "^3.0.3" + "@csstools/postcss-progressive-custom-properties" "^4.0.0" + "@csstools/utilities" "^2.0.0" + +postcss-color-hex-alpha@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/postcss-color-hex-alpha/-/postcss-color-hex-alpha-10.0.0.tgz#5dd3eba1f8facb4ea306cba6e3f7712e876b0c76" + integrity sha512-1kervM2cnlgPs2a8Vt/Qbe5cQ++N7rkYo/2rz2BkqJZIHQwaVuJgQH38REHrAi4uM0b1fqxMkWYmese94iMp3w== + dependencies: + "@csstools/utilities" "^2.0.0" + postcss-value-parser "^4.2.0" + +postcss-color-rebeccapurple@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-10.0.0.tgz#5ada28406ac47e0796dff4056b0a9d5a6ecead98" + integrity sha512-JFta737jSP+hdAIEhk1Vs0q0YF5P8fFcj+09pweS8ktuGuZ8pPlykHsk6mPxZ8awDl4TrcxUqJo9l1IhVr/OjQ== + dependencies: + "@csstools/utilities" "^2.0.0" + postcss-value-parser "^4.2.0" + postcss-colormin@^6.1.0: version "6.1.0" resolved "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-6.1.0.tgz" @@ -7362,6 +7753,44 @@ postcss-convert-values@^6.1.0: browserslist "^4.23.0" postcss-value-parser "^4.2.0" +postcss-custom-media@^11.0.5: + version "11.0.5" + resolved "https://registry.yarnpkg.com/postcss-custom-media/-/postcss-custom-media-11.0.5.tgz#2fcd88a9b1d4da41c67dac6f2def903063a3377d" + integrity sha512-SQHhayVNgDvSAdX9NQ/ygcDQGEY+aSF4b/96z7QUX6mqL5yl/JgG/DywcF6fW9XbnCRE+aVYk+9/nqGuzOPWeQ== + dependencies: + "@csstools/cascade-layer-name-parser" "^2.0.4" + "@csstools/css-parser-algorithms" "^3.0.4" + "@csstools/css-tokenizer" "^3.0.3" + "@csstools/media-query-list-parser" "^4.0.2" + +postcss-custom-properties@^14.0.4: + version "14.0.4" + resolved "https://registry.yarnpkg.com/postcss-custom-properties/-/postcss-custom-properties-14.0.4.tgz#de9c663285a98833a946d7003a34369d3ce373a9" + integrity sha512-QnW8FCCK6q+4ierwjnmXF9Y9KF8q0JkbgVfvQEMa93x1GT8FvOiUevWCN2YLaOWyByeDX8S6VFbZEeWoAoXs2A== + dependencies: + "@csstools/cascade-layer-name-parser" "^2.0.4" + "@csstools/css-parser-algorithms" "^3.0.4" + "@csstools/css-tokenizer" "^3.0.3" + "@csstools/utilities" "^2.0.0" + postcss-value-parser "^4.2.0" + +postcss-custom-selectors@^8.0.4: + version "8.0.4" + resolved "https://registry.yarnpkg.com/postcss-custom-selectors/-/postcss-custom-selectors-8.0.4.tgz#95ef8268fdbbbd84f34cf84a4517c9d99d419c5a" + integrity sha512-ASOXqNvDCE0dAJ/5qixxPeL1aOVGHGW2JwSy7HyjWNbnWTQCl+fDc968HY1jCmZI0+BaYT5CxsOiUhavpG/7eg== + dependencies: + "@csstools/cascade-layer-name-parser" "^2.0.4" + "@csstools/css-parser-algorithms" "^3.0.4" + "@csstools/css-tokenizer" "^3.0.3" + postcss-selector-parser "^7.0.0" + +postcss-dir-pseudo-class@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-9.0.1.tgz#80d9e842c9ae9d29f6bf5fd3cf9972891d6cc0ca" + integrity sha512-tRBEK0MHYvcMUrAuYMEOa0zg9APqirBcgzi6P21OhxtJyJADo/SWBwY1CAwEohQ/6HDaa9jCjLRG7K3PVQYHEA== + dependencies: + postcss-selector-parser "^7.0.0" + postcss-discard-comments@^6.0.2: version "6.0.2" resolved "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-6.0.2.tgz" @@ -7389,6 +7818,58 @@ postcss-discard-unused@^6.0.5: dependencies: postcss-selector-parser "^6.0.16" +postcss-double-position-gradients@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/postcss-double-position-gradients/-/postcss-double-position-gradients-6.0.0.tgz#eddd424ec754bb543d057d4d2180b1848095d4d2" + integrity sha512-JkIGah3RVbdSEIrcobqj4Gzq0h53GG4uqDPsho88SgY84WnpkTpI0k50MFK/sX7XqVisZ6OqUfFnoUO6m1WWdg== + dependencies: + "@csstools/postcss-progressive-custom-properties" "^4.0.0" + "@csstools/utilities" "^2.0.0" + postcss-value-parser "^4.2.0" + +postcss-focus-visible@^10.0.1: + version "10.0.1" + resolved "https://registry.yarnpkg.com/postcss-focus-visible/-/postcss-focus-visible-10.0.1.tgz#1f7904904368a2d1180b220595d77b6f8a957868" + integrity sha512-U58wyjS/I1GZgjRok33aE8juW9qQgQUNwTSdxQGuShHzwuYdcklnvK/+qOWX1Q9kr7ysbraQ6ht6r+udansalA== + dependencies: + postcss-selector-parser "^7.0.0" + +postcss-focus-within@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/postcss-focus-within/-/postcss-focus-within-9.0.1.tgz#ac01ce80d3f2e8b2b3eac4ff84f8e15cd0057bc7" + integrity sha512-fzNUyS1yOYa7mOjpci/bR+u+ESvdar6hk8XNK/TRR0fiGTp2QT5N+ducP0n3rfH/m9I7H/EQU6lsa2BrgxkEjw== + dependencies: + postcss-selector-parser "^7.0.0" + +postcss-font-variant@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/postcss-font-variant/-/postcss-font-variant-5.0.0.tgz#efd59b4b7ea8bb06127f2d031bfbb7f24d32fa66" + integrity sha512-1fmkBaCALD72CK2a9i468mA/+tr9/1cBxRRMXOUaZqO43oWPR5imcyPjXwuv7PXbCid4ndlP5zWhidQVVa3hmA== + +postcss-gap-properties@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/postcss-gap-properties/-/postcss-gap-properties-6.0.0.tgz#d5ff0bdf923c06686499ed2b12e125fe64054fed" + integrity sha512-Om0WPjEwiM9Ru+VhfEDPZJAKWUd0mV1HmNXqp2C29z80aQ2uP9UVhLc7e3aYMIor/S5cVhoPgYQ7RtfeZpYTRw== + +postcss-image-set-function@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/postcss-image-set-function/-/postcss-image-set-function-7.0.0.tgz#538e94e16716be47f9df0573b56bbaca86e1da53" + integrity sha512-QL7W7QNlZuzOwBTeXEmbVckNt1FSmhQtbMRvGGqqU4Nf4xk6KUEQhAoWuMzwbSv5jxiRiSZ5Tv7eiDB9U87znA== + dependencies: + "@csstools/utilities" "^2.0.0" + postcss-value-parser "^4.2.0" + +postcss-lab-function@^7.0.6: + version "7.0.6" + resolved "https://registry.yarnpkg.com/postcss-lab-function/-/postcss-lab-function-7.0.6.tgz#3121800fc7939ed1d9a1e87abeb33c407151252c" + integrity sha512-HPwvsoK7C949vBZ+eMyvH2cQeMr3UREoHvbtra76/UhDuiViZH6pir+z71UaJQohd7VDSVUdR6TkWYKExEc9aQ== + dependencies: + "@csstools/css-color-parser" "^3.0.6" + "@csstools/css-parser-algorithms" "^3.0.4" + "@csstools/css-tokenizer" "^3.0.3" + "@csstools/postcss-progressive-custom-properties" "^4.0.0" + "@csstools/utilities" "^2.0.0" + postcss-loader@^7.3.3: version "7.3.4" resolved "https://registry.npmjs.org/postcss-loader/-/postcss-loader-7.3.4.tgz" @@ -7398,6 +7879,13 @@ postcss-loader@^7.3.3: jiti "^1.20.0" semver "^7.5.4" +postcss-logical@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/postcss-logical/-/postcss-logical-8.0.0.tgz#0db0b90c2dc53b485a8074a4b7a906297544f58d" + integrity sha512-HpIdsdieClTjXLOyYdUPAX/XQASNIwdKt5hoZW08ZOAiI+tbV0ta1oclkpVkW5ANU+xJvk3KkA0FejkjGLXUkg== + dependencies: + postcss-value-parser "^4.2.0" + postcss-merge-idents@^6.0.3: version "6.0.3" resolved "https://registry.npmjs.org/postcss-merge-idents/-/postcss-merge-idents-6.0.3.tgz" @@ -7484,6 +7972,15 @@ postcss-modules-values@^4.0.0: dependencies: icss-utils "^5.0.0" +postcss-nesting@^13.0.1: + version "13.0.1" + resolved "https://registry.yarnpkg.com/postcss-nesting/-/postcss-nesting-13.0.1.tgz#c405796d7245a3e4c267a9956cacfe9670b5d43e" + integrity sha512-VbqqHkOBOt4Uu3G8Dm8n6lU5+9cJFxiuty9+4rcoyRPO9zZS1JIs6td49VIoix3qYqELHlJIn46Oih9SAKo+yQ== + dependencies: + "@csstools/selector-resolve-nested" "^3.0.0" + "@csstools/selector-specificity" "^5.0.0" + postcss-selector-parser "^7.0.0" + postcss-normalize-charset@^6.0.2: version "6.0.2" resolved "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-6.0.2.tgz" @@ -7546,6 +8043,11 @@ postcss-normalize-whitespace@^6.0.2: dependencies: postcss-value-parser "^4.2.0" +postcss-opacity-percentage@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/postcss-opacity-percentage/-/postcss-opacity-percentage-3.0.0.tgz#0b0db5ed5db5670e067044b8030b89c216e1eb0a" + integrity sha512-K6HGVzyxUxd/VgZdX04DCtdwWJ4NGLG212US4/LA1TLAbHgmAsTWVR86o+gGIbFtnTkfOpb9sCRBx8K7HO66qQ== + postcss-ordered-values@^6.0.2: version "6.0.2" resolved "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-6.0.2.tgz" @@ -7554,6 +8056,101 @@ postcss-ordered-values@^6.0.2: cssnano-utils "^4.0.2" postcss-value-parser "^4.2.0" +postcss-overflow-shorthand@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/postcss-overflow-shorthand/-/postcss-overflow-shorthand-6.0.0.tgz#f5252b4a2ee16c68cd8a9029edb5370c4a9808af" + integrity sha512-BdDl/AbVkDjoTofzDQnwDdm/Ym6oS9KgmO7Gr+LHYjNWJ6ExORe4+3pcLQsLA9gIROMkiGVjjwZNoL/mpXHd5Q== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-page-break@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/postcss-page-break/-/postcss-page-break-3.0.4.tgz#7fbf741c233621622b68d435babfb70dd8c1ee5f" + integrity sha512-1JGu8oCjVXLa9q9rFTo4MbeeA5FMe00/9C7lN4va606Rdb+HkxXtXsmEDrIraQ11fGz/WvKWa8gMuCKkrXpTsQ== + +postcss-place@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/postcss-place/-/postcss-place-10.0.0.tgz#ba36ee4786ca401377ced17a39d9050ed772e5a9" + integrity sha512-5EBrMzat2pPAxQNWYavwAfoKfYcTADJ8AXGVPcUZ2UkNloUTWzJQExgrzrDkh3EKzmAx1evfTAzF9I8NGcc+qw== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-preset-env@^10.1.0: + version "10.1.1" + resolved "https://registry.yarnpkg.com/postcss-preset-env/-/postcss-preset-env-10.1.1.tgz#6ee631272353fb1c4a9711943e9b80a178ffce44" + integrity sha512-wqqsnBFD6VIwcHHRbhjTOcOi4qRVlB26RwSr0ordPj7OubRRxdWebv/aLjKLRR8zkZrbxZyuus03nOIgC5elMQ== + dependencies: + "@csstools/postcss-cascade-layers" "^5.0.1" + "@csstools/postcss-color-function" "^4.0.6" + "@csstools/postcss-color-mix-function" "^3.0.6" + "@csstools/postcss-content-alt-text" "^2.0.4" + "@csstools/postcss-exponential-functions" "^2.0.5" + "@csstools/postcss-font-format-keywords" "^4.0.0" + "@csstools/postcss-gamut-mapping" "^2.0.6" + "@csstools/postcss-gradients-interpolation-method" "^5.0.6" + "@csstools/postcss-hwb-function" "^4.0.6" + "@csstools/postcss-ic-unit" "^4.0.0" + "@csstools/postcss-initial" "^2.0.0" + "@csstools/postcss-is-pseudo-class" "^5.0.1" + "@csstools/postcss-light-dark-function" "^2.0.7" + "@csstools/postcss-logical-float-and-clear" "^3.0.0" + "@csstools/postcss-logical-overflow" "^2.0.0" + "@csstools/postcss-logical-overscroll-behavior" "^2.0.0" + "@csstools/postcss-logical-resize" "^3.0.0" + "@csstools/postcss-logical-viewport-units" "^3.0.3" + "@csstools/postcss-media-minmax" "^2.0.5" + "@csstools/postcss-media-queries-aspect-ratio-number-values" "^3.0.4" + "@csstools/postcss-nested-calc" "^4.0.0" + "@csstools/postcss-normalize-display-values" "^4.0.0" + "@csstools/postcss-oklab-function" "^4.0.6" + "@csstools/postcss-progressive-custom-properties" "^4.0.0" + "@csstools/postcss-random-function" "^1.0.1" + "@csstools/postcss-relative-color-syntax" "^3.0.6" + "@csstools/postcss-scope-pseudo-class" "^4.0.1" + "@csstools/postcss-sign-functions" "^1.1.0" + "@csstools/postcss-stepped-value-functions" "^4.0.5" + "@csstools/postcss-text-decoration-shorthand" "^4.0.1" + "@csstools/postcss-trigonometric-functions" "^4.0.5" + "@csstools/postcss-unset-value" "^4.0.0" + autoprefixer "^10.4.19" + browserslist "^4.23.1" + css-blank-pseudo "^7.0.1" + css-has-pseudo "^7.0.1" + css-prefers-color-scheme "^10.0.0" + cssdb "^8.2.1" + postcss-attribute-case-insensitive "^7.0.1" + postcss-clamp "^4.1.0" + postcss-color-functional-notation "^7.0.6" + postcss-color-hex-alpha "^10.0.0" + postcss-color-rebeccapurple "^10.0.0" + postcss-custom-media "^11.0.5" + postcss-custom-properties "^14.0.4" + postcss-custom-selectors "^8.0.4" + postcss-dir-pseudo-class "^9.0.1" + postcss-double-position-gradients "^6.0.0" + postcss-focus-visible "^10.0.1" + postcss-focus-within "^9.0.1" + postcss-font-variant "^5.0.0" + postcss-gap-properties "^6.0.0" + postcss-image-set-function "^7.0.0" + postcss-lab-function "^7.0.6" + postcss-logical "^8.0.0" + postcss-nesting "^13.0.1" + postcss-opacity-percentage "^3.0.0" + postcss-overflow-shorthand "^6.0.0" + postcss-page-break "^3.0.4" + postcss-place "^10.0.0" + postcss-pseudo-class-any-link "^10.0.1" + postcss-replace-overflow-wrap "^4.0.0" + postcss-selector-not "^8.0.1" + +postcss-pseudo-class-any-link@^10.0.1: + version "10.0.1" + resolved "https://registry.yarnpkg.com/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-10.0.1.tgz#06455431171bf44b84d79ebaeee9fd1c05946544" + integrity sha512-3el9rXlBOqTFaMFkWDOkHUTQekFIYnaQY55Rsp8As8QQkpiSgIYEcF/6Ond93oHiDsGb4kad8zjt+NPlOC1H0Q== + dependencies: + postcss-selector-parser "^7.0.0" + postcss-reduce-idents@^6.0.3: version "6.0.3" resolved "https://registry.npmjs.org/postcss-reduce-idents/-/postcss-reduce-idents-6.0.3.tgz" @@ -7576,6 +8173,18 @@ postcss-reduce-transforms@^6.0.2: dependencies: postcss-value-parser "^4.2.0" +postcss-replace-overflow-wrap@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-4.0.0.tgz#d2df6bed10b477bf9c52fab28c568b4b29ca4319" + integrity sha512-KmF7SBPphT4gPPcKZc7aDkweHiKEEO8cla/GjcBK+ckKxiZslIu3C4GCRW3DNfL0o7yW7kMQu9xlZ1kXRXLXtw== + +postcss-selector-not@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/postcss-selector-not/-/postcss-selector-not-8.0.1.tgz#f2df9c6ac9f95e9fe4416ca41a957eda16130172" + integrity sha512-kmVy/5PYVb2UOhy0+LqUYAhKj7DUGDpSWa5LZqlkWJaaAV+dxxsOG3+St0yNLu6vsKD7Dmqx+nWQt0iil89+WA== + dependencies: + postcss-selector-parser "^7.0.0" + postcss-selector-parser@^6.0.11, postcss-selector-parser@^6.0.16, postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4: version "6.0.16" resolved "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.16.tgz" @@ -7584,6 +8193,14 @@ postcss-selector-parser@^6.0.11, postcss-selector-parser@^6.0.16, postcss-select cssesc "^3.0.0" util-deprecate "^1.0.2" +postcss-selector-parser@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-7.0.0.tgz#41bd8b56f177c093ca49435f65731befe25d6b9c" + integrity sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ== + dependencies: + cssesc "^3.0.0" + util-deprecate "^1.0.2" + postcss-sort-media-queries@^5.2.0: version "5.2.0" resolved "https://registry.npmjs.org/postcss-sort-media-queries/-/postcss-sort-media-queries-5.2.0.tgz" From cde7ce49be4945940135b76a3e8cf4058c809f62 Mon Sep 17 00:00:00 2001 From: Robert Brennan Date: Wed, 20 Nov 2024 10:42:02 -0500 Subject: [PATCH 29/56] fix up lockup when long actions are run (#5144) --- openhands/runtime/action_execution_server.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openhands/runtime/action_execution_server.py b/openhands/runtime/action_execution_server.py index aeb0d4c7a407..1251aa346838 100644 --- a/openhands/runtime/action_execution_server.py +++ b/openhands/runtime/action_execution_server.py @@ -52,7 +52,7 @@ from openhands.runtime.utils.files import insert_lines, read_lines from openhands.runtime.utils.runtime_init import init_user_and_working_directory from openhands.runtime.utils.system import check_port_available -from openhands.utils.async_utils import wait_all +from openhands.utils.async_utils import call_sync_from_async, wait_all class ActionRequest(BaseModel): @@ -170,7 +170,8 @@ async def run_action(self, action) -> Observation: async def run( self, action: CmdRunAction ) -> CmdOutputObservation | ErrorObservation: - return self.bash_session.run(action) + obs = await call_sync_from_async(self.bash_session.run, action) + return obs async def run_ipython(self, action: IPythonRunCellAction) -> Observation: if 'jupyter' in self.plugins: From 5c836985241f2bce9fb3ad005fe83d4556e740d2 Mon Sep 17 00:00:00 2001 From: young010101 <93481273+young010101@users.noreply.github.com> Date: Thu, 21 Nov 2024 03:07:06 +0800 Subject: [PATCH 30/56] Docs/fix logging param name (#5146) --- openhands/core/config/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openhands/core/config/utils.py b/openhands/core/config/utils.py index 86794e8aac2f..f664dfcfd656 100644 --- a/openhands/core/config/utils.py +++ b/openhands/core/config/utils.py @@ -384,7 +384,7 @@ def load_app_config( """Load the configuration from the specified config file and environment variables. Args: - set_logger_levels: Whether to set the global variables for logging levels. + set_logging_levels: Whether to set the global variables for logging levels. config_file: Path to the config file. Defaults to 'config.toml' in the current directory. """ config = AppConfig() From 3a65b7b07d712a1ff9181b7a0830d4897fa78965 Mon Sep 17 00:00:00 2001 From: young010101 <93481273+young010101@users.noreply.github.com> Date: Thu, 21 Nov 2024 04:06:02 +0800 Subject: [PATCH 31/56] =?UTF-8?q?docs:=20add=20missing=20toml=5Ffile=20par?= =?UTF-8?q?ameter=20description=20in=20get=5Fllm=5Fconfig=5Fa=E2=80=A6=20(?= =?UTF-8?q?#5147)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- openhands/core/config/utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openhands/core/config/utils.py b/openhands/core/config/utils.py index f664dfcfd656..437754ef22ae 100644 --- a/openhands/core/config/utils.py +++ b/openhands/core/config/utils.py @@ -241,6 +241,7 @@ def get_llm_config_arg( Args: llm_config_arg: The group of llm settings to get from the config.toml file. + toml_file: Path to the configuration file to read from. Defaults to 'config.toml'. Returns: LLMConfig: The LLMConfig object with the settings from the config file. From 07b96cc8c9507f963c9e971fdf8cceecc960fd71 Mon Sep 17 00:00:00 2001 From: Graham Neubig Date: Wed, 20 Nov 2024 15:19:51 -0500 Subject: [PATCH 32/56] docs: Add documentation on how to add new tools to codeact_agent (#5150) Co-authored-by: openhands --- openhands/agenthub/codeact_agent/README.md | 54 ++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/openhands/agenthub/codeact_agent/README.md b/openhands/agenthub/codeact_agent/README.md index 357a723b650c..45ccb42ba085 100644 --- a/openhands/agenthub/codeact_agent/README.md +++ b/openhands/agenthub/codeact_agent/README.md @@ -10,3 +10,57 @@ The conceptual idea is illustrated below. At each turn, the agent can: - Execute any valid `Python` code with [an interactive Python interpreter](https://ipython.org/). This is simulated through `bash` command, see plugin system below for more details. ![image](https://github.com/All-Hands-AI/OpenHands/assets/38853559/92b622e3-72ad-4a61-8f41-8c040b6d5fb3) + +## Adding New Tools + +The CodeAct agent uses a function calling interface to define tools that the agent can use. Tools are defined in `function_calling.py` using the `ChatCompletionToolParam` class from `litellm`. Each tool consists of: + +1. A description string that explains what the tool does and how to use it +2. A tool definition using `ChatCompletionToolParam` that specifies: + - The tool's name + - The tool's parameters and their types + - Required vs optional parameters + +Here's an example of how a tool is defined: + +```python +MyTool = ChatCompletionToolParam( + type='function', + function=ChatCompletionToolParamFunctionChunk( + name='my_tool', + description='Description of what the tool does and how to use it', + parameters={ + 'type': 'object', + 'properties': { + 'param1': { + 'type': 'string', + 'description': 'Description of parameter 1', + }, + 'param2': { + 'type': 'integer', + 'description': 'Description of parameter 2', + }, + }, + 'required': ['param1'], # List required parameters here + }, + ), +) +``` + +To add a new tool: + +1. Define your tool in `function_calling.py` following the pattern above +2. Add your tool to the `get_tools()` function in `function_calling.py` +3. Implement the corresponding action handler in the agent to process the tool's invocation + +The agent currently supports several built-in tools: +- `execute_bash`: Execute bash commands +- `execute_ipython_cell`: Run Python code in IPython +- `browser`: Interact with a web browser +- `str_replace_editor`: Edit files using string replacement +- `edit_file`: Edit files using LLM-based editing + +Tools can be enabled/disabled through configuration parameters: +- `codeact_enable_browsing`: Enable browser interaction +- `codeact_enable_jupyter`: Enable IPython code execution +- `codeact_enable_llm_editor`: Enable LLM-based file editing (if disabled, uses string replacement editor instead) From e211152f93bbbaefdb317ad5f11c2f01bbbf9a5e Mon Sep 17 00:00:00 2001 From: OpenHands Date: Wed, 20 Nov 2024 21:36:47 -0500 Subject: [PATCH 33/56] Fix issue #5159: [Bug]: lint-fix workflow terminates prematurely due to exit code 1 (#5160) --- .github/workflows/lint-fix.yml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/lint-fix.yml b/.github/workflows/lint-fix.yml index b877de6bb4d2..9fa97eaaf2f1 100644 --- a/.github/workflows/lint-fix.yml +++ b/.github/workflows/lint-fix.yml @@ -44,11 +44,8 @@ jobs: run: pip install pre-commit==3.7.0 - name: Fix python lint issues run: | - pre-commit run trailing-whitespace --files openhands/**/* evaluation/**/* tests/**/* --config ./dev_config/python/.pre-commit-config.yaml - pre-commit run end-of-file-fixer --files openhands/**/* evaluation/**/* tests/**/* --config ./dev_config/python/.pre-commit-config.yaml - pre-commit run pyproject-fmt --files openhands/**/* evaluation/**/* tests/**/* --config ./dev_config/python/.pre-commit-config.yaml - pre-commit run ruff --files openhands/**/* evaluation/**/* tests/**/* --config ./dev_config/python/.pre-commit-config.yaml - pre-commit run ruff-format --files openhands/**/* evaluation/**/* tests/**/* --config ./dev_config/python/.pre-commit-config.yaml + # Run all pre-commit hooks and continue even if they modify files (exit code 1) + pre-commit run --config ./dev_config/python/.pre-commit-config.yaml --files openhands/**/* evaluation/**/* tests/**/* || true # Commit and push changes if any - name: Check for changes From 27f136b8021b0fdd5a377d6f886c9c436748414c Mon Sep 17 00:00:00 2001 From: Robert Brennan Date: Wed, 20 Nov 2024 22:40:30 -0500 Subject: [PATCH 34/56] mitigate memory leak (#5152) --- openhands/server/session/session.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openhands/server/session/session.py b/openhands/server/session/session.py index c65ae73a30ab..c64e17020aa4 100644 --- a/openhands/server/session/session.py +++ b/openhands/server/session/session.py @@ -57,6 +57,9 @@ def close(self): self.websocket = None finally: self.agent_session.close() + del ( + self.agent_session + ) # FIXME: this should not be necessary but it mitigates a memory leak async def loop_recv(self): try: From 746722e1b518890e831bcd08d4b15bc51f11b075 Mon Sep 17 00:00:00 2001 From: young010101 <93481273+young010101@users.noreply.github.com> Date: Thu, 21 Nov 2024 11:41:51 +0800 Subject: [PATCH 35/56] style: remove extra newline in LLM wrapper function (#5149) --- openhands/llm/llm.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openhands/llm/llm.py b/openhands/llm/llm.py index 0590945995c1..6faf2c6605f4 100644 --- a/openhands/llm/llm.py +++ b/openhands/llm/llm.py @@ -164,7 +164,6 @@ def __init__( ) def wrapper(*args, **kwargs): """Wrapper for the litellm completion function. Logs the input and output of the completion function.""" - from openhands.core.utils import json messages: list[dict[str, Any]] | dict[str, Any] = [] From 94a8f58ece37fc8a5af4190e0841334b9efadbe5 Mon Sep 17 00:00:00 2001 From: Robert Brennan Date: Wed, 20 Nov 2024 22:42:13 -0500 Subject: [PATCH 36/56] fix up logging in listen.py (#5145) --- openhands/server/listen.py | 64 ++++++++++++++++++++++++++++++++------ 1 file changed, 55 insertions(+), 9 deletions(-) diff --git a/openhands/server/listen.py b/openhands/server/listen.py index f5463a517306..c8b258b46ead 100644 --- a/openhands/server/listen.py +++ b/openhands/server/listen.py @@ -62,7 +62,7 @@ from openhands.events.serialization import event_to_dict from openhands.events.stream import AsyncEventStreamWrapper from openhands.llm import bedrock -from openhands.runtime.base import Runtime +from openhands.runtime.base import Runtime, RuntimeUnavailableError from openhands.server.auth.auth import get_sid_from_token, sign_token from openhands.server.middleware import ( InMemoryRateLimiter, @@ -517,7 +517,14 @@ async def list_files(request: Request, path: str | None = None): ) runtime: Runtime = request.state.conversation.runtime - file_list = await call_sync_from_async(runtime.list_files, path) + try: + file_list = await call_sync_from_async(runtime.list_files, path) + except RuntimeUnavailableError as e: + logger.error(f'Error listing files: {e}', exc_info=True) + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content={'error': f'Error listing files: {e}'}, + ) if path: file_list = [os.path.join(path, f) for f in file_list] @@ -537,7 +544,14 @@ async def filter_for_gitignore(file_list, base_path): file_list = [entry for entry in file_list if not spec.match_file(entry)] return file_list - file_list = await filter_for_gitignore(file_list, '') + try: + file_list = await filter_for_gitignore(file_list, '') + except RuntimeUnavailableError as e: + logger.error(f'Error filtering files: {e}', exc_info=True) + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content={'error': f'Error filtering files: {e}'}, + ) return file_list @@ -566,7 +580,14 @@ async def select_file(file: str, request: Request): file = os.path.join(runtime.config.workspace_mount_path_in_sandbox, file) read_action = FileReadAction(file) - observation = await call_sync_from_async(runtime.run_action, read_action) + try: + observation = await call_sync_from_async(runtime.run_action, read_action) + except RuntimeUnavailableError as e: + logger.error(f'Error opening file {file}: {e}', exc_info=True) + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content={'error': f'Error opening file: {e}'}, + ) if isinstance(observation, FileReadObservation): content = observation.content @@ -662,9 +683,20 @@ async def upload_file(request: Request, files: list[UploadFile]): tmp_file.flush() runtime: Runtime = request.state.conversation.runtime - runtime.copy_to( - tmp_file_path, runtime.config.workspace_mount_path_in_sandbox - ) + try: + await call_sync_from_async( + runtime.copy_to, + tmp_file_path, + runtime.config.workspace_mount_path_in_sandbox, + ) + except RuntimeUnavailableError as e: + logger.error( + f'Error saving file {safe_filename}: {e}', exc_info=True + ) + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content={'error': f'Error saving file: {e}'}, + ) uploaded_files.append(safe_filename) response_content = { @@ -795,7 +827,14 @@ async def save_file(request: Request): runtime.config.workspace_mount_path_in_sandbox, file_path ) write_action = FileWriteAction(file_path, content) - observation = await call_sync_from_async(runtime.run_action, write_action) + try: + observation = await call_sync_from_async(runtime.run_action, write_action) + except RuntimeUnavailableError as e: + logger.error(f'Error saving file: {e}', exc_info=True) + return JSONResponse( + status_code=500, + content={'error': f'Error saving file: {e}'}, + ) if isinstance(observation, FileWriteObservation): return JSONResponse( @@ -846,7 +885,14 @@ async def zip_current_workspace(request: Request, background_tasks: BackgroundTa logger.debug('Zipping workspace') runtime: Runtime = request.state.conversation.runtime path = runtime.config.workspace_mount_path_in_sandbox - zip_file = await call_sync_from_async(runtime.copy_from, path) + try: + zip_file = await call_sync_from_async(runtime.copy_from, path) + except RuntimeUnavailableError as e: + logger.error(f'Error zipping workspace: {e}', exc_info=True) + return JSONResponse( + status_code=500, + content={'error': f'Error zipping workspace: {e}'}, + ) response = FileResponse( path=zip_file, filename='workspace.zip', From f4a2df859f6e1bfdfaccad202c1ff698a5f23e4d Mon Sep 17 00:00:00 2001 From: Rohit Malhotra Date: Wed, 20 Nov 2024 22:46:08 -0500 Subject: [PATCH 37/56] [Bug][Resolver] Enable caching for reusable workflow (#5165) --- .github/workflows/openhands-resolver.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/openhands-resolver.yml b/.github/workflows/openhands-resolver.yml index b517f4c46e27..a2b82232eaaf 100644 --- a/.github/workflows/openhands-resolver.yml +++ b/.github/workflows/openhands-resolver.yml @@ -80,11 +80,11 @@ jobs: github.event.label.name == 'fix-me-experimental' || ( (github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment') && - startsWith(github.event.comment.body, inputs.macro || '@openhands-agent-exp') + startsWith(github.event.comment.body, '@openhands-agent-exp') ) || ( github.event_name == 'pull_request_review' && - startsWith(github.event.review.body, inputs.macro || '@openhands-agent-exp') + startsWith(github.event.review.body, '@openhands-agent-exp') ) ) uses: actions/cache@v3 From ebce77ab5677ff209895bc09e636da415129a471 Mon Sep 17 00:00:00 2001 From: OpenHands Date: Wed, 20 Nov 2024 23:03:22 -0500 Subject: [PATCH 38/56] Fix issue #5155: [Resolver] Could we get a .md of tips for the .openhands_instructions file? (#5163) Co-authored-by: Graham Neubig --- docs/modules/usage/how-to/github-action.md | 50 ++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/docs/modules/usage/how-to/github-action.md b/docs/modules/usage/how-to/github-action.md index 3b55ab4952a7..4864736e3cac 100644 --- a/docs/modules/usage/how-to/github-action.md +++ b/docs/modules/usage/how-to/github-action.md @@ -43,3 +43,53 @@ To customize the default macro (`@openhands-agent`): 1. [Create a repository variable](https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/store-information-in-variables#creating-configuration-variables-for-a-repository) named `OPENHANDS_MACRO` 2. Assign the variable a custom value + +## Writing Effective .openhands_instructions Files + +The `.openhands_instructions` file is a file that you can put in the root directory of your repository to guide OpenHands in understanding and working with your repository effectively. Here are key tips for writing high-quality instructions: + +### Core Principles + +1. **Concise but Informative**: Provide a clear, focused overview of the repository that emphasizes the most common actions OpenHands will need to perform. + +2. **Repository Structure**: Explain the key directories and their purposes, especially highlighting where different types of code (e.g., frontend, backend) are located. + +3. **Development Workflows**: Document the essential commands for: + - Building and setting up the project + - Running tests + - Linting and code quality checks + - Any environment-specific requirements + +4. **Testing Guidelines**: Specify: + - Where tests are located + - How to run specific test suites + - Any testing conventions or requirements + +### Example Structure + +```markdown +# Repository Overview +[Brief description of the project] + +## General Setup +- Main build command +- Development environment setup +- Pre-commit checks + +## Backend +- Location and structure +- Testing instructions +- Environment requirements + +## Frontend +- Setup prerequisites +- Build and test commands +- Environment variables + +## Additional Guidelines +- Code style requirements +- Special considerations +- Common workflows +``` + +For a real-world example, refer to the [OpenHands repository's .openhands_instructions](https://github.com/All-Hands-AI/OpenHands/blob/main/.openhands_instructions). From 12ed523c014f1d54ec9b3b5057c524423f2d1a59 Mon Sep 17 00:00:00 2001 From: Graham Neubig Date: Wed, 20 Nov 2024 23:07:21 -0500 Subject: [PATCH 39/56] docs: Add note about organizational token policies (#5161) Co-authored-by: openhands --- openhands/resolver/README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openhands/resolver/README.md b/openhands/resolver/README.md index d91699fe0c20..2042280ae940 100644 --- a/openhands/resolver/README.md +++ b/openhands/resolver/README.md @@ -15,6 +15,8 @@ Follow these steps to use this workflow in your own repository: 1. [Create a personal access token](https://github.com/settings/tokens?type=beta) with read/write scope for "contents", "issues", "pull requests", and "workflows" + Note: If you're working with an organizational repository, you may need to configure the organization's personal access token policy first. See [Setting a personal access token policy for your organization](https://docs.github.com/en/organizations/managing-programmatic-access-to-your-organization/setting-a-personal-access-token-policy-for-your-organization) for details. + 2. Create an API key for the [Claude API](https://www.anthropic.com/api) (recommended) or another supported LLM service 3. Copy `examples/openhands-resolver.yml` to your repository's `.github/workflows/` directory @@ -83,11 +85,14 @@ pip install openhands-ai 3. Set up environment variables: ```bash + # GitHub credentials + export GITHUB_TOKEN="your-github-token" export GITHUB_USERNAME="your-github-username" # Optional, defaults to token owner # LLM configuration + export LLM_MODEL="anthropic/claude-3-5-sonnet-20241022" # Recommended export LLM_API_KEY="your-llm-api-key" export LLM_BASE_URL="your-api-url" # Optional, for API proxies From 7e382977325684ee82a33a559e0ce40f5f572d48 Mon Sep 17 00:00:00 2001 From: Cheng Yang <93481273+young010101@users.noreply.github.com> Date: Thu, 21 Nov 2024 19:39:32 +0800 Subject: [PATCH 40/56] fix: correct relative links in agenthub README.md (#5170) --- openhands/agenthub/README.md | 38 ++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/openhands/agenthub/README.md b/openhands/agenthub/README.md index 05de596f1ff4..4cb588bfec2a 100644 --- a/openhands/agenthub/README.md +++ b/openhands/agenthub/README.md @@ -7,10 +7,10 @@ Contributors from different backgrounds and interests can choose to contribute t ## Constructing an Agent -The abstraction for an agent can be found [here](../openhands/controller/agent.py). +The abstraction for an agent can be found [here](../controller/agent.py). Agents are run inside of a loop. At each iteration, `agent.step()` is called with a -[State](../openhands/controller/state/state.py) input, and the agent must output an [Action](../openhands/events/action). +[State](../controller/state/state.py) input, and the agent must output an [Action](../events/action). Every agent also has a `self.llm` which it can use to interact with the LLM configured by the user. See the [LiteLLM docs for `self.llm.completion`](https://docs.litellm.ai/docs/completion). @@ -46,17 +46,17 @@ The agent can add and modify subtasks through the `AddTaskAction` and `ModifyTas Here is a list of available Actions, which can be returned by `agent.step()`: -- [`CmdRunAction`](../openhands/events/action/commands.py) - Runs a command inside a sandboxed terminal -- [`IPythonRunCellAction`](../openhands/events/action/commands.py) - Execute a block of Python code interactively (in Jupyter notebook) and receives `CmdOutputObservation`. Requires setting up `jupyter` [plugin](../openhands/runtime/plugins) as a requirement. -- [`FileReadAction`](../openhands/events/action/files.py) - Reads the content of a file -- [`FileWriteAction`](../openhands/events/action/files.py) - Writes new content to a file -- [`BrowseURLAction`](../openhands/events/action/browse.py) - Gets the content of a URL -- [`AddTaskAction`](../openhands/events/action/tasks.py) - Adds a subtask to the plan -- [`ModifyTaskAction`](../openhands/events/action/tasks.py) - Changes the state of a subtask. -- [`AgentFinishAction`](../openhands/events/action/agent.py) - Stops the control loop, allowing the user/delegator agent to enter a new task -- [`AgentRejectAction`](../openhands/events/action/agent.py) - Stops the control loop, allowing the user/delegator agent to enter a new task -- [`AgentFinishAction`](../openhands/events/action/agent.py) - Stops the control loop, allowing the user to enter a new task -- [`MessageAction`](../openhands/events/action/message.py) - Represents a message from an agent or the user +- [`CmdRunAction`](../events/action/commands.py) - Runs a command inside a sandboxed terminal +- [`IPythonRunCellAction`](../events/action/commands.py) - Execute a block of Python code interactively (in Jupyter notebook) and receives `CmdOutputObservation`. Requires setting up `jupyter` [plugin](../runtime/plugins) as a requirement. +- [`FileReadAction`](../events/action/files.py) - Reads the content of a file +- [`FileWriteAction`](../events/action/files.py) - Writes new content to a file +- [`BrowseURLAction`](../events/action/browse.py) - Gets the content of a URL +- [`AddTaskAction`](../events/action/tasks.py) - Adds a subtask to the plan +- [`ModifyTaskAction`](../events/action/tasks.py) - Changes the state of a subtask. +- [`AgentFinishAction`](../events/action/agent.py) - Stops the control loop, allowing the user/delegator agent to enter a new task +- [`AgentRejectAction`](../events/action/agent.py) - Stops the control loop, allowing the user/delegator agent to enter a new task +- [`AgentFinishAction`](../events/action/agent.py) - Stops the control loop, allowing the user to enter a new task +- [`MessageAction`](../events/action/message.py) - Represents a message from an agent or the user To serialize and deserialize an action, you can use: - `action.to_dict()` to serialize the action to a dictionary to be sent to the UI, including a user-friendly string representation of the message @@ -70,12 +70,12 @@ But they may also appear as a result of asynchronous events (e.g. a message from Here is a list of available Observations: -- [`CmdOutputObservation`](../openhands/events/observation/commands.py) -- [`BrowserOutputObservation`](../openhands/events/observation/browse.py) -- [`FileReadObservation`](../openhands/events/observation/files.py) -- [`FileWriteObservation`](../openhands/events/observation/files.py) -- [`ErrorObservation`](../openhands/events/observation/error.py) -- [`SuccessObservation`](../openhands/events/observation/success.py) +- [`CmdOutputObservation`](../events/observation/commands.py) +- [`BrowserOutputObservation`](../events/observation/browse.py) +- [`FileReadObservation`](../events/observation/files.py) +- [`FileWriteObservation`](../events/observation/files.py) +- [`ErrorObservation`](../events/observation/error.py) +- [`SuccessObservation`](../events/observation/success.py) You can use `observation.to_dict()` and `observation_from_dict` to serialize and deserialize observations. From 68e52a9c62f4cc6d48d33c5f1179aa4c1008b5a8 Mon Sep 17 00:00:00 2001 From: Cheng Yang <93481273+young010101@users.noreply.github.com> Date: Thu, 21 Nov 2024 21:00:46 +0800 Subject: [PATCH 41/56] feat: add return type hints to LLM class methods (#5173) --- openhands/llm/llm.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/openhands/llm/llm.py b/openhands/llm/llm.py index 6faf2c6605f4..0f9f6376c79c 100644 --- a/openhands/llm/llm.py +++ b/openhands/llm/llm.py @@ -369,16 +369,16 @@ def init_model_info(self): ): self.config.max_output_tokens = self.model_info['max_tokens'] - def vision_is_active(self): + def vision_is_active(self) -> bool: with warnings.catch_warnings(): warnings.simplefilter('ignore') return not self.config.disable_vision and self._supports_vision() - def _supports_vision(self): + def _supports_vision(self) -> bool: """Acquire from litellm if model is vision capable. Returns: - bool: True if model is vision capable. If model is not supported by litellm, it will return False. + bool: True if model is vision capable. Return False if model not supported by litellm. """ # litellm.supports_vision currently returns False for 'openai/gpt-...' or 'anthropic/claude-...' (with prefixes) # but model_info will have the correct value for some reason. @@ -476,7 +476,7 @@ def _post_completion(self, response: ModelResponse) -> None: if stats: logger.debug(stats) - def get_token_count(self, messages): + def get_token_count(self, messages) -> int: """Get the number of tokens in a list of messages. Args: @@ -491,7 +491,7 @@ def get_token_count(self, messages): # TODO: this is to limit logspam in case token count is not supported return 0 - def _is_local(self): + def _is_local(self) -> bool: """Determines if the system is using a locally running LLM. Returns: @@ -506,7 +506,7 @@ def _is_local(self): return True return False - def _completion_cost(self, response): + def _completion_cost(self, response) -> float: """Calculate the cost of a completion response based on the model. Local models are treated as free. Add the current cost into total cost in metrics. @@ -555,7 +555,7 @@ def __str__(self): def __repr__(self): return str(self) - def reset(self): + def reset(self) -> None: self.metrics.reset() def format_messages_for_llm(self, messages: Message | list[Message]) -> list[dict]: From d08886f30e41121c3c36bd6b4aee2ca32e61aa33 Mon Sep 17 00:00:00 2001 From: Engel Nyst Date: Thu, 21 Nov 2024 19:18:49 +0100 Subject: [PATCH 42/56] Fix non-function calls messages (#5026) Co-authored-by: Xingyao Wang --- openhands/core/message.py | 48 +++++++++++++++++++----------- openhands/llm/fn_call_converter.py | 12 ++++---- openhands/llm/llm.py | 21 ++++++------- 3 files changed, 49 insertions(+), 32 deletions(-) diff --git a/openhands/core/message.py b/openhands/core/message.py index a707ea3881ea..a5b67917eaee 100644 --- a/openhands/core/message.py +++ b/openhands/core/message.py @@ -56,6 +56,7 @@ class Message(BaseModel): cache_enabled: bool = False vision_enabled: bool = False # function calling + function_calling_enabled: bool = False # - tool calls (from LLM) tool_calls: list[ChatCompletionMessageToolCall] | None = None # - tool execution result (to LLM) @@ -72,22 +73,22 @@ def serialize_model(self) -> dict: # - into a single string: for providers that don't support list of content items (e.g. no vision, no tool calls) # - into a list of content items: the new APIs of providers with vision/prompt caching/tool calls # NOTE: remove this when litellm or providers support the new API - if ( - self.cache_enabled - or self.vision_enabled - or self.tool_call_id is not None - or self.tool_calls is not None - ): + if self.cache_enabled or self.vision_enabled or self.function_calling_enabled: return self._list_serializer() + # some providers, like HF and Groq/llama, don't support a list here, but a single string return self._string_serializer() - def _string_serializer(self): + def _string_serializer(self) -> dict: + # convert content to a single string content = '\n'.join( item.text for item in self.content if isinstance(item, TextContent) ) - return {'content': content, 'role': self.role} + message_dict: dict = {'content': content, 'role': self.role} + + # add tool call keys if we have a tool call or response + return self._add_tool_call_keys(message_dict) - def _list_serializer(self): + def _list_serializer(self) -> dict: content: list[dict] = [] role_tool_with_prompt_caching = False for item in self.content: @@ -102,24 +103,37 @@ def _list_serializer(self): elif isinstance(item, ImageContent) and self.vision_enabled: content.extend(d) - ret: dict = {'content': content, 'role': self.role} + message_dict: dict = {'content': content, 'role': self.role} + # pop content if it's empty if not content or ( len(content) == 1 and content[0]['type'] == 'text' and content[0]['text'] == '' ): - ret.pop('content') + message_dict.pop('content') if role_tool_with_prompt_caching: - ret['cache_control'] = {'type': 'ephemeral'} + message_dict['cache_control'] = {'type': 'ephemeral'} + + # add tool call keys if we have a tool call or response + return self._add_tool_call_keys(message_dict) + def _add_tool_call_keys(self, message_dict: dict) -> dict: + """Add tool call keys if we have a tool call or response. + + NOTE: this is necessary for both native and non-native tool calling""" + + # an assistant message calling a tool + if self.tool_calls is not None: + message_dict['tool_calls'] = self.tool_calls + + # an observation message with tool response if self.tool_call_id is not None: assert ( self.name is not None ), 'name is required when tool_call_id is not None' - ret['tool_call_id'] = self.tool_call_id - ret['name'] = self.name - if self.tool_calls: - ret['tool_calls'] = self.tool_calls - return ret + message_dict['tool_call_id'] = self.tool_call_id + message_dict['name'] = self.name + + return message_dict diff --git a/openhands/llm/fn_call_converter.py b/openhands/llm/fn_call_converter.py index 5ddbcb305064..b33c7b43503b 100644 --- a/openhands/llm/fn_call_converter.py +++ b/openhands/llm/fn_call_converter.py @@ -320,9 +320,8 @@ def convert_fncall_messages_to_non_fncall_messages( converted_messages = [] first_user_message_encountered = False for message in messages: - role, content = message['role'], message['content'] - if content is None: - content = '' + role = message['role'] + content = message.get('content', '') # 1. SYSTEM MESSAGES # append system prompt suffix to content @@ -339,6 +338,7 @@ def convert_fncall_messages_to_non_fncall_messages( f'Unexpected content type {type(content)}. Expected str or list. Content: {content}' ) converted_messages.append({'role': 'system', 'content': content}) + # 2. USER MESSAGES (no change) elif role == 'user': # Add in-context learning example for the first user message @@ -447,10 +447,12 @@ def convert_fncall_messages_to_non_fncall_messages( f'Unexpected content type {type(content)}. Expected str or list. Content: {content}' ) converted_messages.append({'role': 'assistant', 'content': content}) + # 4. TOOL MESSAGES (tool outputs) elif role == 'tool': - # Convert tool result as assistant message - prefix = f'EXECUTION RESULT of [{message["name"]}]:\n' + # Convert tool result as user message + tool_name = message.get('name', 'function') + prefix = f'EXECUTION RESULT of [{tool_name}]:\n' # and omit "tool_call_id" AND "name" if isinstance(content, str): content = prefix + content diff --git a/openhands/llm/llm.py b/openhands/llm/llm.py index 0f9f6376c79c..2191818f8216 100644 --- a/openhands/llm/llm.py +++ b/openhands/llm/llm.py @@ -122,6 +122,9 @@ def __init__( drop_params=self.config.drop_params, ) + with warnings.catch_warnings(): + warnings.simplefilter('ignore') + self.init_model_info() if self.vision_is_active(): logger.debug('LLM: model has vision enabled') if self.is_caching_prompt_active(): @@ -143,16 +146,6 @@ def __init__( drop_params=self.config.drop_params, ) - with warnings.catch_warnings(): - warnings.simplefilter('ignore') - self.init_model_info() - if self.vision_is_active(): - logger.debug('LLM: model has vision enabled') - if self.is_caching_prompt_active(): - logger.debug('LLM: caching prompt enabled') - if self.is_function_calling_active(): - logger.debug('LLM: model supports function calling') - self._completion_unwrapped = self._completion @self.retry_decorator( @@ -342,6 +335,13 @@ def init_model_info(self): pass logger.debug(f'Model info: {self.model_info}') + if self.config.model.startswith('huggingface'): + # HF doesn't support the OpenAI default value for top_p (1) + logger.debug( + f'Setting top_p to 0.9 for Hugging Face model: {self.config.model}' + ) + self.config.top_p = 0.9 if self.config.top_p == 1 else self.config.top_p + # Set the max tokens in an LM-specific way if not set if self.config.max_input_tokens is None: if ( @@ -566,6 +566,7 @@ def format_messages_for_llm(self, messages: Message | list[Message]) -> list[dic for message in messages: message.cache_enabled = self.is_caching_prompt_active() message.vision_enabled = self.vision_is_active() + message.function_calling_enabled = self.is_function_calling_active() # let pydantic handle the serialization return [message.model_dump() for message in messages] From ea6809b283dec09e6716a3e33c46fecefe766767 Mon Sep 17 00:00:00 2001 From: diwu-sf Date: Thu, 21 Nov 2024 11:17:58 -0800 Subject: [PATCH 43/56] =?UTF-8?q?rename=20github=20to=20github=5Futils=20t?= =?UTF-8?q?o=20avoid=20import=20circular=20dependency=20pro=E2=80=A6=20(#5?= =?UTF-8?q?180)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- openhands/server/{github.py => github_utils.py} | 0 openhands/server/listen.py | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename openhands/server/{github.py => github_utils.py} (100%) diff --git a/openhands/server/github.py b/openhands/server/github_utils.py similarity index 100% rename from openhands/server/github.py rename to openhands/server/github_utils.py diff --git a/openhands/server/listen.py b/openhands/server/listen.py index c8b258b46ead..51d2696720a5 100644 --- a/openhands/server/listen.py +++ b/openhands/server/listen.py @@ -13,7 +13,7 @@ from openhands.security.options import SecurityAnalyzers from openhands.server.data_models.feedback import FeedbackDataModel, store_feedback -from openhands.server.github import ( +from openhands.server.github_utils import ( GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET, UserVerifier, From 39dad706ca43c4f3e8c8e90134e5c20f9f65d550 Mon Sep 17 00:00:00 2001 From: mamoodi Date: Thu, 21 Nov 2024 14:42:33 -0500 Subject: [PATCH 44/56] Release 0.14.2 (#5182) --- frontend/package-lock.json | 4 ++-- frontend/package.json | 2 +- pyproject.toml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index a5abe9cc6a58..2b718e2c146f 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "openhands-frontend", - "version": "0.14.1", + "version": "0.14.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "openhands-frontend", - "version": "0.14.1", + "version": "0.14.2", "dependencies": { "@monaco-editor/react": "^4.6.0", "@nextui-org/react": "^2.4.8", diff --git a/frontend/package.json b/frontend/package.json index 1757adbe8ac3..98a1408857de 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "openhands-frontend", - "version": "0.14.1", + "version": "0.14.2", "private": true, "type": "module", "engines": { diff --git a/pyproject.toml b/pyproject.toml index 53648ae7d8e8..fc1807f72fc3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "openhands-ai" -version = "0.14.1" +version = "0.14.2" description = "OpenHands: Code Less, Make More" authors = ["OpenHands"] license = "MIT" From 68d1e76ccda11eb8e900a532795d1f35afd8cabe Mon Sep 17 00:00:00 2001 From: niliy01 Date: Fri, 22 Nov 2024 08:55:26 +0800 Subject: [PATCH 45/56] fix: remove repeated completion assignment in llm.py (#5167) --- openhands/llm/llm.py | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/openhands/llm/llm.py b/openhands/llm/llm.py index 2191818f8216..58f41ca46244 100644 --- a/openhands/llm/llm.py +++ b/openhands/llm/llm.py @@ -108,20 +108,8 @@ def __init__( ) os.makedirs(self.config.log_completions_folder, exist_ok=True) - self._completion = partial( - litellm_completion, - model=self.config.model, - api_key=self.config.api_key, - base_url=self.config.base_url, - api_version=self.config.api_version, - custom_llm_provider=self.config.custom_llm_provider, - max_tokens=self.config.max_output_tokens, - timeout=self.config.timeout, - temperature=self.config.temperature, - top_p=self.config.top_p, - drop_params=self.config.drop_params, - ) - + # call init_model_info to initialize config.max_output_tokens + # which is used in partial function with warnings.catch_warnings(): warnings.simplefilter('ignore') self.init_model_info() From 83add629917de56f4f7a238df36977b6a88bbcfd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 22 Nov 2024 10:50:36 +0400 Subject: [PATCH 46/56] Bump the eslint group across 1 directory with 2 updates (#5200) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- frontend/package-lock.json | 113 ++++++------------------------------- frontend/package.json | 4 +- 2 files changed, 18 insertions(+), 99 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 2b718e2c146f..5f72fe6d0b96 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -69,9 +69,9 @@ "eslint-config-airbnb-typescript": "^18.0.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-import": "^2.29.1", - "eslint-plugin-jsx-a11y": "^6.9.0", + "eslint-plugin-jsx-a11y": "^6.10.2", "eslint-plugin-prettier": "^5.2.1", - "eslint-plugin-react": "^7.35.0", + "eslint-plugin-react": "^7.37.2", "eslint-plugin-react-hooks": "^4.6.2", "husky": "^9.1.6", "jsdom": "^25.0.1", @@ -8161,38 +8161,6 @@ "node": ">=6" } }, - "node_modules/deep-equal": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz", - "integrity": "sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==", - "dev": true, - "dependencies": { - "array-buffer-byte-length": "^1.0.0", - "call-bind": "^1.0.5", - "es-get-iterator": "^1.1.3", - "get-intrinsic": "^1.2.2", - "is-arguments": "^1.1.1", - "is-array-buffer": "^3.0.2", - "is-date-object": "^1.0.5", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "isarray": "^2.0.5", - "object-is": "^1.1.5", - "object-keys": "^1.1.1", - "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.5.1", - "side-channel": "^1.0.4", - "which-boxed-primitive": "^1.0.2", - "which-collection": "^1.0.1", - "which-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -8593,26 +8561,6 @@ "node": ">= 0.4" } }, - "node_modules/es-get-iterator": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", - "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.3", - "has-symbols": "^1.0.3", - "is-arguments": "^1.1.1", - "is-map": "^2.0.2", - "is-set": "^2.0.2", - "is-string": "^1.0.7", - "isarray": "^2.0.5", - "stop-iteration-iterator": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/es-iterator-helpers": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.1.0.tgz", @@ -9062,12 +9010,12 @@ } }, "node_modules/eslint-plugin-jsx-a11y": { - "version": "6.10.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.0.tgz", - "integrity": "sha512-ySOHvXX8eSN6zz8Bywacm7CvGNhUtdjvqfQDVe6020TUK34Cywkw7m0KsCCk1Qtm9G1FayfTN1/7mMYnYO2Bhg==", + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", + "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", "dev": true, "dependencies": { - "aria-query": "~5.1.3", + "aria-query": "^5.3.2", "array-includes": "^3.1.8", "array.prototype.flatmap": "^1.3.2", "ast-types-flow": "^0.0.8", @@ -9075,14 +9023,13 @@ "axobject-query": "^4.1.0", "damerau-levenshtein": "^1.0.8", "emoji-regex": "^9.2.2", - "es-iterator-helpers": "^1.0.19", "hasown": "^2.0.2", "jsx-ast-utils": "^3.3.5", "language-tags": "^1.0.9", "minimatch": "^3.1.2", "object.fromentries": "^2.0.8", "safe-regex-test": "^1.0.3", - "string.prototype.includes": "^2.0.0" + "string.prototype.includes": "^2.0.1" }, "engines": { "node": ">=4.0" @@ -9092,12 +9039,12 @@ } }, "node_modules/eslint-plugin-jsx-a11y/node_modules/aria-query": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", - "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", "dev": true, - "dependencies": { - "deep-equal": "^2.0.5" + "engines": { + "node": ">= 0.4" } }, "node_modules/eslint-plugin-jsx-a11y/node_modules/brace-expansion": { @@ -9153,9 +9100,9 @@ } }, "node_modules/eslint-plugin-react": { - "version": "7.37.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.1.tgz", - "integrity": "sha512-xwTnwDqzbDRA8uJ7BMxPs/EXRB3i8ZfnOIp8BsxEQkT0nHPp+WWceqGgo6rKb9ctNi8GJLDT4Go5HAWELa/WMg==", + "version": "7.37.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.2.tgz", + "integrity": "sha512-EsTAnj9fLVr/GZleBLFbj/sSuXeWmp1eXIN60ceYnZveqEaUCyW4X+Vh4WTdUhCkW4xutXYqTXCUSyqD4rB75w==", "dev": true, "dependencies": { "array-includes": "^3.1.8", @@ -9163,7 +9110,7 @@ "array.prototype.flatmap": "^1.3.2", "array.prototype.tosorted": "^1.1.4", "doctrine": "^2.1.0", - "es-iterator-helpers": "^1.0.19", + "es-iterator-helpers": "^1.1.0", "estraverse": "^5.3.0", "hasown": "^2.0.2", "jsx-ast-utils": "^2.4.1 || ^3.0.0", @@ -18934,22 +18881,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/object-is": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", - "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", @@ -22698,18 +22629,6 @@ "integrity": "sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==", "dev": true }, - "node_modules/stop-iteration-iterator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", - "integrity": "sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==", - "dev": true, - "dependencies": { - "internal-slot": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/stream-shift": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", diff --git a/frontend/package.json b/frontend/package.json index 98a1408857de..8e134ec5f0e1 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -95,9 +95,9 @@ "eslint-config-airbnb-typescript": "^18.0.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-import": "^2.29.1", - "eslint-plugin-jsx-a11y": "^6.9.0", + "eslint-plugin-jsx-a11y": "^6.10.2", "eslint-plugin-prettier": "^5.2.1", - "eslint-plugin-react": "^7.35.0", + "eslint-plugin-react": "^7.37.2", "eslint-plugin-react-hooks": "^4.6.2", "husky": "^9.1.6", "jsdom": "^25.0.1", From 135a62ca9cae5a66b679a48872fb0888e1a1e448 Mon Sep 17 00:00:00 2001 From: Rohit Malhotra Date: Fri, 22 Nov 2024 09:28:38 -0500 Subject: [PATCH 47/56] [Resolver]: Removing redundant checks (#5196) Co-authored-by: Graham Neubig --- .../resolver/examples/openhands-resolver.yml | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/openhands/resolver/examples/openhands-resolver.yml b/openhands/resolver/examples/openhands-resolver.yml index 2e2f42be0bac..13571b7703e1 100644 --- a/openhands/resolver/examples/openhands-resolver.yml +++ b/openhands/resolver/examples/openhands-resolver.yml @@ -19,21 +19,6 @@ permissions: jobs: call-openhands-resolver: - if: | - github.event.label.name == 'fix-me' || - - ( - ((github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment') && - (startsWith(github.event.comment.body, inputs.macro || '@openhands-agent') || startsWith(github.event.comment.body, inputs.macro || vars.OPENHANDS_MACRO)) && - (github.event.comment.author_association == 'OWNER' || github.event.comment.author_association == 'COLLABORATOR' || github.event.comment.author_association == 'MEMBER') - ) || - - (github.event_name == 'pull_request_review' && - (startsWith(github.event.review.body, inputs.macro || '@openhands-agent') || startsWith(github.event.review.body, inputs.macro || vars.OPENHANDS_MACRO)) && - (github.event.review.author_association == 'OWNER' || github.event.review.author_association == 'COLLABORATOR' || github.event.review.author_association == 'MEMBER') - ) - ) - uses: All-Hands-AI/OpenHands/.github/workflows/openhands-resolver.yml@main with: macro: ${{ vars.OPENHANDS_MACRO || '@openhands-agent' }} From 24d5facec5c9a0e0f9b37dc5259d7104cce32e22 Mon Sep 17 00:00:00 2001 From: Raymond Xu Date: Fri, 22 Nov 2024 08:43:45 -0800 Subject: [PATCH 48/56] Show the link to the All Hands product roadmap (#5192) Co-authored-by: Graham Neubig --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 97de3104472c..77633dbbbf72 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,8 @@ See more about the community in [COMMUNITY.md](./COMMUNITY.md) or find details o ## 📈 Progress +See the monthly OpenHands roadmap [here](https://github.com/orgs/All-Hands-AI/projects/1) (updated at the maintainer's meeting at the end of each month). +

Star History Chart From 36e3dc5c19c323c45de88aa18906dc560fc99d4b Mon Sep 17 00:00:00 2001 From: mamoodi Date: Fri, 22 Nov 2024 13:24:33 -0500 Subject: [PATCH 49/56] Add eval workflow that triggers remote eval job (#5108) --- .github/workflows/run-eval.yml | 53 ++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 .github/workflows/run-eval.yml diff --git a/.github/workflows/run-eval.yml b/.github/workflows/run-eval.yml new file mode 100644 index 000000000000..df79872aec26 --- /dev/null +++ b/.github/workflows/run-eval.yml @@ -0,0 +1,53 @@ +# Run evaluation on a PR +name: Run Eval + +# Runs when a PR is labeled with one of the "run-eval-" labels +on: + pull_request: + types: [labeled] + +jobs: + trigger-job: + name: Trigger remote eval job + if: ${{ github.event.label.name == 'run-eval-xs' || github.event.label.name == 'run-eval-s' || github.event.label.name == 'run-eval-m' }} + runs-on: ubuntu-latest + + steps: + - name: Checkout PR branch + uses: actions/checkout@v3 + with: + ref: ${{ github.head_ref }} + + - name: Trigger remote job + run: | + REPO_URL="https://github.com/${{ github.repository }}" + PR_BRANCH="${{ github.head_ref }}" + echo "Repository URL: $REPO_URL" + echo "PR Branch: $PR_BRANCH" + + if [[ "${{ github.event.label.name }}" == "run-eval-xs" ]]; then + EVAL_INSTANCES="1" + elif [[ "${{ github.event.label.name }}" == "run-eval-s" ]]; then + EVAL_INSTANCES="5" + elif [[ "${{ github.event.label.name }}" == "run-eval-m" ]]; then + EVAL_INSTANCES="30" + fi + + curl -X POST \ + -H "Authorization: Bearer ${{ secrets.PAT_TOKEN }}" \ + -H "Accept: application/vnd.github+json" \ + -d "{\"ref\": \"main\", \"inputs\": {\"github-repo\": \"${REPO_URL}\", \"github-branch\": \"${PR_BRANCH}\", \"pr-number\": \"${{ github.event.pull_request.number }}\", \"eval-instances\": \"${EVAL_INSTANCES}\"}}" \ + https://api.github.com/repos/All-Hands-AI/evaluation/actions/workflows/create-branch.yml/dispatches + + # Send Slack message + PR_URL="https://github.com/${{ github.repository }}/pull/${{ github.event.pull_request.number }}" + slack_text="PR $PR_URL has triggered evaluation on $EVAL_INSTANCES instances..." + curl -X POST -H 'Content-type: application/json' --data '{"text":"'"$slack_text"'"}' \ + https://hooks.slack.com/services/${{ secrets.SLACK_TOKEN }} + + - name: Comment on PR + uses: KeisukeYamashita/create-comment@v1 + with: + unique: false + comment: | + Running evaluation on the PR. Once eval is done, the results will be posted. From bb8b4a0b18c3ff32ddbcaec5ebb7b87fac860e90 Mon Sep 17 00:00:00 2001 From: Xingyao Wang Date: Fri, 22 Nov 2024 12:28:32 -0600 Subject: [PATCH 50/56] feat(runtime): add system resource metrics to /server_info endpoint (#5207) Co-authored-by: openhands --- openhands/runtime/action_execution_server.py | 8 ++- openhands/runtime/utils/system_stats.py | 62 ++++++++++++++++++++ tests/runtime/utils/test_system_stats.py | 60 +++++++++++++++++++ 3 files changed, 129 insertions(+), 1 deletion(-) create mode 100644 openhands/runtime/utils/system_stats.py create mode 100644 tests/runtime/utils/test_system_stats.py diff --git a/openhands/runtime/action_execution_server.py b/openhands/runtime/action_execution_server.py index 1251aa346838..e8043133d9b5 100644 --- a/openhands/runtime/action_execution_server.py +++ b/openhands/runtime/action_execution_server.py @@ -52,6 +52,7 @@ from openhands.runtime.utils.files import insert_lines, read_lines from openhands.runtime.utils.runtime_init import init_user_and_working_directory from openhands.runtime.utils.system import check_port_available +from openhands.runtime.utils.system_stats import get_system_stats from openhands.utils.async_utils import call_sync_from_async, wait_all @@ -420,7 +421,12 @@ async def get_server_info(): current_time = time.time() uptime = current_time - client.start_time idle_time = current_time - client.last_execution_time - return {'uptime': uptime, 'idle_time': idle_time} + + return { + 'uptime': uptime, + 'idle_time': idle_time, + 'resources': get_system_stats(), + } @app.post('/execute_action') async def execute_action(action_request: ActionRequest): diff --git a/openhands/runtime/utils/system_stats.py b/openhands/runtime/utils/system_stats.py new file mode 100644 index 000000000000..d0068c248793 --- /dev/null +++ b/openhands/runtime/utils/system_stats.py @@ -0,0 +1,62 @@ +"""Utilities for getting system resource statistics.""" + +import time + +import psutil + + +def get_system_stats() -> dict: + """Get current system resource statistics. + + Returns: + dict: A dictionary containing: + - cpu_percent: CPU usage percentage for the current process + - memory: Memory usage stats (rss, vms, percent) + - disk: Disk usage stats (total, used, free, percent) + - io: I/O statistics (read/write bytes) + """ + process = psutil.Process() + # Get initial CPU percentage (this will return 0.0) + process.cpu_percent() + # Wait a bit and get the actual CPU percentage + time.sleep(0.1) + + with process.oneshot(): + cpu_percent = process.cpu_percent() + memory_info = process.memory_info() + memory_percent = process.memory_percent() + + disk_usage = psutil.disk_usage('/') + + # Get I/O stats directly from /proc/[pid]/io to avoid psutil's field name assumptions + try: + with open(f'/proc/{process.pid}/io', 'rb') as f: + io_stats = {} + for line in f: + if line: + try: + name, value = line.strip().split(b': ') + io_stats[name.decode('ascii')] = int(value) + except (ValueError, UnicodeDecodeError): + continue + except (FileNotFoundError, PermissionError): + io_stats = {'read_bytes': 0, 'write_bytes': 0} + + return { + 'cpu_percent': cpu_percent, + 'memory': { + 'rss': memory_info.rss, + 'vms': memory_info.vms, + 'percent': memory_percent, + }, + 'disk': { + 'total': disk_usage.total, + 'used': disk_usage.used, + 'free': disk_usage.free, + 'percent': disk_usage.percent, + }, + 'io': { + 'read_bytes': io_stats.get('read_bytes', 0), + 'write_bytes': io_stats.get('write_bytes', 0), + }, + } diff --git a/tests/runtime/utils/test_system_stats.py b/tests/runtime/utils/test_system_stats.py new file mode 100644 index 000000000000..afb6c00c2942 --- /dev/null +++ b/tests/runtime/utils/test_system_stats.py @@ -0,0 +1,60 @@ +"""Tests for system stats utilities.""" + +import psutil + +from openhands.runtime.utils.system_stats import get_system_stats + + +def test_get_system_stats(): + """Test that get_system_stats returns valid system statistics.""" + stats = get_system_stats() + + # Test structure + assert isinstance(stats, dict) + assert set(stats.keys()) == {'cpu_percent', 'memory', 'disk', 'io'} + + # Test CPU stats + assert isinstance(stats['cpu_percent'], float) + assert 0 <= stats['cpu_percent'] <= 100 * psutil.cpu_count() + + # Test memory stats + assert isinstance(stats['memory'], dict) + assert set(stats['memory'].keys()) == {'rss', 'vms', 'percent'} + assert isinstance(stats['memory']['rss'], int) + assert isinstance(stats['memory']['vms'], int) + assert isinstance(stats['memory']['percent'], float) + assert stats['memory']['rss'] > 0 + assert stats['memory']['vms'] > 0 + assert 0 <= stats['memory']['percent'] <= 100 + + # Test disk stats + assert isinstance(stats['disk'], dict) + assert set(stats['disk'].keys()) == {'total', 'used', 'free', 'percent'} + assert isinstance(stats['disk']['total'], int) + assert isinstance(stats['disk']['used'], int) + assert isinstance(stats['disk']['free'], int) + assert isinstance(stats['disk']['percent'], float) + assert stats['disk']['total'] > 0 + assert stats['disk']['used'] >= 0 + assert stats['disk']['free'] >= 0 + assert 0 <= stats['disk']['percent'] <= 100 + # Verify that used + free is less than or equal to total + # (might not be exactly equal due to filesystem overhead) + assert stats['disk']['used'] + stats['disk']['free'] <= stats['disk']['total'] + + # Test I/O stats + assert isinstance(stats['io'], dict) + assert set(stats['io'].keys()) == {'read_bytes', 'write_bytes'} + assert isinstance(stats['io']['read_bytes'], int) + assert isinstance(stats['io']['write_bytes'], int) + assert stats['io']['read_bytes'] >= 0 + assert stats['io']['write_bytes'] >= 0 + + +def test_get_system_stats_stability(): + """Test that get_system_stats can be called multiple times without errors.""" + # Call multiple times to ensure stability + for _ in range(3): + stats = get_system_stats() + assert isinstance(stats, dict) + assert stats['cpu_percent'] >= 0 From becb17f0c86ca4c2454bfff59a14b0e780860589 Mon Sep 17 00:00:00 2001 From: "sp.wack" <83104063+amanape@users.noreply.github.com> Date: Fri, 22 Nov 2024 23:38:27 +0400 Subject: [PATCH 51/56] feat(frontend): Utilize TanStack Query (#5096) --- frontend/.eslintrc | 3 +- frontend/__tests__/clear-session.test.ts | 40 --- .../components/feedback-form.test.tsx | 14 +- .../file-explorer/FileExplorer.test.tsx | 13 +- .../components/user-actions.test.tsx | 12 +- frontend/__tests__/routes/_oh.test.tsx | 162 ++++++++++-- frontend/__tests__/utils/cache.test.ts | 53 ---- .../utils/extract-next-page-from-link.test.ts | 13 + .../utils/handle-capture-consent.test.ts | 44 ++++ frontend/package-lock.json | 139 ++++++++++ frontend/package.json | 2 + frontend/playwright.config.ts | 6 +- frontend/src/api/github.ts | 76 +----- frontend/src/api/open-hands.ts | 196 +++++++++----- .../analytics-consent-form-modal.tsx | 29 +- frontend/src/components/chat-interface.tsx | 7 +- frontend/src/components/controls.tsx | 26 +- frontend/src/components/event-handler.tsx | 86 +++--- frontend/src/components/feedback-form.tsx | 27 +- .../components/file-explorer/FileExplorer.tsx | 108 ++++---- .../src/components/file-explorer/TreeNode.tsx | 66 ++--- .../src/components/form/settings-form.tsx | 88 ++++--- .../github-repositories-suggestion-box.tsx | 11 +- .../modals/AccountSettingsModal.tsx | 52 ++-- .../modals/ConnectToGitHubByTokenModal.tsx | 54 ---- .../modals/connect-to-github-modal.tsx | 28 +- .../project-menu/ProjectMenuCard.tsx | 2 +- .../project-menu/project-menu-details.tsx | 4 +- frontend/src/components/user-actions.tsx | 9 +- frontend/src/context/auth-context.tsx | 82 ++++++ frontend/src/context/user-prefs-context.tsx | 55 ++++ frontend/src/entry.client.tsx | 31 ++- frontend/src/hooks/mutation/use-save-file.ts | 21 ++ .../src/hooks/mutation/use-submit-feedback.ts | 21 ++ .../src/hooks/mutation/use-upload-files.ts | 16 ++ .../src/hooks/query/use-ai-config-options.ts | 14 + frontend/src/hooks/query/use-config.ts | 8 + frontend/src/hooks/query/use-github-user.ts | 40 +++ frontend/src/hooks/query/use-is-authed.ts | 19 ++ .../src/hooks/query/use-latest-repo-commit.ts | 28 ++ frontend/src/hooks/query/use-list-file.ts | 17 ++ frontend/src/hooks/query/use-list-files.ts | 24 ++ .../src/hooks/query/use-user-repositories.ts | 63 +++++ frontend/src/hooks/use-end-session.ts | 31 +++ frontend/src/hooks/use-github-auth-url.ts | 20 ++ frontend/src/routes/_oh._index/route.tsx | 113 +++----- frontend/src/routes/_oh._index/task-form.tsx | 31 ++- .../_oh.app._index/code-editor-component.tsx | 67 ++--- frontend/src/routes/_oh.app._index/route.tsx | 43 +-- frontend/src/routes/_oh.app.tsx | 76 ++---- frontend/src/routes/_oh.tsx | 249 ++++++------------ frontend/src/routes/end-session.ts | 7 - frontend/src/routes/login.ts | 9 - frontend/src/routes/logout.ts | 13 - frontend/src/routes/oauth.github.callback.tsx | 42 +-- frontend/src/routes/set-consent.ts | 9 - frontend/src/routes/settings.ts | 108 -------- frontend/src/services/auth.ts | 31 +-- frontend/src/utils/cache.ts | 61 ----- frontend/src/utils/clear-session.ts | 21 -- .../src/utils/extract-next-page-from-link.ts | 11 + frontend/src/utils/handle-capture-consent.ts | 15 ++ frontend/src/utils/settings-utils.ts | 95 +++++++ frontend/src/utils/user-is-authenticated.ts | 20 -- frontend/test-utils.tsx | 18 +- frontend/tests/redirect.spec.ts | 12 + 66 files changed, 1617 insertions(+), 1294 deletions(-) delete mode 100644 frontend/__tests__/clear-session.test.ts delete mode 100644 frontend/__tests__/utils/cache.test.ts create mode 100644 frontend/__tests__/utils/extract-next-page-from-link.test.ts create mode 100644 frontend/__tests__/utils/handle-capture-consent.test.ts delete mode 100644 frontend/src/components/modals/ConnectToGitHubByTokenModal.tsx create mode 100644 frontend/src/context/auth-context.tsx create mode 100644 frontend/src/context/user-prefs-context.tsx create mode 100644 frontend/src/hooks/mutation/use-save-file.ts create mode 100644 frontend/src/hooks/mutation/use-submit-feedback.ts create mode 100644 frontend/src/hooks/mutation/use-upload-files.ts create mode 100644 frontend/src/hooks/query/use-ai-config-options.ts create mode 100644 frontend/src/hooks/query/use-config.ts create mode 100644 frontend/src/hooks/query/use-github-user.ts create mode 100644 frontend/src/hooks/query/use-is-authed.ts create mode 100644 frontend/src/hooks/query/use-latest-repo-commit.ts create mode 100644 frontend/src/hooks/query/use-list-file.ts create mode 100644 frontend/src/hooks/query/use-list-files.ts create mode 100644 frontend/src/hooks/query/use-user-repositories.ts create mode 100644 frontend/src/hooks/use-end-session.ts create mode 100644 frontend/src/hooks/use-github-auth-url.ts delete mode 100644 frontend/src/routes/end-session.ts delete mode 100644 frontend/src/routes/login.ts delete mode 100644 frontend/src/routes/logout.ts delete mode 100644 frontend/src/routes/set-consent.ts delete mode 100644 frontend/src/routes/settings.ts delete mode 100644 frontend/src/utils/cache.ts delete mode 100644 frontend/src/utils/clear-session.ts create mode 100644 frontend/src/utils/extract-next-page-from-link.ts create mode 100644 frontend/src/utils/handle-capture-consent.ts create mode 100644 frontend/src/utils/settings-utils.ts delete mode 100644 frontend/src/utils/user-is-authenticated.ts diff --git a/frontend/.eslintrc b/frontend/.eslintrc index d5cb543bd728..29896d083c35 100644 --- a/frontend/.eslintrc +++ b/frontend/.eslintrc @@ -10,7 +10,8 @@ "plugin:@typescript-eslint/eslint-recommended", "plugin:@typescript-eslint/recommended", "plugin:react/recommended", - "plugin:react-hooks/recommended" + "plugin:react-hooks/recommended", + "plugin:@tanstack/query/recommended" ], "plugins": [ "prettier" diff --git a/frontend/__tests__/clear-session.test.ts b/frontend/__tests__/clear-session.test.ts deleted file mode 100644 index 4a172608497f..000000000000 --- a/frontend/__tests__/clear-session.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { describe, it, expect, beforeEach, vi } from "vitest"; -import { clearSession } from "../src/utils/clear-session"; -import store from "../src/store"; -import { initialState as browserInitialState } from "../src/state/browserSlice"; - -describe("clearSession", () => { - beforeEach(() => { - // Mock localStorage - const localStorageMock = { - getItem: vi.fn(), - setItem: vi.fn(), - removeItem: vi.fn(), - clear: vi.fn(), - }; - vi.stubGlobal("localStorage", localStorageMock); - - // Set initial browser state to non-default values - store.dispatch({ - type: "browser/setUrl", - payload: "https://example.com", - }); - store.dispatch({ - type: "browser/setScreenshotSrc", - payload: "base64screenshot", - }); - }); - - it("should clear localStorage and reset browser state", () => { - clearSession(); - - // Verify localStorage items were removed - expect(localStorage.removeItem).toHaveBeenCalledWith("token"); - expect(localStorage.removeItem).toHaveBeenCalledWith("repo"); - - // Verify browser state was reset - const state = store.getState(); - expect(state.browser.url).toBe(browserInitialState.url); - expect(state.browser.screenshotSrc).toBe(browserInitialState.screenshotSrc); - }); -}); diff --git a/frontend/__tests__/components/feedback-form.test.tsx b/frontend/__tests__/components/feedback-form.test.tsx index 28684401e2cb..f686c45bf9eb 100644 --- a/frontend/__tests__/components/feedback-form.test.tsx +++ b/frontend/__tests__/components/feedback-form.test.tsx @@ -1,6 +1,7 @@ import { render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { afterEach, describe, expect, it, vi } from "vitest"; +import { renderWithProviders } from "test-utils"; import { FeedbackForm } from "#/components/feedback-form"; describe("FeedbackForm", () => { @@ -12,7 +13,9 @@ describe("FeedbackForm", () => { }); it("should render correctly", () => { - render(); + renderWithProviders( + , + ); screen.getByLabelText("Email"); screen.getByLabelText("Private"); @@ -23,7 +26,9 @@ describe("FeedbackForm", () => { }); it("should switch between private and public permissions", async () => { - render(); + renderWithProviders( + , + ); const privateRadio = screen.getByLabelText("Private"); const publicRadio = screen.getByLabelText("Public"); @@ -40,10 +45,11 @@ describe("FeedbackForm", () => { }); it("should call onClose when the close button is clicked", async () => { - render(); + renderWithProviders( + , + ); await user.click(screen.getByRole("button", { name: "Cancel" })); expect(onCloseMock).toHaveBeenCalled(); }); - }); diff --git a/frontend/__tests__/components/file-explorer/FileExplorer.test.tsx b/frontend/__tests__/components/file-explorer/FileExplorer.test.tsx index a1c0717783e9..357dd61e1bd9 100644 --- a/frontend/__tests__/components/file-explorer/FileExplorer.test.tsx +++ b/frontend/__tests__/components/file-explorer/FileExplorer.test.tsx @@ -16,16 +16,13 @@ vi.mock("../../services/fileService", async () => ({ })); const renderFileExplorerWithRunningAgentState = () => - renderWithProviders( - {}} />, - { - preloadedState: { - agent: { - curAgentState: AgentState.RUNNING, - }, + renderWithProviders( {}} />, { + preloadedState: { + agent: { + curAgentState: AgentState.RUNNING, }, }, - ); + }); describe.skip("FileExplorer", () => { afterEach(() => { diff --git a/frontend/__tests__/components/user-actions.test.tsx b/frontend/__tests__/components/user-actions.test.tsx index 6186562ab9f1..b9bb65d0f3c9 100644 --- a/frontend/__tests__/components/user-actions.test.tsx +++ b/frontend/__tests__/components/user-actions.test.tsx @@ -1,7 +1,6 @@ import { render, screen } from "@testing-library/react"; import { describe, expect, it, test, vi, afterEach } from "vitest"; import userEvent from "@testing-library/user-event"; -import * as Remix from "@remix-run/react"; import { UserActions } from "#/components/user-actions"; describe("UserActions", () => { @@ -9,14 +8,9 @@ describe("UserActions", () => { const onClickAccountSettingsMock = vi.fn(); const onLogoutMock = vi.fn(); - const useFetcherSpy = vi.spyOn(Remix, "useFetcher"); - // @ts-expect-error - Only returning the relevant properties for the test - useFetcherSpy.mockReturnValue({ state: "idle" }); - afterEach(() => { onClickAccountSettingsMock.mockClear(); onLogoutMock.mockClear(); - useFetcherSpy.mockClear(); }); it("should render", () => { @@ -111,10 +105,8 @@ describe("UserActions", () => { expect(onLogoutMock).not.toHaveBeenCalled(); }); - it("should display the loading spinner", () => { - // @ts-expect-error - Only returning the relevant properties for the test - useFetcherSpy.mockReturnValue({ state: "loading" }); - + // FIXME: Spinner now provided through useQuery + it.skip("should display the loading spinner", () => { render( { - describe("brand logo", () => { - it.todo("should not do anything if the user is in the main screen"); - it.todo( - "should be clickable and redirect to the main screen if the user is not in the main screen", + const RemixStub = createRemixStub([{ Component: MainApp, path: "/" }]); + + const { userIsAuthenticatedMock, settingsAreUpToDateMock } = vi.hoisted( + () => ({ + userIsAuthenticatedMock: vi.fn(), + settingsAreUpToDateMock: vi.fn(), + }), + ); + + beforeAll(() => { + vi.mock("#/utils/user-is-authenticated", () => ({ + userIsAuthenticated: userIsAuthenticatedMock.mockReturnValue(true), + })); + + vi.mock("#/services/settings", async (importOriginal) => ({ + ...(await importOriginal()), + settingsAreUpToDate: settingsAreUpToDateMock, + })); + }); + + afterEach(() => { + vi.clearAllMocks(); + localStorage.clear(); + }); + + it("should render", async () => { + renderWithProviders(); + await screen.findByTestId("root-layout"); + }); + + it("should render the AI config modal if the user is authed", async () => { + // Our mock return value is true by default + renderWithProviders(); + await screen.findByTestId("ai-config-modal"); + }); + + it("should render the AI config modal if settings are not up-to-date", async () => { + settingsAreUpToDateMock.mockReturnValue(false); + renderWithProviders(); + + await screen.findByTestId("ai-config-modal"); + }); + + it("should not render the AI config modal if the settings are up-to-date", async () => { + settingsAreUpToDateMock.mockReturnValue(true); + renderWithProviders(); + + await waitFor(() => { + expect(screen.queryByTestId("ai-config-modal")).not.toBeInTheDocument(); + }); + }); + + it("should capture the user's consent", async () => { + const user = userEvent.setup(); + const handleCaptureConsentSpy = vi.spyOn( + CaptureConsent, + "handleCaptureConsent", ); + + renderWithProviders(); + + // The user has not consented to tracking + const consentForm = await screen.findByTestId("user-capture-consent-form"); + expect(handleCaptureConsentSpy).not.toHaveBeenCalled(); + expect(localStorage.getItem("analytics-consent")).toBeNull(); + + const submitButton = within(consentForm).getByRole("button", { + name: /confirm preferences/i, + }); + await user.click(submitButton); + + // The user has now consented to tracking + expect(handleCaptureConsentSpy).toHaveBeenCalledWith(true); + expect(localStorage.getItem("analytics-consent")).toBe("true"); + expect( + screen.queryByTestId("user-capture-consent-form"), + ).not.toBeInTheDocument(); }); - describe("user menu", () => { - it.todo("should open the user menu when clicked"); + it("should not render the user consent form if the user has already made a decision", async () => { + localStorage.setItem("analytics-consent", "true"); + renderWithProviders(); - describe("logged out", () => { - it.todo("should display a placeholder"); - test.todo("the logout option in the user menu should be disabled"); + await waitFor(() => { + expect( + screen.queryByTestId("user-capture-consent-form"), + ).not.toBeInTheDocument(); }); + }); + + it("should render a new project button if a token is set", async () => { + localStorage.setItem("token", "test-token"); + const { rerender } = renderWithProviders(); - describe("logged in", () => { - it.todo("should display the user's avatar"); - it.todo("should log the user out when the logout option is clicked"); + await screen.findByTestId("new-project-button"); + + localStorage.removeItem("token"); + rerender(); + + await waitFor(() => { + expect( + screen.queryByTestId("new-project-button"), + ).not.toBeInTheDocument(); }); }); - describe("config", () => { - it.todo("should open the config modal when clicked"); - it.todo( - "should not save the config and close the config modal when the close button is clicked", - ); - it.todo( - "should save the config when the save button is clicked and close the modal", - ); - it.todo("should warn the user about saving the config when in /app"); + // TODO: Move to e2e tests + it.skip("should update the i18n language when the language settings change", async () => { + const changeLanguageSpy = vi.spyOn(i18n, "changeLanguage"); + const { rerender } = renderWithProviders(); + + // The default language is English + expect(changeLanguageSpy).toHaveBeenCalledWith("en"); + + localStorage.setItem("LANGUAGE", "es"); + + rerender(); + expect(changeLanguageSpy).toHaveBeenCalledWith("es"); + + rerender(); + // The language has not changed, so the spy should not have been called again + expect(changeLanguageSpy).toHaveBeenCalledTimes(2); + }); + + // FIXME: logoutCleanup has been replaced with a hook + it.skip("should call logoutCleanup after a logout", async () => { + const user = userEvent.setup(); + localStorage.setItem("ghToken", "test-token"); + + // const logoutCleanupSpy = vi.spyOn(LogoutCleanup, "logoutCleanup"); + renderWithProviders(); + + const userActions = await screen.findByTestId("user-actions"); + const userAvatar = within(userActions).getByTestId("user-avatar"); + await user.click(userAvatar); + + const logout = within(userActions).getByRole("button", { name: /logout/i }); + await user.click(logout); + + // expect(logoutCleanupSpy).toHaveBeenCalled(); + expect(localStorage.getItem("ghToken")).toBeNull(); }); }); diff --git a/frontend/__tests__/utils/cache.test.ts b/frontend/__tests__/utils/cache.test.ts deleted file mode 100644 index 6b3762c38a65..000000000000 --- a/frontend/__tests__/utils/cache.test.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { afterEach } from "node:test"; -import { beforeEach, describe, expect, it, vi } from "vitest"; -import { cache } from "#/utils/cache"; - -describe("Cache", () => { - const testKey = "key"; - const testData = { message: "Hello, world!" }; - const testTTL = 1000; // 1 second - - beforeEach(() => { - vi.useFakeTimers(); - }); - - afterEach(() => { - vi.useRealTimers(); - }); - - it("gets data from memory if not expired", () => { - cache.set(testKey, testData, testTTL); - - expect(cache.get(testKey)).toEqual(testData); - }); - - it("should expire after 5 minutes by default", () => { - cache.set(testKey, testData); - expect(cache.get(testKey)).not.toBeNull(); - - vi.advanceTimersByTime(5 * 60 * 1000 + 1); - - expect(cache.get(testKey)).toBeNull(); - }); - - it("returns null if cached data is expired", () => { - cache.set(testKey, testData, testTTL); - - vi.advanceTimersByTime(testTTL + 1); - expect(cache.get(testKey)).toBeNull(); - }); - - it("deletes data from memory", () => { - cache.set(testKey, testData, testTTL); - cache.delete(testKey); - expect(cache.get(testKey)).toBeNull(); - }); - - it("clears all data with the app prefix from memory", () => { - cache.set(testKey, testData, testTTL); - cache.set("anotherKey", { data: "More data" }, testTTL); - cache.clearAll(); - expect(cache.get(testKey)).toBeNull(); - expect(cache.get("anotherKey")).toBeNull(); - }); -}); diff --git a/frontend/__tests__/utils/extract-next-page-from-link.test.ts b/frontend/__tests__/utils/extract-next-page-from-link.test.ts new file mode 100644 index 000000000000..a7541f95a0ad --- /dev/null +++ b/frontend/__tests__/utils/extract-next-page-from-link.test.ts @@ -0,0 +1,13 @@ +import { expect, test } from "vitest"; +import { extractNextPageFromLink } from "#/utils/extract-next-page-from-link"; + +test("extractNextPageFromLink", () => { + const link = `; rel="prev", ; rel="next", ; rel="last", ; rel="first"`; + expect(extractNextPageFromLink(link)).toBe(4); + + const noNextLink = `; rel="prev", ; rel="first"`; + expect(extractNextPageFromLink(noNextLink)).toBeNull(); + + const extra = `; rel="next", ; rel="last"`; + expect(extractNextPageFromLink(extra)).toBe(2); +}); diff --git a/frontend/__tests__/utils/handle-capture-consent.test.ts b/frontend/__tests__/utils/handle-capture-consent.test.ts new file mode 100644 index 000000000000..3b337424a7ae --- /dev/null +++ b/frontend/__tests__/utils/handle-capture-consent.test.ts @@ -0,0 +1,44 @@ +import posthog from "posthog-js"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { handleCaptureConsent } from "#/utils/handle-capture-consent"; + +describe("handleCaptureConsent", () => { + const optInSpy = vi.spyOn(posthog, "opt_in_capturing"); + const optOutSpy = vi.spyOn(posthog, "opt_out_capturing"); + const hasOptedInSpy = vi.spyOn(posthog, "has_opted_in_capturing"); + const hasOptedOutSpy = vi.spyOn(posthog, "has_opted_out_capturing"); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it("should opt out of of capturing", () => { + handleCaptureConsent(false); + + expect(optOutSpy).toHaveBeenCalled(); + expect(optInSpy).not.toHaveBeenCalled(); + }); + + it("should opt in to capturing if the user consents", () => { + handleCaptureConsent(true); + + expect(optInSpy).toHaveBeenCalled(); + expect(optOutSpy).not.toHaveBeenCalled(); + }); + + it("should not opt in to capturing if the user is already opted in", () => { + hasOptedInSpy.mockReturnValueOnce(true); + handleCaptureConsent(true); + + expect(optInSpy).not.toHaveBeenCalled(); + expect(optOutSpy).not.toHaveBeenCalled(); + }); + + it("should not opt out of capturing if the user is already opted out", () => { + hasOptedOutSpy.mockReturnValueOnce(true); + handleCaptureConsent(false); + + expect(optOutSpy).not.toHaveBeenCalled(); + expect(optInSpy).not.toHaveBeenCalled(); + }); +}); diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 5f72fe6d0b96..89613585652a 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -15,6 +15,7 @@ "@remix-run/node": "^2.11.2", "@remix-run/react": "^2.11.2", "@remix-run/serve": "^2.11.2", + "@tanstack/react-query": "^5.60.5", "@vitejs/plugin-react": "^4.3.2", "@xterm/addon-fit": "^0.10.0", "@xterm/xterm": "^5.4.0", @@ -50,6 +51,7 @@ "@remix-run/dev": "^2.11.2", "@remix-run/testing": "^2.11.2", "@tailwindcss/typography": "^0.5.15", + "@tanstack/eslint-plugin-query": "^5.60.1", "@testing-library/jest-dom": "^6.6.1", "@testing-library/react": "^16.0.1", "@testing-library/user-event": "^14.5.2", @@ -5812,6 +5814,143 @@ "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20" } }, + "node_modules/@tanstack/eslint-plugin-query": { + "version": "5.60.1", + "resolved": "https://registry.npmjs.org/@tanstack/eslint-plugin-query/-/eslint-plugin-query-5.60.1.tgz", + "integrity": "sha512-oCaWtFKa6WwX14fm/Sp486eTFXXgadiDzEYxhM/tiAlM+xzvPwp6ZHgR6sndmvYK+s/jbksDCTLIPS0PCH8L2g==", + "dev": true, + "dependencies": { + "@typescript-eslint/utils": "^8.3.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + } + }, + "node_modules/@tanstack/eslint-plugin-query/node_modules/@typescript-eslint/scope-manager": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.14.0.tgz", + "integrity": "sha512-aBbBrnW9ARIDn92Zbo7rguLnqQ/pOrUguVpbUwzOhkFg2npFDwTgPGqFqE0H5feXcOoJOfX3SxlJaKEVtq54dw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.14.0", + "@typescript-eslint/visitor-keys": "8.14.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@tanstack/eslint-plugin-query/node_modules/@typescript-eslint/types": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.14.0.tgz", + "integrity": "sha512-yjeB9fnO/opvLJFAsPNYlKPnEM8+z4og09Pk504dkqonT02AyL5Z9SSqlE0XqezS93v6CXn49VHvB2G7XSsl0g==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@tanstack/eslint-plugin-query/node_modules/@typescript-eslint/typescript-estree": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.14.0.tgz", + "integrity": "sha512-OPXPLYKGZi9XS/49rdaCbR5j/S14HazviBlUQFvSKz3npr3NikF+mrgK7CFVur6XEt95DZp/cmke9d5i3vtVnQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.14.0", + "@typescript-eslint/visitor-keys": "8.14.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@tanstack/eslint-plugin-query/node_modules/@typescript-eslint/utils": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.14.0.tgz", + "integrity": "sha512-OGqj6uB8THhrHj0Fk27DcHPojW7zKwKkPmHXHvQ58pLYp4hy8CSUdTKykKeh+5vFqTTVmjz0zCOOPKRovdsgHA==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.14.0", + "@typescript-eslint/types": "8.14.0", + "@typescript-eslint/typescript-estree": "8.14.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + } + }, + "node_modules/@tanstack/eslint-plugin-query/node_modules/@typescript-eslint/visitor-keys": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.14.0.tgz", + "integrity": "sha512-vG0XZo8AdTH9OE6VFRwAZldNc7qtJ/6NLGWak+BtENuEUXGZgFpihILPiBvKXvJ2nFu27XNGC6rKiwuaoMbYzQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.14.0", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@tanstack/query-core": { + "version": "5.60.5", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.60.5.tgz", + "integrity": "sha512-jiS1aC3XI3BJp83ZiTuDLerTmn9P3U95r6p+6/SNauLJaYxfIC4dMuWygwnBHIZxjn2zJqEpj3nysmPieoxfPQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.60.5", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.60.5.tgz", + "integrity": "sha512-M77bOsPwj1wYE56gk7iJvxGAr4IC12NWdIDhT+Eo8ldkWRHMvIR8I/rufIvT1OXoV/bl7EECwuRuMlxxWtvW2Q==", + "dependencies": { + "@tanstack/query-core": "5.60.5" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, "node_modules/@testing-library/dom": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 8e134ec5f0e1..5b7d375b692f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,6 +14,7 @@ "@remix-run/node": "^2.11.2", "@remix-run/react": "^2.11.2", "@remix-run/serve": "^2.11.2", + "@tanstack/react-query": "^5.60.5", "@vitejs/plugin-react": "^4.3.2", "@xterm/addon-fit": "^0.10.0", "@xterm/xterm": "^5.4.0", @@ -76,6 +77,7 @@ "@remix-run/dev": "^2.11.2", "@remix-run/testing": "^2.11.2", "@tailwindcss/typography": "^0.5.15", + "@tanstack/eslint-plugin-query": "^5.60.1", "@testing-library/jest-dom": "^6.6.1", "@testing-library/react": "^16.0.1", "@testing-library/user-event": "^14.5.2", diff --git a/frontend/playwright.config.ts b/frontend/playwright.config.ts index 53a48004433d..cfbc10779e14 100644 --- a/frontend/playwright.config.ts +++ b/frontend/playwright.config.ts @@ -26,7 +26,7 @@ export default defineConfig({ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { /* Base URL to use in actions like `await page.goto('/')`. */ - baseURL: "http://127.0.0.1:3000", + baseURL: "http://localhost:3001/", /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ trace: "on-first-retry", @@ -72,8 +72,8 @@ export default defineConfig({ /* Run your local dev server before starting the tests */ webServer: { - command: "npm run dev:mock -- --port 3000", - url: "http://127.0.0.1:3000", + command: "npm run dev:mock -- --port 3001", + url: "http://localhost:3001/", reuseExistingServer: !process.env.CI, }, }); diff --git a/frontend/src/api/github.ts b/frontend/src/api/github.ts index 2a0b5c509254..1cd3c7587cc2 100644 --- a/frontend/src/api/github.ts +++ b/frontend/src/api/github.ts @@ -27,82 +27,19 @@ export const isGitHubErrorReponse = >( */ export const retrieveGitHubUserRepositories = async ( token: string, - per_page = 30, page = 1, + per_page = 30, ): Promise => { const url = new URL("https://api.github.com/user/repos"); url.searchParams.append("sort", "pushed"); // sort by most recently pushed - url.searchParams.append("per_page", per_page.toString()); url.searchParams.append("page", page.toString()); + url.searchParams.append("per_page", per_page.toString()); return fetch(url.toString(), { headers: generateGitHubAPIHeaders(token), }); }; -/** - * Given a GitHub token, retrieves all repositories of the authenticated user - * @param token The GitHub token - * @returns A list of repositories or an error response - */ -export const retrieveAllGitHubUserRepositories = async ( - token: string, -): Promise => { - const repositories: GitHubRepository[] = []; - - // Fetch the first page to extract the last page number and get the first batch of data - const firstPageResponse = await retrieveGitHubUserRepositories(token, 100, 1); - - if (!firstPageResponse.ok) { - return { - message: "Failed to fetch repositories", - documentation_url: - "https://docs.github.com/rest/reference/repos#list-repositories-for-the-authenticated-user", - status: firstPageResponse.status, - }; - } - - const firstPageData = await firstPageResponse.json(); - repositories.push(...firstPageData); - - // Check for pagination and extract the last page number - const link = firstPageResponse.headers.get("link"); - const lastPageMatch = link?.match(/page=(\d+)>; rel="last"/); - const lastPage = lastPageMatch ? parseInt(lastPageMatch[1], 10) : 1; - - // If there is only one page, return the fetched repositories - if (lastPage === 1) { - return repositories; - } - - // Create an array of promises for the remaining pages - const promises = []; - for (let page = 2; page <= lastPage; page += 1) { - promises.push(retrieveGitHubUserRepositories(token, 100, page)); - } - - // Fetch all pages in parallel - const responses = await Promise.all(promises); - - for (const response of responses) { - if (response.ok) { - // TODO: Is there a way to avoid using await within a loop? - // eslint-disable-next-line no-await-in-loop - const data = await response.json(); - repositories.push(...data); - } else { - return { - message: "Failed to fetch repositories", - documentation_url: - "https://docs.github.com/rest/reference/repos#list-repositories-for-the-authenticated-user", - status: response.status, - }; - } - } - - return repositories; -}; - /** * Given a GitHub token, retrieves the authenticated user * @param token The GitHub token @@ -114,6 +51,11 @@ export const retrieveGitHubUser = async ( const response = await fetch("https://api.github.com/user", { headers: generateGitHubAPIHeaders(token), }); + + if (!response.ok) { + throw new Error("Failed to retrieve user data"); + } + const data = await response.json(); if (!isGitHubErrorReponse(data)) { @@ -149,5 +91,9 @@ export const retrieveLatestGitHubCommit = async ( headers: generateGitHubAPIHeaders(token), }); + if (!response.ok) { + throw new Error("Failed to retrieve latest commit"); + } + return response.json(); }; diff --git a/frontend/src/api/open-hands.ts b/frontend/src/api/open-hands.ts index 33f08d94f21e..20e7843befca 100644 --- a/frontend/src/api/open-hands.ts +++ b/frontend/src/api/open-hands.ts @@ -1,5 +1,4 @@ import { request } from "#/services/api"; -import { cache } from "#/utils/cache"; import { SaveFileSuccessResponse, FileUploadSuccessResponse, @@ -17,13 +16,13 @@ class OpenHands { * @returns List of models available */ static async getModels(): Promise { - const cachedData = cache.get("models"); - if (cachedData) return cachedData; + const response = await fetch("/api/options/models"); - const data = await request("/api/options/models"); - cache.set("models", data); + if (!response.ok) { + throw new Error("Failed to fetch models"); + } - return data; + return response.json(); } /** @@ -31,13 +30,13 @@ class OpenHands { * @returns List of agents available */ static async getAgents(): Promise { - const cachedData = cache.get("agents"); - if (cachedData) return cachedData; + const response = await fetch("/api/options/agents"); - const data = await request(`/api/options/agents`); - cache.set("agents", data); + if (!response.ok) { + throw new Error("Failed to fetch agents"); + } - return data; + return response.json(); } /** @@ -45,23 +44,23 @@ class OpenHands { * @returns List of security analyzers available */ static async getSecurityAnalyzers(): Promise { - const cachedData = cache.get("agents"); - if (cachedData) return cachedData; + const response = await fetch("/api/options/security-analyzers"); - const data = await request(`/api/options/security-analyzers`); - cache.set("security-analyzers", data); + if (!response.ok) { + throw new Error("Failed to fetch security analyzers"); + } - return data; + return response.json(); } static async getConfig(): Promise { - const cachedData = cache.get("config"); - if (cachedData) return cachedData; + const response = await fetch("/config.json"); - const data = await request("/config.json"); - cache.set("config", data); + if (!response.ok) { + throw new Error("Failed to fetch config"); + } - return data; + return response.json(); } /** @@ -69,10 +68,21 @@ class OpenHands { * @param path Path to list files from * @returns List of files available in the given path. If path is not provided, it lists all the files in the workspace */ - static async getFiles(path?: string): Promise { - let url = "/api/list-files"; - if (path) url += `?path=${encodeURIComponent(path)}`; - return request(url); + static async getFiles(token: string, path?: string): Promise { + const url = new URL("/api/list-files", window.location.origin); + if (path) url.searchParams.append("path", path); + + const response = await fetch(url.toString(), { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + + if (!response.ok) { + throw new Error("Failed to fetch files"); + } + + return response.json(); } /** @@ -80,9 +90,21 @@ class OpenHands { * @param path Full path of the file to retrieve * @returns Content of the file */ - static async getFile(path: string): Promise { - const url = `/api/select-file?file=${encodeURIComponent(path)}`; - const data = await request(url); + static async getFile(token: string, path: string): Promise { + const url = new URL("/api/select-file", window.location.origin); + url.searchParams.append("file", path); + + const response = await fetch(url.toString(), { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + + if (!response.ok) { + throw new Error("Failed to fetch file"); + } + + const data = await response.json(); return data.code; } @@ -93,16 +115,32 @@ class OpenHands { * @returns Success message or error message */ static async saveFile( + token: string, path: string, content: string, - ): Promise { - return request(`/api/save-file`, { + ): Promise { + const response = await fetch("/api/save-file", { method: "POST", body: JSON.stringify({ filePath: path, content }), headers: { "Content-Type": "application/json", + Authorization: `Bearer ${token}`, }, }); + + if (!response.ok) { + throw new Error("Failed to save file"); + } + + const data = (await response.json()) as + | SaveFileSuccessResponse + | ErrorResponse; + + if ("error" in data) { + throw new Error(data.error); + } + + return data; } /** @@ -111,24 +149,33 @@ class OpenHands { * @returns Success message or error message */ static async uploadFiles( - file: File[], - ): Promise { + token: string, + files: File[], + ): Promise { const formData = new FormData(); - file.forEach((f) => formData.append("files", f)); + files.forEach((file) => formData.append("files", file)); - return request(`/api/upload-files`, { + const response = await fetch("/api/upload-files", { method: "POST", body: formData, + headers: { + Authorization: `Bearer ${token}`, + }, }); - } - /** - * Get the blob of the workspace zip - * @returns Blob of the workspace zip - */ - static async getWorkspaceZip(): Promise { - const response = await request(`/api/zip-directory`, {}, false, true); - return response.blob(); + if (!response.ok) { + throw new Error("Failed to upload files"); + } + + const data = (await response.json()) as + | FileUploadSuccessResponse + | ErrorResponse; + + if ("error" in data) { + throw new Error(data.error); + } + + return data; } /** @@ -136,14 +183,53 @@ class OpenHands { * @param data Feedback data * @returns The stored feedback data */ - static async submitFeedback(data: Feedback): Promise { - return request(`/api/submit-feedback`, { + static async submitFeedback( + token: string, + feedback: Feedback, + ): Promise { + const response = await fetch("/api/submit-feedback", { method: "POST", - body: JSON.stringify(data), + body: JSON.stringify(feedback), headers: { "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + }); + + if (!response.ok) { + throw new Error("Failed to submit feedback"); + } + + return response.json(); + } + + /** + * Authenticate with GitHub token + * @returns Response with authentication status and user info if successful + */ + static async authenticate( + gitHubToken: string, + appMode: GetConfigResponse["APP_MODE"], + ): Promise { + if (appMode === "oss") return true; + + const response = await fetch("/api/authenticate", { + method: "POST", + headers: { + "X-GitHub-Token": gitHubToken, }, }); + + return response.ok; + } + + /** + * Get the blob of the workspace zip + * @returns Blob of the workspace zip + */ + static async getWorkspaceZip(): Promise { + const response = await request(`/api/zip-directory`, {}, false, true); + return response.blob(); } /** @@ -153,27 +239,19 @@ class OpenHands { static async getGitHubAccessToken( code: string, ): Promise { - return request(`/api/github/callback`, { + const response = await fetch("/api/github/callback", { method: "POST", body: JSON.stringify({ code }), headers: { "Content-Type": "application/json", }, }); - } - /** - * Authenticate with GitHub token - * @returns Response with authentication status and user info if successful - */ - static async authenticate(): Promise { - return request( - `/api/authenticate`, - { - method: "POST", - }, - true, - ); + if (!response.ok) { + throw new Error("Failed to get GitHub access token"); + } + + return response.json(); } /** diff --git a/frontend/src/components/analytics-consent-form-modal.tsx b/frontend/src/components/analytics-consent-form-modal.tsx index e122b9e8a9bf..b5ea03810f4c 100644 --- a/frontend/src/components/analytics-consent-form-modal.tsx +++ b/frontend/src/components/analytics-consent-form-modal.tsx @@ -1,4 +1,3 @@ -import { useFetcher } from "@remix-run/react"; import { ModalBackdrop } from "./modals/modal-backdrop"; import ModalBody from "./modals/ModalBody"; import ModalButton from "./buttons/ModalButton"; @@ -6,15 +5,31 @@ import { BaseModalTitle, BaseModalDescription, } from "./modals/confirmation-modals/BaseModal"; +import { handleCaptureConsent } from "#/utils/handle-capture-consent"; -export function AnalyticsConsentFormModal() { - const fetcher = useFetcher({ key: "set-consent" }); +interface AnalyticsConsentFormModalProps { + onClose: () => void; +} + +export function AnalyticsConsentFormModal({ + onClose, +}: AnalyticsConsentFormModalProps) { + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + const formData = new FormData(e.currentTarget); + const analytics = formData.get("analytics") === "on"; + + handleCaptureConsent(analytics); + localStorage.setItem("analytics-consent", analytics.toString()); + + onClose(); + }; return ( - @@ -36,7 +51,7 @@ export function AnalyticsConsentFormModal() { className="bg-primary text-white w-full hover:opacity-80" /> - + ); } diff --git a/frontend/src/components/chat-interface.tsx b/frontend/src/components/chat-interface.tsx index f0004bd749d4..b53c668c0c8f 100644 --- a/frontend/src/components/chat-interface.tsx +++ b/frontend/src/components/chat-interface.tsx @@ -1,7 +1,6 @@ import { useDispatch, useSelector } from "react-redux"; import React from "react"; import posthog from "posthog-js"; -import { useRouteLoaderData } from "@remix-run/react"; import { convertImageToBase64 } from "#/utils/convert-image-to-base-64"; import { ChatMessage } from "./chat-message"; import { FeedbackActions } from "./feedback-actions"; @@ -27,22 +26,22 @@ import { WsClientProviderStatus, } from "#/context/ws-client-provider"; import OpenHands from "#/api/open-hands"; -import { clientLoader } from "#/routes/_oh"; import { downloadWorkspace } from "#/utils/download-workspace"; import { SuggestionItem } from "./suggestion-item"; +import { useAuth } from "#/context/auth-context"; const isErrorMessage = ( message: Message | ErrorMessage, ): message is ErrorMessage => "error" in message; export function ChatInterface() { + const { gitHubToken } = useAuth(); const { send, status, isLoadingMessages } = useWsClient(); const dispatch = useDispatch(); const scrollRef = React.useRef(null); const { scrollDomToBottom, onChatBodyScroll, hitBottom } = useScrollToBottom(scrollRef); - const rootLoaderData = useRouteLoaderData("routes/_oh"); const { messages } = useSelector((state: RootState) => state.chat); const { curAgentState } = useSelector((state: RootState) => state.agent); @@ -175,7 +174,7 @@ export function ChatInterface() { {(curAgentState === AgentState.AWAITING_USER_INPUT || curAgentState === AgentState.FINISHED) && (

- {rootLoaderData?.ghToken ? ( + {gitHubToken ? ( void; @@ -19,22 +18,21 @@ export function Controls({ showSecurityLock, lastCommitData, }: ControlsProps) { - const rootData = useRouteLoaderData("routes/_oh"); - const appData = useRouteLoaderData("routes/_oh.app"); + const { gitHubToken } = useAuth(); + const { selectedRepository } = useSelector( + (state: RootState) => state.initalQuery, + ); const projectMenuCardData = React.useMemo( () => - rootData?.user && - !isGitHubErrorReponse(rootData.user) && - appData?.repo && - lastCommitData + selectedRepository && lastCommitData ? { - avatar: rootData.user.avatar_url, - repoName: appData.repo, + repoName: selectedRepository, lastCommit: lastCommitData, + avatar: null, // TODO: fetch repo avatar } : null, - [rootData, appData, lastCommitData], + [selectedRepository, lastCommitData], ); return ( @@ -55,7 +53,7 @@ export function Controls({
diff --git a/frontend/src/components/event-handler.tsx b/frontend/src/components/event-handler.tsx index 930bbafc840f..014035ed2998 100644 --- a/frontend/src/components/event-handler.tsx +++ b/frontend/src/components/event-handler.tsx @@ -1,12 +1,6 @@ import React from "react"; -import { - useFetcher, - useLoaderData, - useRouteLoaderData, -} from "@remix-run/react"; import { useDispatch, useSelector } from "react-redux"; import toast from "react-hot-toast"; - import posthog from "posthog-js"; import { useWsClient, @@ -24,17 +18,18 @@ import { clearSelectedRepository, setImportedProjectZip, } from "#/state/initial-query-slice"; -import { clientLoader as appClientLoader } from "#/routes/_oh.app"; import store, { RootState } from "#/store"; import { createChatMessage } from "#/services/chatService"; -import { clientLoader as rootClientLoader } from "#/routes/_oh"; import { isGitHubErrorReponse } from "#/api/github"; -import OpenHands from "#/api/open-hands"; import { base64ToBlob } from "#/utils/base64-to-blob"; import { setCurrentAgentState } from "#/state/agentSlice"; import AgentState from "#/types/AgentState"; -import { getSettings } from "#/services/settings"; import { generateAgentStateChangeEvent } from "#/services/agentStateService"; +import { useGitHubUser } from "#/hooks/query/use-github-user"; +import { useUploadFiles } from "#/hooks/mutation/use-upload-files"; +import { useAuth } from "#/context/auth-context"; +import { useEndSession } from "#/hooks/use-end-session"; +import { useUserPrefs } from "#/context/user-prefs-context"; interface ServerError { error: boolean | string; @@ -48,41 +43,48 @@ const isErrorObservation = (data: object): data is ErrorObservation => "observation" in data && data.observation === "error"; export function EventHandler({ children }: React.PropsWithChildren) { + const { setToken, gitHubToken } = useAuth(); + const { settings } = useUserPrefs(); const { events, status, send } = useWsClient(); const statusRef = React.useRef(null); const runtimeActive = status === WsClientProviderStatus.ACTIVE; - const fetcher = useFetcher(); const dispatch = useDispatch(); const { files, importedProjectZip, initialQuery } = useSelector( (state: RootState) => state.initalQuery, ); - const { ghToken, repo } = useLoaderData(); + const endSession = useEndSession(); + + // FIXME: Bad practice - should be handled with state + const { selectedRepository } = useSelector( + (state: RootState) => state.initalQuery, + ); + + const { data: user } = useGitHubUser(); + const { mutate: uploadFiles } = useUploadFiles(); const sendInitialQuery = (query: string, base64Files: string[]) => { const timestamp = new Date().toISOString(); send(createChatMessage(query, base64Files, timestamp)); }; - const data = useRouteLoaderData("routes/_oh"); const userId = React.useMemo(() => { - if (data?.user && !isGitHubErrorReponse(data.user)) return data.user.id; + if (user && !isGitHubErrorReponse(user)) return user.id; return null; - }, [data?.user]); - const userSettings = getSettings(); + }, [user]); React.useEffect(() => { if (!events.length) { return; } const event = events[events.length - 1]; - if (event.token) { - fetcher.submit({ token: event.token as string }, { method: "post" }); + if (event.token && typeof event.token === "string") { + setToken(event.token); return; } if (isServerError(event)) { if (event.error_code === 401) { toast.error("Session expired."); - fetcher.submit({}, { method: "POST", action: "/end-session" }); + endSession(); return; } @@ -120,9 +122,9 @@ export function EventHandler({ children }: React.PropsWithChildren) { if (status === WsClientProviderStatus.ACTIVE) { let additionalInfo = ""; - if (ghToken && repo) { - send(getCloneRepoCommand(ghToken, repo)); - additionalInfo = `Repository ${repo} has been cloned to /workspace. Please check the /workspace for files.`; + if (gitHubToken && selectedRepository) { + send(getCloneRepoCommand(gitHubToken, selectedRepository)); + additionalInfo = `Repository ${selectedRepository} has been cloned to /workspace. Please check the /workspace for files.`; dispatch(clearSelectedRepository()); // reset selected repository; maybe better to move this to '/'? } // if there's an uploaded project zip, add it to the chat @@ -157,35 +159,35 @@ export function EventHandler({ children }: React.PropsWithChildren) { }, [status]); React.useEffect(() => { - if (runtimeActive && userId && ghToken) { + if (runtimeActive && userId && gitHubToken) { // Export if the user valid, this could happen mid-session so it is handled here - send(getGitHubTokenCommand(ghToken)); + send(getGitHubTokenCommand(gitHubToken)); } - }, [userId, ghToken, runtimeActive]); + }, [userId, gitHubToken, runtimeActive]); React.useEffect(() => { - (async () => { - if (runtimeActive && importedProjectZip) { - // upload files action - try { - const blob = base64ToBlob(importedProjectZip); - const file = new File([blob], "imported-project.zip", { - type: blob.type, - }); - await OpenHands.uploadFiles([file]); - dispatch(setImportedProjectZip(null)); - } catch (error) { - toast.error("Failed to upload project files."); - } - } - })(); + if (runtimeActive && importedProjectZip) { + const blob = base64ToBlob(importedProjectZip); + const file = new File([blob], "imported-project.zip", { + type: blob.type, + }); + uploadFiles( + { files: [file] }, + { + onError: () => { + toast.error("Failed to upload project files."); + }, + }, + ); + dispatch(setImportedProjectZip(null)); + } }, [runtimeActive, importedProjectZip]); React.useEffect(() => { - if (userSettings.LLM_API_KEY) { + if (settings.LLM_API_KEY) { posthog.capture("user_activated"); } - }, [userSettings.LLM_API_KEY]); + }, [settings.LLM_API_KEY]); return children; } diff --git a/frontend/src/components/feedback-form.tsx b/frontend/src/components/feedback-form.tsx index 078e4b0ccca6..bc68de9bffc3 100644 --- a/frontend/src/components/feedback-form.tsx +++ b/frontend/src/components/feedback-form.tsx @@ -2,7 +2,7 @@ import React from "react"; import hotToast from "react-hot-toast"; import ModalButton from "./buttons/ModalButton"; import { Feedback } from "#/api/open-hands.types"; -import OpenHands from "#/api/open-hands"; +import { useSubmitFeedback } from "#/hooks/mutation/use-submit-feedback"; const FEEDBACK_VERSION = "1.0"; const VIEWER_PAGE = "https://www.all-hands.dev/share"; @@ -13,8 +13,6 @@ interface FeedbackFormProps { } export function FeedbackForm({ onClose, polarity }: FeedbackFormProps) { - const [isSubmitting, setIsSubmitting] = React.useState(false); - const copiedToClipboardToast = () => { hotToast("Password copied to clipboard", { icon: "📋", @@ -53,10 +51,11 @@ export function FeedbackForm({ onClose, polarity }: FeedbackFormProps) { ); }; + const { mutate: submitFeedback, isPending } = useSubmitFeedback(); + const handleSubmit = async (event: React.FormEvent) => { event?.preventDefault(); const formData = new FormData(event.currentTarget); - setIsSubmitting(true); const email = formData.get("email")?.toString() || ""; const permissions = (formData.get("permissions")?.toString() || @@ -71,11 +70,17 @@ export function FeedbackForm({ onClose, polarity }: FeedbackFormProps) { token: "", }; - const response = await OpenHands.submitFeedback(feedback); - const { message, feedback_id, password } = response.body; // eslint-disable-line - const link = `${VIEWER_PAGE}?share_id=${feedback_id}`; - shareFeedbackToast(message, link, password); - setIsSubmitting(false); + submitFeedback( + { feedback }, + { + onSuccess: (data) => { + const { message, feedback_id, password } = data.body; // eslint-disable-line + const link = `${VIEWER_PAGE}?share_id=${feedback_id}`; + shareFeedbackToast(message, link, password); + onClose(); + }, + }, + ); }; return ( @@ -109,13 +114,13 @@ export function FeedbackForm({ onClose, polarity }: FeedbackFormProps) {
void; @@ -95,13 +94,9 @@ function ExplorerActions({ interface FileExplorerProps { isOpen: boolean; onToggle: () => void; - error: string | null; } -function FileExplorer({ error, isOpen, onToggle }: FileExplorerProps) { - const { revalidate } = useRevalidator(); - - const { paths, setPaths } = useFiles(); +function FileExplorer({ isOpen, onToggle }: FileExplorerProps) { const [isDragging, setIsDragging] = React.useState(false); const { curAgentState } = useSelector((state: RootState) => state.agent); @@ -112,64 +107,59 @@ function FileExplorer({ error, isOpen, onToggle }: FileExplorerProps) { fileInputRef.current?.click(); // Trigger the file browser }; - const refreshWorkspace = () => { - if ( - curAgentState === AgentState.LOADING || - curAgentState === AgentState.STOPPED - ) { - return; - } - dispatch(setRefreshID(Math.random())); - OpenHands.getFiles().then(setPaths); - revalidate(); - }; + const { data: paths, refetch, error } = useListFiles(); - const uploadFileData = async (files: FileList) => { - try { - const result = await OpenHands.uploadFiles(Array.from(files)); + const handleUploadSuccess = (data: FileUploadSuccessResponse) => { + const uploadedCount = data.uploaded_files.length; + const skippedCount = data.skipped_files.length; - if (isOpenHandsErrorResponse(result)) { - // Handle error response - toast.error( - `upload-error-${new Date().getTime()}`, - result.error || t(I18nKey.EXPLORER$UPLOAD_ERROR_MESSAGE), - ); - return; - } + if (uploadedCount > 0) { + toast.success( + `upload-success-${new Date().getTime()}`, + t(I18nKey.EXPLORER$UPLOAD_SUCCESS_MESSAGE, { + count: uploadedCount, + }), + ); + } - const uploadedCount = result.uploaded_files.length; - const skippedCount = result.skipped_files.length; + if (skippedCount > 0) { + const message = t(I18nKey.EXPLORER$UPLOAD_PARTIAL_SUCCESS_MESSAGE, { + count: skippedCount, + }); + toast.info(message); + } - if (uploadedCount > 0) { - toast.success( - `upload-success-${new Date().getTime()}`, - t(I18nKey.EXPLORER$UPLOAD_SUCCESS_MESSAGE, { - count: uploadedCount, - }), - ); - } + if (uploadedCount === 0 && skippedCount === 0) { + toast.info(t(I18nKey.EXPLORER$NO_FILES_UPLOADED_MESSAGE)); + } + }; - if (skippedCount > 0) { - const message = t(I18nKey.EXPLORER$UPLOAD_PARTIAL_SUCCESS_MESSAGE, { - count: skippedCount, - }); - toast.info(message); - } + const handleUploadError = (e: Error) => { + toast.error( + `upload-error-${new Date().getTime()}`, + e.message || t(I18nKey.EXPLORER$UPLOAD_ERROR_MESSAGE), + ); + }; - if (uploadedCount === 0 && skippedCount === 0) { - toast.info(t(I18nKey.EXPLORER$NO_FILES_UPLOADED_MESSAGE)); - } + const { mutate: uploadFiles } = useUploadFiles(); - refreshWorkspace(); - } catch (e) { - // Handle unexpected errors (network issues, etc.) - toast.error( - `upload-error-${new Date().getTime()}`, - t(I18nKey.EXPLORER$UPLOAD_ERROR_MESSAGE), - ); + const refreshWorkspace = () => { + if ( + curAgentState !== AgentState.LOADING && + curAgentState !== AgentState.STOPPED + ) { + refetch(); } }; + const uploadFileData = (files: FileList) => { + uploadFiles( + { files: Array.from(files) }, + { onSuccess: handleUploadSuccess, onError: handleUploadError }, + ); + refreshWorkspace(); + }; + const handleVSCodeClick = async (e: React.MouseEvent) => { e.preventDefault(); try { @@ -265,13 +255,13 @@ function FileExplorer({ error, isOpen, onToggle }: FileExplorerProps) { {!error && (
- +
)} {error && (
-

{error}

+

{error.message}

)} {isOpen && ( diff --git a/frontend/src/components/file-explorer/TreeNode.tsx b/frontend/src/components/file-explorer/TreeNode.tsx index b3aa3c28335c..d65eb07148ad 100644 --- a/frontend/src/components/file-explorer/TreeNode.tsx +++ b/frontend/src/components/file-explorer/TreeNode.tsx @@ -1,12 +1,10 @@ import React from "react"; -import { useSelector } from "react-redux"; -import toast from "react-hot-toast"; -import { RootState } from "#/store"; import FolderIcon from "../FolderIcon"; import FileIcon from "../FileIcons"; -import OpenHands from "#/api/open-hands"; import { useFiles } from "#/context/files"; import { cn } from "#/utils/utils"; +import { useListFiles } from "#/hooks/query/use-list-files"; +import { useListFile } from "#/hooks/query/use-list-file"; interface TitleProps { name: string; @@ -44,51 +42,35 @@ function TreeNode({ path, defaultOpen = false }: TreeNodeProps) { selectedPath, } = useFiles(); const [isOpen, setIsOpen] = React.useState(defaultOpen); - const [children, setChildren] = React.useState(null); - const refreshID = useSelector((state: RootState) => state.code.refreshID); - - const fileParts = path.split("/"); - const filename = - fileParts[fileParts.length - 1] || fileParts[fileParts.length - 2]; const isDirectory = path.endsWith("/"); - const refreshChildren = async () => { - if (!isDirectory || !isOpen) { - setChildren(null); - return; - } + const { data: paths } = useListFiles({ + path, + enabled: isDirectory && isOpen, + }); - try { - const newChildren = await OpenHands.getFiles(path); - setChildren(newChildren); - } catch (error) { - toast.error("Failed to fetch files"); - } - }; + const { data: fileContent, refetch } = useListFile({ path }); React.useEffect(() => { - (async () => { - await refreshChildren(); - })(); - }, [refreshID, isOpen]); - - const handleClick = async () => { - if (isDirectory) { - setIsOpen((prev) => !prev); - } else { + if (fileContent) { const code = modifiedFiles[path] || files[path]; - - try { - const fetchedCode = await OpenHands.getFile(path); - setSelectedPath(path); - if (!code || fetchedCode !== files[path]) { - setFileContent(path, fetchedCode); - } - } catch (error) { - toast.error("Failed to fetch file"); + if (!code || fileContent !== files[path]) { + setFileContent(path, fileContent); } } + }, [fileContent, path]); + + const fileParts = path.split("/"); + const filename = + fileParts[fileParts.length - 1] || fileParts[fileParts.length - 2]; + + const handleClick = async () => { + if (isDirectory) setIsOpen((prev) => !prev); + else { + setSelectedPath(path); + await refetch(); + } }; return ( @@ -116,9 +98,9 @@ function TreeNode({ path, defaultOpen = false }: TreeNodeProps) { )} - {isOpen && children && ( + {isOpen && paths && (
- {children.map((child, index) => ( + {paths.map((child, index) => ( ))}
diff --git a/frontend/src/components/form/settings-form.tsx b/frontend/src/components/form/settings-form.tsx index 02a042e8a5c4..ec235a2a780e 100644 --- a/frontend/src/components/form/settings-form.tsx +++ b/frontend/src/components/form/settings-form.tsx @@ -4,19 +4,26 @@ import { Input, Switch, } from "@nextui-org/react"; -import { useFetcher, useLocation, useNavigate } from "@remix-run/react"; +import { useLocation } from "@remix-run/react"; import { useTranslation } from "react-i18next"; import clsx from "clsx"; import React from "react"; +import posthog from "posthog-js"; import { organizeModelsAndProviders } from "#/utils/organizeModelsAndProviders"; import { ModelSelector } from "#/components/modals/settings/ModelSelector"; -import { Settings } from "#/services/settings"; +import { getDefaultSettings, Settings } from "#/services/settings"; import { ModalBackdrop } from "#/components/modals/modal-backdrop"; -import { clientAction } from "#/routes/settings"; import { extractModelAndProvider } from "#/utils/extractModelAndProvider"; import ModalButton from "../buttons/ModalButton"; import { DangerModal } from "../modals/confirmation-modals/danger-modal"; import { I18nKey } from "#/i18n/declaration"; +import { + extractSettings, + saveSettingsView, + updateSettingsVersion, +} from "#/utils/settings-utils"; +import { useEndSession } from "#/hooks/use-end-session"; +import { useUserPrefs } from "#/context/user-prefs-context"; interface SettingsFormProps { disabled?: boolean; @@ -35,19 +42,36 @@ export function SettingsForm({ securityAnalyzers, onClose, }: SettingsFormProps) { + const { saveSettings } = useUserPrefs(); + const endSession = useEndSession(); + const location = useLocation(); - const navigate = useNavigate(); const { t } = useTranslation(); - const fetcher = useFetcher(); const formRef = React.useRef(null); - React.useEffect(() => { - if (fetcher.data?.success) { - navigate("/"); + const resetOngoingSession = () => { + if (location.pathname.startsWith("/app")) { + endSession(); onClose(); } - }, [fetcher.data, navigate, onClose]); + }; + + const handleFormSubmission = (formData: FormData) => { + const keys = Array.from(formData.keys()); + const isUsingAdvancedOptions = keys.includes("use-advanced-options"); + const newSettings = extractSettings(formData); + + saveSettings(newSettings); + saveSettingsView(isUsingAdvancedOptions ? "advanced" : "basic"); + updateSettingsVersion(); + resetOngoingSession(); + + posthog.capture("settings_saved", { + LLM_MODEL: newSettings.LLM_MODEL, + LLM_API_KEY: newSettings.LLM_API_KEY ? "SET" : "UNSET", + }); + }; const advancedAlreadyInUse = React.useMemo(() => { if (models.length > 0) { @@ -83,20 +107,17 @@ export function SettingsForm({ React.useState(false); const [showWarningModal, setShowWarningModal] = React.useState(false); - const submitForm = (formData: FormData) => { - if (location.pathname === "/app") formData.set("end-session", "true"); - fetcher.submit(formData, { method: "POST", action: "/settings" }); - }; - const handleConfirmResetSettings = () => { - const formData = new FormData(formRef.current ?? undefined); - formData.set("intent", "reset"); - submitForm(formData); + saveSettings(getDefaultSettings()); + resetOngoingSession(); + posthog.capture("settings_reset"); + + onClose(); }; const handleConfirmEndSession = () => { const formData = new FormData(formRef.current ?? undefined); - submitForm(formData); + handleFormSubmission(formData); }; const handleSubmit = (event: React.FormEvent) => { @@ -106,10 +127,11 @@ export function SettingsForm({ if (!apiKey) { setShowWarningModal(true); - } else if (location.pathname === "/app") { + } else if (location.pathname.startsWith("/app")) { setConfirmEndSessionModalOpen(true); } else { - submitForm(formData); + handleFormSubmission(formData); + onClose(); } }; @@ -117,18 +139,15 @@ export function SettingsForm({ const formData = new FormData(formRef.current ?? undefined); const apiKey = formData.get("api-key"); - if (!apiKey) { - setShowWarningModal(true); - } else { - onClose(); - } + if (!apiKey) setShowWarningModal(true); + else onClose(); }; const handleWarningConfirm = () => { setShowWarningModal(false); const formData = new FormData(formRef.current ?? undefined); formData.set("api-key", ""); // Set null value for API key - submitForm(formData); + handleFormSubmission(formData); onClose(); }; @@ -138,11 +157,9 @@ export function SettingsForm({ return (
- @@ -267,9 +284,7 @@ export function SettingsForm({ aria-label="Agent" data-testid="agent-input" name="agent" - defaultSelectedKey={ - fetcher.formData?.get("agent")?.toString() ?? settings.AGENT - } + defaultSelectedKey={settings.AGENT} isClearable={false} inputProps={{ classNames: { @@ -302,10 +317,7 @@ export function SettingsForm({ id="security-analyzer" name="security-analyzer" aria-label="Security Analyzer" - defaultSelectedKey={ - fetcher.formData?.get("security-analyzer")?.toString() ?? - settings.SECURITY_ANALYZER - } + defaultSelectedKey={settings.SECURITY_ANALYZER} inputProps={{ classNames: { inputWrapper: @@ -346,7 +358,7 @@ export function SettingsForm({
- + {confirmResetDefaultsModalOpen && ( diff --git a/frontend/src/components/github-repositories-suggestion-box.tsx b/frontend/src/components/github-repositories-suggestion-box.tsx index 4886513dd487..b00a48c65749 100644 --- a/frontend/src/components/github-repositories-suggestion-box.tsx +++ b/frontend/src/components/github-repositories-suggestion-box.tsx @@ -1,8 +1,5 @@ import React from "react"; -import { - isGitHubErrorReponse, - retrieveAllGitHubUserRepositories, -} from "#/api/github"; +import { isGitHubErrorReponse } from "#/api/github"; import { SuggestionBox } from "#/routes/_oh._index/suggestion-box"; import { ConnectToGitHubModal } from "./modals/connect-to-github-modal"; import { ModalBackdrop } from "./modals/modal-backdrop"; @@ -12,9 +9,7 @@ import GitHubLogo from "#/assets/branding/github-logo.svg?react"; interface GitHubRepositoriesSuggestionBoxProps { handleSubmit: () => void; - repositories: Awaited< - ReturnType - > | null; + repositories: GitHubRepository[]; gitHubAuthUrl: string | null; user: GitHubErrorReponse | GitHubUser | null; } @@ -57,7 +52,7 @@ export function GitHubRepositoriesSuggestionBox({ isLoggedIn ? ( ) : ( void; @@ -28,41 +27,33 @@ function AccountSettingsModal({ gitHubError, analyticsConsent, }: AccountSettingsModalProps) { + const { gitHubToken, setGitHubToken, logout } = useAuth(); + const { saveSettings } = useUserPrefs(); const { t } = useTranslation(); - const data = useRouteLoaderData("routes/_oh"); - const settingsFetcher = useFetcher({ - key: "settings", - }); - const loginFetcher = useFetcher({ key: "login" }); const handleSubmit = (event: React.FormEvent) => { event.preventDefault(); const formData = new FormData(event.currentTarget); - const language = formData.get("language")?.toString(); + const ghToken = formData.get("ghToken")?.toString(); + const language = formData.get("language")?.toString(); const analytics = formData.get("analytics")?.toString() === "on"; - const accountForm = new FormData(); - const loginForm = new FormData(); + if (ghToken) setGitHubToken(ghToken); - accountForm.append("intent", "account"); + // The form returns the language label, so we need to find the corresponding + // language key to save it in the settings if (language) { const languageKey = AvailableLanguages.find( ({ label }) => label === language, )?.value; - accountForm.append("language", languageKey ?? "en"); + + if (languageKey) saveSettings({ LANGUAGE: languageKey }); } - if (ghToken) loginForm.append("ghToken", ghToken); - accountForm.append("analytics", analytics.toString()); - settingsFetcher.submit(accountForm, { - method: "POST", - action: "/settings", - }); - loginFetcher.submit(loginForm, { - method: "POST", - action: "/login", - }); + handleCaptureConsent(analytics); + const ANALYTICS = analytics.toString(); + localStorage.setItem("analytics-consent", ANALYTICS); onClose(); }; @@ -88,7 +79,7 @@ function AccountSettingsModal({ name="ghToken" label="GitHub Token" type="password" - defaultValue={data?.ghToken ?? ""} + defaultValue={gitHubToken ?? ""} /> {t(I18nKey.CONNECT_TO_GITHUB_MODAL$GET_YOUR_TOKEN)}{" "} @@ -106,15 +97,12 @@ function AccountSettingsModal({ {t(I18nKey.ACCOUNT_SETTINGS_MODAL$GITHUB_TOKEN_INVALID)}

)} - {data?.ghToken && !gitHubError && ( + {gitHubToken && !gitHubError && ( { - settingsFetcher.submit( - {}, - { method: "POST", action: "/logout" }, - ); + logout(); onClose(); }} className="text-danger self-start" @@ -133,10 +121,6 @@ function AccountSettingsModal({
-
- - - -
-
- - - - - - ); -} - -export default ConnectToGitHubByTokenModal; diff --git a/frontend/src/components/modals/connect-to-github-modal.tsx b/frontend/src/components/modals/connect-to-github-modal.tsx index 19cc4ac36ff4..bd0e6b764bef 100644 --- a/frontend/src/components/modals/connect-to-github-modal.tsx +++ b/frontend/src/components/modals/connect-to-github-modal.tsx @@ -1,4 +1,3 @@ -import { useFetcher, useRouteLoaderData } from "@remix-run/react"; import { useTranslation } from "react-i18next"; import ModalBody from "./ModalBody"; import { CustomInput } from "../form/custom-input"; @@ -7,19 +6,26 @@ import { BaseModalDescription, BaseModalTitle, } from "./confirmation-modals/BaseModal"; -import { clientLoader } from "#/routes/_oh"; -import { clientAction } from "#/routes/login"; import { I18nKey } from "#/i18n/declaration"; +import { useAuth } from "#/context/auth-context"; interface ConnectToGitHubModalProps { onClose: () => void; } export function ConnectToGitHubModal({ onClose }: ConnectToGitHubModalProps) { - const data = useRouteLoaderData("routes/_oh"); - const fetcher = useFetcher({ key: "login" }); + const { gitHubToken, setGitHubToken } = useAuth(); const { t } = useTranslation(); + const handleSubmit = (event: React.FormEvent) => { + event.preventDefault(); + const formData = new FormData(event.currentTarget); + const ghToken = formData.get("ghToken")?.toString(); + + if (ghToken) setGitHubToken(ghToken); + onClose(); + }; + return (
@@ -40,18 +46,13 @@ export function ConnectToGitHubModal({ onClose }: ConnectToGitHubModalProps) { } />
- +
@@ -59,7 +60,6 @@ export function ConnectToGitHubModal({ onClose }: ConnectToGitHubModalProps) { testId="connect-to-github" type="submit" text={t(I18nKey.CONNECT_TO_GITHUB_MODAL$CONNECT)} - disabled={fetcher.state === "submitting"} className="bg-[#791B80] w-full" />
- +
); } diff --git a/frontend/src/components/project-menu/ProjectMenuCard.tsx b/frontend/src/components/project-menu/ProjectMenuCard.tsx index 1a32c2f802d1..a840732cea04 100644 --- a/frontend/src/components/project-menu/ProjectMenuCard.tsx +++ b/frontend/src/components/project-menu/ProjectMenuCard.tsx @@ -17,7 +17,7 @@ import { useWsClient } from "#/context/ws-client-provider"; interface ProjectMenuCardProps { isConnectedToGitHub: boolean; githubData: { - avatar: string; + avatar: string | null; repoName: string; lastCommit: GitHubCommit; } | null; diff --git a/frontend/src/components/project-menu/project-menu-details.tsx b/frontend/src/components/project-menu/project-menu-details.tsx index 6b5382a43689..8bb67a2ec8ba 100644 --- a/frontend/src/components/project-menu/project-menu-details.tsx +++ b/frontend/src/components/project-menu/project-menu-details.tsx @@ -5,7 +5,7 @@ import { I18nKey } from "#/i18n/declaration"; interface ProjectMenuDetailsProps { repoName: string; - avatar: string; + avatar: string | null; lastCommit: GitHubCommit; } @@ -23,7 +23,7 @@ export function ProjectMenuDetails({ rel="noreferrer noopener" className="flex items-center gap-2" > - + {avatar && } {repoName}
diff --git a/frontend/src/components/user-actions.tsx b/frontend/src/components/user-actions.tsx index d605cb895d96..c3bfc4bd02e4 100644 --- a/frontend/src/components/user-actions.tsx +++ b/frontend/src/components/user-actions.tsx @@ -1,5 +1,4 @@ import React from "react"; -import { useFetcher } from "@remix-run/react"; import { AccountSettingsContextMenu } from "./context-menu/account-settings-context-menu"; import { UserAvatar } from "./user-avatar"; @@ -14,8 +13,6 @@ export function UserActions({ onLogout, user, }: UserActionsProps) { - const loginFetcher = useFetcher({ key: "login" }); - const [accountContextMenuIsVisible, setAccountContextMenuIsVisible] = React.useState(false); @@ -39,11 +36,7 @@ export function UserActions({ return (
- + {accountContextMenuIsVisible && ( void; + setGitHubToken: (token: string | null) => void; + clearToken: () => void; + clearGitHubToken: () => void; + logout: () => void; +} + +const AuthContext = React.createContext(undefined); + +function AuthProvider({ children }: React.PropsWithChildren) { + const [tokenState, setTokenState] = React.useState(() => + localStorage.getItem("token"), + ); + const [gitHubTokenState, setGitHubTokenState] = React.useState( + () => localStorage.getItem("ghToken"), + ); + + React.useLayoutEffect(() => { + setTokenState(localStorage.getItem("token")); + setGitHubTokenState(localStorage.getItem("ghToken")); + }); + + const setToken = (token: string | null) => { + setTokenState(token); + + if (token) localStorage.setItem("token", token); + else localStorage.removeItem("token"); + }; + + const setGitHubToken = (token: string | null) => { + setGitHubTokenState(token); + + if (token) localStorage.setItem("ghToken", token); + else localStorage.removeItem("ghToken"); + }; + + const clearToken = () => { + setTokenState(null); + localStorage.removeItem("token"); + }; + + const clearGitHubToken = () => { + setGitHubTokenState(null); + localStorage.removeItem("ghToken"); + }; + + const logout = () => { + clearGitHubToken(); + posthog.reset(); + }; + + const value = React.useMemo( + () => ({ + token: tokenState, + gitHubToken: gitHubTokenState, + setToken, + setGitHubToken, + clearToken, + clearGitHubToken, + logout, + }), + [tokenState, gitHubTokenState], + ); + + return {children}; +} + +function useAuth() { + const context = React.useContext(AuthContext); + if (context === undefined) { + throw new Error("useAuth must be used within a AuthProvider"); + } + return context; +} + +export { AuthProvider, useAuth }; diff --git a/frontend/src/context/user-prefs-context.tsx b/frontend/src/context/user-prefs-context.tsx new file mode 100644 index 000000000000..e3573c9234c0 --- /dev/null +++ b/frontend/src/context/user-prefs-context.tsx @@ -0,0 +1,55 @@ +import React from "react"; +import { + getSettings, + Settings, + saveSettings as updateAndSaveSettingsToLocalStorage, + settingsAreUpToDate as checkIfSettingsAreUpToDate, +} from "#/services/settings"; + +interface UserPrefsContextType { + settings: Settings; + settingsAreUpToDate: boolean; + saveSettings: (settings: Partial) => void; +} + +const UserPrefsContext = React.createContext( + undefined, +); + +function UserPrefsProvider({ children }: React.PropsWithChildren) { + const [settings, setSettings] = React.useState(getSettings()); + const [settingsAreUpToDate, setSettingsAreUpToDate] = React.useState( + checkIfSettingsAreUpToDate(), + ); + + const saveSettings = (newSettings: Partial) => { + updateAndSaveSettingsToLocalStorage(newSettings); + setSettings(getSettings()); + setSettingsAreUpToDate(checkIfSettingsAreUpToDate()); + }; + + const value = React.useMemo( + () => ({ + settings, + settingsAreUpToDate, + saveSettings, + }), + [settings, settingsAreUpToDate], + ); + + return ( + + {children} + + ); +} + +function useUserPrefs() { + const context = React.useContext(UserPrefsContext); + if (context === undefined) { + throw new Error("useUserPrefs must be used within a UserPrefsProvider"); + } + return context; +} + +export { UserPrefsProvider, useUserPrefs }; diff --git a/frontend/src/entry.client.tsx b/frontend/src/entry.client.tsx index cb8f3d16f0a9..875daf565d4c 100644 --- a/frontend/src/entry.client.tsx +++ b/frontend/src/entry.client.tsx @@ -11,26 +11,23 @@ import { hydrateRoot } from "react-dom/client"; import { Provider } from "react-redux"; import posthog from "posthog-js"; import "./i18n"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import store from "./store"; -import OpenHands from "./api/open-hands"; +import { useConfig } from "./hooks/query/use-config"; +import { AuthProvider } from "./context/auth-context"; +import { UserPrefsProvider } from "./context/user-prefs-context"; function PosthogInit() { - const [key, setKey] = React.useState(null); + const { data: config } = useConfig(); React.useEffect(() => { - OpenHands.getConfig().then((config) => { - setKey(config.POSTHOG_CLIENT_KEY); - }); - }, []); - - React.useEffect(() => { - if (key) { - posthog.init(key, { + if (config?.POSTHOG_CLIENT_KEY) { + posthog.init(config.POSTHOG_CLIENT_KEY, { api_host: "https://us.i.posthog.com", person_profiles: "identified_only", }); } - }, [key]); + }, [config]); return null; } @@ -48,14 +45,22 @@ async function prepareApp() { } } +const queryClient = new QueryClient(); + prepareApp().then(() => startTransition(() => { hydrateRoot( document, - - + + + + + + + + , ); diff --git a/frontend/src/hooks/mutation/use-save-file.ts b/frontend/src/hooks/mutation/use-save-file.ts new file mode 100644 index 000000000000..30edcda21a15 --- /dev/null +++ b/frontend/src/hooks/mutation/use-save-file.ts @@ -0,0 +1,21 @@ +import { useMutation } from "@tanstack/react-query"; +import toast from "react-hot-toast"; +import OpenHands from "#/api/open-hands"; +import { useAuth } from "#/context/auth-context"; + +type SaveFileArgs = { + path: string; + content: string; +}; + +export const useSaveFile = () => { + const { token } = useAuth(); + + return useMutation({ + mutationFn: ({ path, content }: SaveFileArgs) => + OpenHands.saveFile(token || "", path, content), + onError: (error) => { + toast.error(error.message); + }, + }); +}; diff --git a/frontend/src/hooks/mutation/use-submit-feedback.ts b/frontend/src/hooks/mutation/use-submit-feedback.ts new file mode 100644 index 000000000000..0253b69d559e --- /dev/null +++ b/frontend/src/hooks/mutation/use-submit-feedback.ts @@ -0,0 +1,21 @@ +import { useMutation } from "@tanstack/react-query"; +import toast from "react-hot-toast"; +import { Feedback } from "#/api/open-hands.types"; +import OpenHands from "#/api/open-hands"; +import { useAuth } from "#/context/auth-context"; + +type SubmitFeedbackArgs = { + feedback: Feedback; +}; + +export const useSubmitFeedback = () => { + const { token } = useAuth(); + + return useMutation({ + mutationFn: ({ feedback }: SubmitFeedbackArgs) => + OpenHands.submitFeedback(token || "", feedback), + onError: (error) => { + toast.error(error.message); + }, + }); +}; diff --git a/frontend/src/hooks/mutation/use-upload-files.ts b/frontend/src/hooks/mutation/use-upload-files.ts new file mode 100644 index 000000000000..0f7a31ed3811 --- /dev/null +++ b/frontend/src/hooks/mutation/use-upload-files.ts @@ -0,0 +1,16 @@ +import { useMutation } from "@tanstack/react-query"; +import OpenHands from "#/api/open-hands"; +import { useAuth } from "#/context/auth-context"; + +type UploadFilesArgs = { + files: File[]; +}; + +export const useUploadFiles = () => { + const { token } = useAuth(); + + return useMutation({ + mutationFn: ({ files }: UploadFilesArgs) => + OpenHands.uploadFiles(token || "", files), + }); +}; diff --git a/frontend/src/hooks/query/use-ai-config-options.ts b/frontend/src/hooks/query/use-ai-config-options.ts new file mode 100644 index 000000000000..9e63cf6a8275 --- /dev/null +++ b/frontend/src/hooks/query/use-ai-config-options.ts @@ -0,0 +1,14 @@ +import { useQuery } from "@tanstack/react-query"; +import OpenHands from "#/api/open-hands"; + +const fetchAiConfigOptions = async () => ({ + models: await OpenHands.getModels(), + agents: await OpenHands.getAgents(), + securityAnalyzers: await OpenHands.getSecurityAnalyzers(), +}); + +export const useAIConfigOptions = () => + useQuery({ + queryKey: ["ai-config-options"], + queryFn: fetchAiConfigOptions, + }); diff --git a/frontend/src/hooks/query/use-config.ts b/frontend/src/hooks/query/use-config.ts new file mode 100644 index 000000000000..8b81af13b537 --- /dev/null +++ b/frontend/src/hooks/query/use-config.ts @@ -0,0 +1,8 @@ +import { useQuery } from "@tanstack/react-query"; +import OpenHands from "#/api/open-hands"; + +export const useConfig = () => + useQuery({ + queryKey: ["config"], + queryFn: OpenHands.getConfig, + }); diff --git a/frontend/src/hooks/query/use-github-user.ts b/frontend/src/hooks/query/use-github-user.ts new file mode 100644 index 000000000000..ac0de4acce63 --- /dev/null +++ b/frontend/src/hooks/query/use-github-user.ts @@ -0,0 +1,40 @@ +import { useQuery } from "@tanstack/react-query"; +import React from "react"; +import posthog from "posthog-js"; +import { retrieveGitHubUser, isGitHubErrorReponse } from "#/api/github"; +import { useAuth } from "#/context/auth-context"; +import { useConfig } from "./use-config"; + +export const useGitHubUser = () => { + const { gitHubToken } = useAuth(); + const { data: config } = useConfig(); + + const user = useQuery({ + queryKey: ["user", gitHubToken], + queryFn: async () => { + const data = await retrieveGitHubUser(gitHubToken!); + + if (isGitHubErrorReponse(data)) { + throw new Error("Failed to retrieve user data"); + } + + return data; + }, + enabled: !!gitHubToken && !!config?.APP_MODE, + retry: false, + }); + + React.useEffect(() => { + if (user.data) { + posthog.identify(user.data.login, { + company: user.data.company, + name: user.data.name, + email: user.data.email, + user: user.data.login, + mode: config?.APP_MODE || "oss", + }); + } + }, [user.data]); + + return user; +}; diff --git a/frontend/src/hooks/query/use-is-authed.ts b/frontend/src/hooks/query/use-is-authed.ts new file mode 100644 index 000000000000..9f6971b754b8 --- /dev/null +++ b/frontend/src/hooks/query/use-is-authed.ts @@ -0,0 +1,19 @@ +import { useQuery } from "@tanstack/react-query"; +import React from "react"; +import OpenHands from "#/api/open-hands"; +import { useConfig } from "./use-config"; +import { useAuth } from "#/context/auth-context"; + +export const useIsAuthed = () => { + const { gitHubToken } = useAuth(); + const { data: config } = useConfig(); + + const appMode = React.useMemo(() => config?.APP_MODE, [config]); + + return useQuery({ + queryKey: ["user", "authenticated", gitHubToken, appMode], + queryFn: () => OpenHands.authenticate(gitHubToken || "", appMode!), + enabled: !!appMode, + staleTime: 1000 * 60 * 5, // 5 minutes + }); +}; diff --git a/frontend/src/hooks/query/use-latest-repo-commit.ts b/frontend/src/hooks/query/use-latest-repo-commit.ts new file mode 100644 index 000000000000..3ead53c6c57d --- /dev/null +++ b/frontend/src/hooks/query/use-latest-repo-commit.ts @@ -0,0 +1,28 @@ +import { useQuery } from "@tanstack/react-query"; +import { retrieveLatestGitHubCommit, isGitHubErrorReponse } from "#/api/github"; +import { useAuth } from "#/context/auth-context"; + +interface UseLatestRepoCommitConfig { + repository: string | null; +} + +export const useLatestRepoCommit = (config: UseLatestRepoCommitConfig) => { + const { gitHubToken } = useAuth(); + + return useQuery({ + queryKey: ["latest_commit", gitHubToken, config.repository], + queryFn: async () => { + const data = await retrieveLatestGitHubCommit( + gitHubToken!, + config.repository!, + ); + + if (isGitHubErrorReponse(data)) { + throw new Error("Failed to retrieve latest commit"); + } + + return data[0]; + }, + enabled: !!gitHubToken && !!config.repository, + }); +}; diff --git a/frontend/src/hooks/query/use-list-file.ts b/frontend/src/hooks/query/use-list-file.ts new file mode 100644 index 000000000000..074bf6b72963 --- /dev/null +++ b/frontend/src/hooks/query/use-list-file.ts @@ -0,0 +1,17 @@ +import { useQuery } from "@tanstack/react-query"; +import OpenHands from "#/api/open-hands"; +import { useAuth } from "#/context/auth-context"; + +interface UseListFileConfig { + path: string; +} + +export const useListFile = (config: UseListFileConfig) => { + const { token } = useAuth(); + + return useQuery({ + queryKey: ["file", token, config.path], + queryFn: () => OpenHands.getFile(token || "", config.path), + enabled: false, // don't fetch by default, trigger manually via `refetch` + }); +}; diff --git a/frontend/src/hooks/query/use-list-files.ts b/frontend/src/hooks/query/use-list-files.ts new file mode 100644 index 000000000000..7baa395fd7be --- /dev/null +++ b/frontend/src/hooks/query/use-list-files.ts @@ -0,0 +1,24 @@ +import { useQuery } from "@tanstack/react-query"; +import { + useWsClient, + WsClientProviderStatus, +} from "#/context/ws-client-provider"; +import OpenHands from "#/api/open-hands"; +import { useAuth } from "#/context/auth-context"; + +interface UseListFilesConfig { + path?: string; + enabled?: boolean; +} + +export const useListFiles = (config?: UseListFilesConfig) => { + const { token } = useAuth(); + const { status } = useWsClient(); + const isActive = status === WsClientProviderStatus.ACTIVE; + + return useQuery({ + queryKey: ["files", token, config?.path], + queryFn: () => OpenHands.getFiles(token!, config?.path), + enabled: isActive && config?.enabled && !!token, + }); +}; diff --git a/frontend/src/hooks/query/use-user-repositories.ts b/frontend/src/hooks/query/use-user-repositories.ts new file mode 100644 index 000000000000..8b97d6bcd7d8 --- /dev/null +++ b/frontend/src/hooks/query/use-user-repositories.ts @@ -0,0 +1,63 @@ +import { useInfiniteQuery } from "@tanstack/react-query"; +import React from "react"; +import { + isGitHubErrorReponse, + retrieveGitHubUserRepositories, +} from "#/api/github"; +import { extractNextPageFromLink } from "#/utils/extract-next-page-from-link"; +import { useAuth } from "#/context/auth-context"; + +interface UserRepositoriesQueryFnProps { + pageParam: number; + ghToken: string; +} + +const userRepositoriesQueryFn = async ({ + pageParam, + ghToken, +}: UserRepositoriesQueryFnProps) => { + const response = await retrieveGitHubUserRepositories( + ghToken, + pageParam, + 100, + ); + + if (!response.ok) { + throw new Error("Failed to fetch repositories"); + } + + const data = (await response.json()) as GitHubRepository | GitHubErrorReponse; + + if (isGitHubErrorReponse(data)) { + throw new Error(data.message); + } + + const link = response.headers.get("link") ?? ""; + const nextPage = extractNextPageFromLink(link); + + return { data, nextPage }; +}; + +export const useUserRepositories = () => { + const { gitHubToken } = useAuth(); + + const repos = useInfiniteQuery({ + queryKey: ["repositories", gitHubToken], + queryFn: async ({ pageParam }) => + userRepositoriesQueryFn({ pageParam, ghToken: gitHubToken! }), + initialPageParam: 1, + getNextPageParam: (lastPage) => lastPage.nextPage, + enabled: !!gitHubToken, + }); + + // TODO: Once we create our custom dropdown component, we should fetch data onEndReached + // (nextui autocomplete doesn't support onEndReached nor is it compatible for extending) + const { isSuccess, isFetchingNextPage, hasNextPage, fetchNextPage } = repos; + React.useEffect(() => { + if (!isFetchingNextPage && isSuccess && hasNextPage) { + fetchNextPage(); + } + }, [isFetchingNextPage, isSuccess, hasNextPage, fetchNextPage]); + + return repos; +}; diff --git a/frontend/src/hooks/use-end-session.ts b/frontend/src/hooks/use-end-session.ts new file mode 100644 index 000000000000..602bcfa6779e --- /dev/null +++ b/frontend/src/hooks/use-end-session.ts @@ -0,0 +1,31 @@ +import { useDispatch } from "react-redux"; +import { useNavigate } from "@remix-run/react"; +import { useAuth } from "#/context/auth-context"; +import { + initialState as browserInitialState, + setScreenshotSrc, + setUrl, +} from "#/state/browserSlice"; +import { clearSelectedRepository } from "#/state/initial-query-slice"; + +export const useEndSession = () => { + const navigate = useNavigate(); + const dispatch = useDispatch(); + const { clearToken } = useAuth(); + + /** + * End the current session by clearing the token and redirecting to the home page. + */ + const endSession = () => { + clearToken(); + dispatch(clearSelectedRepository()); + + // Reset browser state to initial values + dispatch(setUrl(browserInitialState.url)); + dispatch(setScreenshotSrc(browserInitialState.screenshotSrc)); + + navigate("/"); + }; + + return endSession; +}; diff --git a/frontend/src/hooks/use-github-auth-url.ts b/frontend/src/hooks/use-github-auth-url.ts new file mode 100644 index 000000000000..e9d493764c0e --- /dev/null +++ b/frontend/src/hooks/use-github-auth-url.ts @@ -0,0 +1,20 @@ +import React from "react"; +import { generateGitHubAuthUrl } from "#/utils/generate-github-auth-url"; +import { GetConfigResponse } from "#/api/open-hands.types"; + +interface UseGitHubAuthUrlConfig { + gitHubToken: string | null; + appMode: GetConfigResponse["APP_MODE"] | null; + gitHubClientId: GetConfigResponse["GITHUB_CLIENT_ID"] | null; +} + +export const useGitHubAuthUrl = (config: UseGitHubAuthUrlConfig) => + React.useMemo(() => { + if (config.appMode === "saas" && !config.gitHubToken) + return generateGitHubAuthUrl( + config.gitHubClientId || "", + new URL(window.location.href), + ); + + return null; + }, [config.gitHubToken, config.appMode, config.gitHubClientId]); diff --git a/frontend/src/routes/_oh._index/route.tsx b/frontend/src/routes/_oh._index/route.tsx index d73ee7fd455d..10e47ba0f57a 100644 --- a/frontend/src/routes/_oh._index/route.tsx +++ b/frontend/src/routes/_oh._index/route.tsx @@ -1,81 +1,40 @@ -import { - Await, - ClientActionFunctionArgs, - ClientLoaderFunctionArgs, - defer, - redirect, - useLoaderData, - useRouteLoaderData, -} from "@remix-run/react"; +import { useLocation, useNavigate } from "@remix-run/react"; import React from "react"; import { useDispatch } from "react-redux"; -import posthog from "posthog-js"; import { SuggestionBox } from "./suggestion-box"; import { TaskForm } from "./task-form"; import { HeroHeading } from "./hero-heading"; -import { retrieveAllGitHubUserRepositories } from "#/api/github"; -import store from "#/store"; -import { - setImportedProjectZip, - setInitialQuery, -} from "#/state/initial-query-slice"; -import { clientLoader as rootClientLoader } from "#/routes/_oh"; -import OpenHands from "#/api/open-hands"; -import { generateGitHubAuthUrl } from "#/utils/generate-github-auth-url"; +import { setImportedProjectZip } from "#/state/initial-query-slice"; import { GitHubRepositoriesSuggestionBox } from "#/components/github-repositories-suggestion-box"; import { convertZipToBase64 } from "#/utils/convert-zip-to-base64"; +import { useUserRepositories } from "#/hooks/query/use-user-repositories"; +import { useGitHubUser } from "#/hooks/query/use-github-user"; +import { useGitHubAuthUrl } from "#/hooks/use-github-auth-url"; +import { useConfig } from "#/hooks/query/use-config"; +import { useAuth } from "#/context/auth-context"; -export const clientLoader = async ({ request }: ClientLoaderFunctionArgs) => { - let isSaas = false; - let githubClientId: string | null = null; - - try { - const config = await OpenHands.getConfig(); - isSaas = config.APP_MODE === "saas"; - githubClientId = config.GITHUB_CLIENT_ID; - } catch (error) { - isSaas = false; - githubClientId = null; - } - - const ghToken = localStorage.getItem("ghToken"); - const token = localStorage.getItem("token"); - if (token) return redirect("/app"); - - let repositories: ReturnType< - typeof retrieveAllGitHubUserRepositories - > | null = null; - if (ghToken) { - const data = retrieveAllGitHubUserRepositories(ghToken); - repositories = data; - } +function Home() { + const { token, gitHubToken } = useAuth(); - let githubAuthUrl: string | null = null; - if (isSaas && githubClientId) { - const requestUrl = new URL(request.url); - githubAuthUrl = generateGitHubAuthUrl(githubClientId, requestUrl); - } + const dispatch = useDispatch(); + const location = useLocation(); + const navigate = useNavigate(); - return defer({ repositories, githubAuthUrl }); -}; + const formRef = React.useRef(null); -export const clientAction = async ({ request }: ClientActionFunctionArgs) => { - const formData = await request.formData(); - const q = formData.get("q")?.toString(); - if (q) store.dispatch(setInitialQuery(q)); + const { data: config } = useConfig(); + const { data: user } = useGitHubUser(); + const { data: repositories } = useUserRepositories(); - posthog.capture("initial_query_submitted", { - query_character_length: q?.length, + const gitHubAuthUrl = useGitHubAuthUrl({ + gitHubToken, + appMode: config?.APP_MODE || null, + gitHubClientId: config?.GITHUB_CLIENT_ID || null, }); - return redirect("/app"); -}; - -function Home() { - const dispatch = useDispatch(); - const rootData = useRouteLoaderData("routes/_oh"); - const { repositories, githubAuthUrl } = useLoaderData(); - const formRef = React.useRef(null); + React.useEffect(() => { + if (token) navigate("/app"); + }, [location.pathname]); return (
- + formRef.current?.requestSubmit()} + repositories={ + repositories?.pages.flatMap((page) => page.data) || [] } - > - - {(resolvedRepositories) => ( - formRef.current?.requestSubmit()} - repositories={resolvedRepositories} - gitHubAuthUrl={githubAuthUrl} - user={rootData?.user || null} - /> - )} - - + gitHubAuthUrl={gitHubAuthUrl} + user={user || null} + // onEndReached={} + /> ((_, ref) => { const dispatch = useDispatch(); const navigation = useNavigation(); + const navigate = useNavigate(); const { selectedRepository, files } = useSelector( (state: RootState) => state.initalQuery, @@ -51,13 +57,26 @@ export const TaskForm = React.forwardRef((_, ref) => { return "What do you want to build?"; }, [selectedRepository]); + const handleSubmit = (event: React.FormEvent) => { + event.preventDefault(); + const formData = new FormData(event.currentTarget); + + const q = formData.get("q")?.toString(); + if (q) dispatch(setInitialQuery(q)); + + posthog.capture("initial_query_submitted", { + query_character_length: q?.length, + }); + + navigate("/app"); + }; + return (
-
((_, ref) => { disabled={navigation.state === "submitting"} />
- + { const promises = uploadedFiles.map(convertImageToBase64); diff --git a/frontend/src/routes/_oh.app._index/code-editor-component.tsx b/frontend/src/routes/_oh.app._index/code-editor-component.tsx index b9f82befa43f..673010b8b1d9 100644 --- a/frontend/src/routes/_oh.app._index/code-editor-component.tsx +++ b/frontend/src/routes/_oh.app._index/code-editor-component.tsx @@ -2,10 +2,9 @@ import { Editor, EditorProps } from "@monaco-editor/react"; import React from "react"; import { useTranslation } from "react-i18next"; import { VscCode } from "react-icons/vsc"; -import toast from "react-hot-toast"; import { I18nKey } from "#/i18n/declaration"; import { useFiles } from "#/context/files"; -import OpenHands from "#/api/open-hands"; +import { useSaveFile } from "#/hooks/mutation/use-save-file"; interface CodeEditorComponentProps { onMount: EditorProps["onMount"]; @@ -25,6 +24,8 @@ function CodeEditorComponent({ saveFileContent: saveNewFileContent, } = useFiles(); + const { mutate: saveFile } = useSaveFile(); + const handleEditorChange = (value: string | undefined) => { if (selectedPath && value) modifyFileContent(selectedPath, value); }; @@ -39,11 +40,7 @@ function CodeEditorComponent({ const content = saveNewFileContent(selectedPath); if (content) { - try { - await OpenHands.saveFile(selectedPath, content); - } catch (error) { - toast.error("Failed to save file"); - } + saveFile({ path: selectedPath, content }); } } }; @@ -66,34 +63,42 @@ function CodeEditorComponent({ ); } - const fileContent = modifiedFiles[selectedPath] || files[selectedPath]; + const fileContent: string | undefined = + modifiedFiles[selectedPath] || files[selectedPath]; - if (isBase64Image(fileContent)) { - return ( -
- {selectedPath} -
- ); - } + if (fileContent) { + if (isBase64Image(fileContent)) { + return ( +
+ {selectedPath} +
+ ); + } - if (isPDF(fileContent)) { - return ( -