mirror of
https://github.com/OpenHands/OpenHands.git
synced 2025-12-26 05:48:36 +08:00
Merge branch 'feature/public-conversation-sharing' of https://github.com/OpenHands/OpenHands into feature/public-conversation-sharing
This commit is contained in:
commit
32fb4767f1
12
.github/CODEOWNERS
vendored
12
.github/CODEOWNERS
vendored
@ -1,12 +1,8 @@
|
||||
# CODEOWNERS file for OpenHands repository
|
||||
# See https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
|
||||
|
||||
# Frontend code owners
|
||||
/frontend/ @amanape
|
||||
/openhands-ui/ @amanape
|
||||
|
||||
# Evaluation code owners
|
||||
/frontend/ @amanape @hieptl
|
||||
/openhands-ui/ @amanape @hieptl
|
||||
/openhands/ @tofarr @malhotra5 @hieptl
|
||||
/enterprise/ @chuckbutkus @tofarr @malhotra5
|
||||
/evaluation/ @xingyaoww @neubig
|
||||
|
||||
# Documentation code owners
|
||||
/docs/ @mamoodi
|
||||
|
||||
@ -31,9 +31,8 @@ RUN pip install alembic psycopg2-binary cloud-sql-python-connector pg8000 gsprea
|
||||
"pillow>=11.3.0"
|
||||
|
||||
WORKDIR /app
|
||||
COPY enterprise .
|
||||
COPY --chown=openhands:openhands --chmod=770 enterprise .
|
||||
|
||||
RUN chown -R openhands:openhands /app && chmod -R 770 /app
|
||||
USER openhands
|
||||
|
||||
# Command will be overridden by Kubernetes deployment template
|
||||
|
||||
@ -116,7 +116,7 @@ lines.append('POSTHOG_CLIENT_KEY=test')
|
||||
lines.append('ENABLE_PROACTIVE_CONVERSATION_STARTERS=true')
|
||||
lines.append('MAX_CONCURRENT_CONVERSATIONS=10')
|
||||
lines.append('LITE_LLM_API_URL=https://llm-proxy.eval.all-hands.dev')
|
||||
lines.append('LITELLM_DEFAULT_MODEL=litellm_proxy/claude-sonnet-4-20250514')
|
||||
lines.append('LITELLM_DEFAULT_MODEL=litellm_proxy/claude-opus-4-5-20251101')
|
||||
lines.append(f'LITE_LLM_API_KEY={lite_llm_api_key}')
|
||||
lines.append('LOCAL_DEPLOYMENT=true')
|
||||
lines.append('DB_HOST=localhost')
|
||||
|
||||
@ -38,3 +38,8 @@ ROLE_CHECK_ENABLED = os.getenv('ROLE_CHECK_ENABLED', 'false').lower() in (
|
||||
'y',
|
||||
'on',
|
||||
)
|
||||
BLOCKED_EMAIL_DOMAINS = [
|
||||
domain.strip().lower()
|
||||
for domain in os.getenv('BLOCKED_EMAIL_DOMAINS', '').split(',')
|
||||
if domain.strip()
|
||||
]
|
||||
|
||||
56
enterprise/server/auth/domain_blocker.py
Normal file
56
enterprise/server/auth/domain_blocker.py
Normal file
@ -0,0 +1,56 @@
|
||||
from server.auth.constants import BLOCKED_EMAIL_DOMAINS
|
||||
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
|
||||
|
||||
class DomainBlocker:
|
||||
def __init__(self) -> None:
|
||||
logger.debug('Initializing DomainBlocker')
|
||||
self.blocked_domains: list[str] = BLOCKED_EMAIL_DOMAINS
|
||||
if self.blocked_domains:
|
||||
logger.info(
|
||||
f'Successfully loaded {len(self.blocked_domains)} blocked email domains: {self.blocked_domains}'
|
||||
)
|
||||
|
||||
def is_active(self) -> bool:
|
||||
"""Check if domain blocking is enabled"""
|
||||
return bool(self.blocked_domains)
|
||||
|
||||
def _extract_domain(self, email: str) -> str | None:
|
||||
"""Extract and normalize email domain from email address"""
|
||||
if not email:
|
||||
return None
|
||||
try:
|
||||
# Extract domain part after @
|
||||
if '@' not in email:
|
||||
return None
|
||||
domain = email.split('@')[1].strip().lower()
|
||||
return domain if domain else None
|
||||
except Exception:
|
||||
logger.debug(f'Error extracting domain from email: {email}', exc_info=True)
|
||||
return None
|
||||
|
||||
def is_domain_blocked(self, email: str) -> bool:
|
||||
"""Check if email domain is blocked"""
|
||||
if not self.is_active():
|
||||
return False
|
||||
|
||||
if not email:
|
||||
logger.debug('No email provided for domain check')
|
||||
return False
|
||||
|
||||
domain = self._extract_domain(email)
|
||||
if not domain:
|
||||
logger.debug(f'Could not extract domain from email: {email}')
|
||||
return False
|
||||
|
||||
is_blocked = domain in self.blocked_domains
|
||||
if is_blocked:
|
||||
logger.warning(f'Email domain {domain} is blocked for email: {email}')
|
||||
else:
|
||||
logger.debug(f'Email domain {domain} is not blocked')
|
||||
|
||||
return is_blocked
|
||||
|
||||
|
||||
domain_blocker = DomainBlocker()
|
||||
@ -13,6 +13,7 @@ from server.auth.auth_error import (
|
||||
ExpiredError,
|
||||
NoCredentialsError,
|
||||
)
|
||||
from server.auth.domain_blocker import domain_blocker
|
||||
from server.auth.token_manager import TokenManager
|
||||
from server.config import get_config
|
||||
from server.logger import logger
|
||||
@ -312,6 +313,16 @@ async def saas_user_auth_from_signed_token(signed_token: str) -> SaasUserAuth:
|
||||
user_id = access_token_payload['sub']
|
||||
email = access_token_payload['email']
|
||||
email_verified = access_token_payload['email_verified']
|
||||
|
||||
# Check if email domain is blocked
|
||||
if email and domain_blocker.is_active() and domain_blocker.is_domain_blocked(email):
|
||||
logger.warning(
|
||||
f'Blocked authentication attempt for existing user with email: {email}'
|
||||
)
|
||||
raise AuthError(
|
||||
'Access denied: Your email domain is not allowed to access this service'
|
||||
)
|
||||
|
||||
logger.debug('saas_user_auth_from_signed_token:return')
|
||||
|
||||
return SaasUserAuth(
|
||||
|
||||
@ -527,6 +527,49 @@ class TokenManager:
|
||||
github_id = github_ids[0]
|
||||
return github_id
|
||||
|
||||
async def disable_keycloak_user(
|
||||
self, user_id: str, email: str | None = None
|
||||
) -> None:
|
||||
"""Disable a Keycloak user account.
|
||||
|
||||
Args:
|
||||
user_id: The Keycloak user ID to disable
|
||||
email: Optional email address for logging purposes
|
||||
|
||||
This method attempts to disable the user account but will not raise exceptions.
|
||||
Errors are logged but do not prevent the operation from completing.
|
||||
"""
|
||||
try:
|
||||
keycloak_admin = get_keycloak_admin(self.external)
|
||||
# Get current user to preserve other fields
|
||||
user = await keycloak_admin.a_get_user(user_id)
|
||||
if user:
|
||||
# Update user with enabled=False to disable the account
|
||||
await keycloak_admin.a_update_user(
|
||||
user_id=user_id,
|
||||
payload={
|
||||
'enabled': False,
|
||||
'username': user.get('username', ''),
|
||||
'email': user.get('email', ''),
|
||||
'emailVerified': user.get('emailVerified', False),
|
||||
},
|
||||
)
|
||||
email_str = f', email: {email}' if email else ''
|
||||
logger.info(
|
||||
f'Disabled Keycloak account for user_id: {user_id}{email_str}'
|
||||
)
|
||||
else:
|
||||
logger.warning(
|
||||
f'User not found in Keycloak when attempting to disable: {user_id}'
|
||||
)
|
||||
except Exception as e:
|
||||
# Log error but don't raise - the caller should handle the blocking regardless
|
||||
email_str = f', email: {email}' if email else ''
|
||||
logger.error(
|
||||
f'Failed to disable Keycloak account for user_id: {user_id}{email_str}: {str(e)}',
|
||||
exc_info=True,
|
||||
)
|
||||
|
||||
def store_org_token(self, installation_id: int, installation_token: str):
|
||||
"""Store a GitHub App installation token.
|
||||
|
||||
|
||||
@ -25,6 +25,7 @@ USER_SETTINGS_VERSION_TO_MODEL = {
|
||||
2: 'claude-3-7-sonnet-20250219',
|
||||
3: 'claude-sonnet-4-20250514',
|
||||
4: 'claude-sonnet-4-20250514',
|
||||
5: 'claude-opus-4-5-20251101',
|
||||
}
|
||||
|
||||
LITELLM_DEFAULT_MODEL = os.getenv('LITELLM_DEFAULT_MODEL')
|
||||
|
||||
@ -14,6 +14,7 @@ from server.auth.constants import (
|
||||
KEYCLOAK_SERVER_URL_EXT,
|
||||
ROLE_CHECK_ENABLED,
|
||||
)
|
||||
from server.auth.domain_blocker import domain_blocker
|
||||
from server.auth.gitlab_sync import schedule_gitlab_repo_sync
|
||||
from server.auth.saas_user_auth import SaasUserAuth
|
||||
from server.auth.token_manager import TokenManager
|
||||
@ -145,7 +146,24 @@ async def keycloak_callback(
|
||||
content={'error': 'Missing user ID or username in response'},
|
||||
)
|
||||
|
||||
# Check if email domain is blocked
|
||||
email = user_info.get('email')
|
||||
user_id = user_info['sub']
|
||||
if email and domain_blocker.is_active() and domain_blocker.is_domain_blocked(email):
|
||||
logger.warning(
|
||||
f'Blocked authentication attempt for email: {email}, user_id: {user_id}'
|
||||
)
|
||||
|
||||
# Disable the Keycloak account
|
||||
await token_manager.disable_keycloak_user(user_id, email)
|
||||
|
||||
return JSONResponse(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
content={
|
||||
'error': 'Access denied: Your email domain is not allowed to access this service'
|
||||
},
|
||||
)
|
||||
|
||||
# default to github IDP for now.
|
||||
# TODO: remove default once Keycloak is updated universally with the new attribute.
|
||||
idp: str = user_info.get('identity_provider', ProviderType.GITHUB.value)
|
||||
|
||||
@ -804,6 +804,8 @@ class SaasNestedConversationManager(ConversationManager):
|
||||
env_vars['ENABLE_V1'] = '0'
|
||||
env_vars['SU_TO_USER'] = SU_TO_USER
|
||||
env_vars['DISABLE_VSCODE_PLUGIN'] = str(DISABLE_VSCODE_PLUGIN).lower()
|
||||
env_vars['BROWSERGYM_DOWNLOAD_DIR'] = '/workspace/.downloads/'
|
||||
env_vars['PLAYWRIGHT_BROWSERS_PATH'] = '/opt/playwright-browsers'
|
||||
|
||||
# We need this for LLM traces tracking to identify the source of the LLM calls
|
||||
env_vars['WEB_HOST'] = WEB_HOST
|
||||
|
||||
@ -17,10 +17,13 @@ from openhands.core.logger import openhands_logger as logger
|
||||
class ApiKeyStore:
|
||||
session_maker: sessionmaker
|
||||
|
||||
API_KEY_PREFIX = 'sk-oh-'
|
||||
|
||||
def generate_api_key(self, length: int = 32) -> str:
|
||||
"""Generate a random API key."""
|
||||
"""Generate a random API key with the sk-oh- prefix."""
|
||||
alphabet = string.ascii_letters + string.digits
|
||||
return ''.join(secrets.choice(alphabet) for _ in range(length))
|
||||
random_part = ''.join(secrets.choice(alphabet) for _ in range(length))
|
||||
return f'{self.API_KEY_PREFIX}{random_part}'
|
||||
|
||||
def create_api_key(
|
||||
self, user_id: str, name: str | None = None, expires_at: datetime | None = None
|
||||
|
||||
@ -95,8 +95,6 @@ class SaasSettingsStore(SettingsStore):
|
||||
self._decrypt_kwargs(kwargs)
|
||||
settings = Settings(**kwargs)
|
||||
|
||||
settings.v1_enabled = True
|
||||
|
||||
return settings
|
||||
|
||||
async def store(self, item: Settings):
|
||||
|
||||
@ -25,10 +25,12 @@ def api_key_store(mock_session_maker):
|
||||
|
||||
|
||||
def test_generate_api_key(api_key_store):
|
||||
"""Test that generate_api_key returns a string of the expected length."""
|
||||
"""Test that generate_api_key returns a string with sk-oh- prefix and expected length."""
|
||||
key = api_key_store.generate_api_key(length=32)
|
||||
assert isinstance(key, str)
|
||||
assert len(key) == 32
|
||||
assert key.startswith('sk-oh-')
|
||||
# Total length should be prefix (6 chars) + random part (32 chars) = 38 chars
|
||||
assert len(key) == len('sk-oh-') + 32
|
||||
|
||||
|
||||
def test_create_api_key(api_key_store, mock_session):
|
||||
|
||||
@ -442,3 +442,196 @@ async def test_logout_without_refresh_token():
|
||||
|
||||
mock_token_manager.logout.assert_not_called()
|
||||
assert 'set-cookie' in result.headers
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_keycloak_callback_blocked_email_domain(mock_request):
|
||||
"""Test keycloak_callback when email domain is blocked."""
|
||||
# Arrange
|
||||
with (
|
||||
patch('server.routes.auth.token_manager') as mock_token_manager,
|
||||
patch('server.routes.auth.domain_blocker') as mock_domain_blocker,
|
||||
):
|
||||
mock_token_manager.get_keycloak_tokens = AsyncMock(
|
||||
return_value=('test_access_token', 'test_refresh_token')
|
||||
)
|
||||
mock_token_manager.get_user_info = AsyncMock(
|
||||
return_value={
|
||||
'sub': 'test_user_id',
|
||||
'preferred_username': 'test_user',
|
||||
'email': 'user@colsch.us',
|
||||
'identity_provider': 'github',
|
||||
}
|
||||
)
|
||||
mock_token_manager.disable_keycloak_user = AsyncMock()
|
||||
|
||||
mock_domain_blocker.is_active.return_value = True
|
||||
mock_domain_blocker.is_domain_blocked.return_value = True
|
||||
|
||||
# Act
|
||||
result = await keycloak_callback(
|
||||
code='test_code', state='test_state', request=mock_request
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert isinstance(result, JSONResponse)
|
||||
assert result.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
assert 'error' in result.body.decode()
|
||||
assert 'email domain is not allowed' in result.body.decode()
|
||||
mock_domain_blocker.is_domain_blocked.assert_called_once_with('user@colsch.us')
|
||||
mock_token_manager.disable_keycloak_user.assert_called_once_with(
|
||||
'test_user_id', 'user@colsch.us'
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_keycloak_callback_allowed_email_domain(mock_request):
|
||||
"""Test keycloak_callback when email domain is not blocked."""
|
||||
# Arrange
|
||||
with (
|
||||
patch('server.routes.auth.token_manager') as mock_token_manager,
|
||||
patch('server.routes.auth.domain_blocker') as mock_domain_blocker,
|
||||
patch('server.routes.auth.user_verifier') as mock_verifier,
|
||||
patch('server.routes.auth.session_maker') as mock_session_maker,
|
||||
):
|
||||
mock_session = MagicMock()
|
||||
mock_session_maker.return_value.__enter__.return_value = mock_session
|
||||
mock_query = MagicMock()
|
||||
mock_session.query.return_value = mock_query
|
||||
mock_query.filter.return_value = mock_query
|
||||
|
||||
mock_user_settings = MagicMock()
|
||||
mock_user_settings.accepted_tos = '2025-01-01'
|
||||
mock_query.first.return_value = mock_user_settings
|
||||
|
||||
mock_token_manager.get_keycloak_tokens = AsyncMock(
|
||||
return_value=('test_access_token', 'test_refresh_token')
|
||||
)
|
||||
mock_token_manager.get_user_info = AsyncMock(
|
||||
return_value={
|
||||
'sub': 'test_user_id',
|
||||
'preferred_username': 'test_user',
|
||||
'email': 'user@example.com',
|
||||
'identity_provider': 'github',
|
||||
}
|
||||
)
|
||||
mock_token_manager.store_idp_tokens = AsyncMock()
|
||||
mock_token_manager.validate_offline_token = AsyncMock(return_value=True)
|
||||
|
||||
mock_domain_blocker.is_active.return_value = True
|
||||
mock_domain_blocker.is_domain_blocked.return_value = False
|
||||
|
||||
mock_verifier.is_active.return_value = True
|
||||
mock_verifier.is_user_allowed.return_value = True
|
||||
|
||||
# Act
|
||||
result = await keycloak_callback(
|
||||
code='test_code', state='test_state', request=mock_request
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert isinstance(result, RedirectResponse)
|
||||
mock_domain_blocker.is_domain_blocked.assert_called_once_with(
|
||||
'user@example.com'
|
||||
)
|
||||
mock_token_manager.disable_keycloak_user.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_keycloak_callback_domain_blocking_inactive(mock_request):
|
||||
"""Test keycloak_callback when domain blocking is not active."""
|
||||
# Arrange
|
||||
with (
|
||||
patch('server.routes.auth.token_manager') as mock_token_manager,
|
||||
patch('server.routes.auth.domain_blocker') as mock_domain_blocker,
|
||||
patch('server.routes.auth.user_verifier') as mock_verifier,
|
||||
patch('server.routes.auth.session_maker') as mock_session_maker,
|
||||
):
|
||||
mock_session = MagicMock()
|
||||
mock_session_maker.return_value.__enter__.return_value = mock_session
|
||||
mock_query = MagicMock()
|
||||
mock_session.query.return_value = mock_query
|
||||
mock_query.filter.return_value = mock_query
|
||||
|
||||
mock_user_settings = MagicMock()
|
||||
mock_user_settings.accepted_tos = '2025-01-01'
|
||||
mock_query.first.return_value = mock_user_settings
|
||||
|
||||
mock_token_manager.get_keycloak_tokens = AsyncMock(
|
||||
return_value=('test_access_token', 'test_refresh_token')
|
||||
)
|
||||
mock_token_manager.get_user_info = AsyncMock(
|
||||
return_value={
|
||||
'sub': 'test_user_id',
|
||||
'preferred_username': 'test_user',
|
||||
'email': 'user@colsch.us',
|
||||
'identity_provider': 'github',
|
||||
}
|
||||
)
|
||||
mock_token_manager.store_idp_tokens = AsyncMock()
|
||||
mock_token_manager.validate_offline_token = AsyncMock(return_value=True)
|
||||
|
||||
mock_domain_blocker.is_active.return_value = False
|
||||
|
||||
mock_verifier.is_active.return_value = True
|
||||
mock_verifier.is_user_allowed.return_value = True
|
||||
|
||||
# Act
|
||||
result = await keycloak_callback(
|
||||
code='test_code', state='test_state', request=mock_request
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert isinstance(result, RedirectResponse)
|
||||
mock_domain_blocker.is_domain_blocked.assert_not_called()
|
||||
mock_token_manager.disable_keycloak_user.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_keycloak_callback_missing_email(mock_request):
|
||||
"""Test keycloak_callback when user info does not contain email."""
|
||||
# Arrange
|
||||
with (
|
||||
patch('server.routes.auth.token_manager') as mock_token_manager,
|
||||
patch('server.routes.auth.domain_blocker') as mock_domain_blocker,
|
||||
patch('server.routes.auth.user_verifier') as mock_verifier,
|
||||
patch('server.routes.auth.session_maker') as mock_session_maker,
|
||||
):
|
||||
mock_session = MagicMock()
|
||||
mock_session_maker.return_value.__enter__.return_value = mock_session
|
||||
mock_query = MagicMock()
|
||||
mock_session.query.return_value = mock_query
|
||||
mock_query.filter.return_value = mock_query
|
||||
|
||||
mock_user_settings = MagicMock()
|
||||
mock_user_settings.accepted_tos = '2025-01-01'
|
||||
mock_query.first.return_value = mock_user_settings
|
||||
|
||||
mock_token_manager.get_keycloak_tokens = AsyncMock(
|
||||
return_value=('test_access_token', 'test_refresh_token')
|
||||
)
|
||||
mock_token_manager.get_user_info = AsyncMock(
|
||||
return_value={
|
||||
'sub': 'test_user_id',
|
||||
'preferred_username': 'test_user',
|
||||
'identity_provider': 'github',
|
||||
# No email field
|
||||
}
|
||||
)
|
||||
mock_token_manager.store_idp_tokens = AsyncMock()
|
||||
mock_token_manager.validate_offline_token = AsyncMock(return_value=True)
|
||||
|
||||
mock_domain_blocker.is_active.return_value = True
|
||||
|
||||
mock_verifier.is_active.return_value = True
|
||||
mock_verifier.is_user_allowed.return_value = True
|
||||
|
||||
# Act
|
||||
result = await keycloak_callback(
|
||||
code='test_code', state='test_state', request=mock_request
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert isinstance(result, RedirectResponse)
|
||||
mock_domain_blocker.is_domain_blocked.assert_not_called()
|
||||
mock_token_manager.disable_keycloak_user.assert_not_called()
|
||||
|
||||
181
enterprise/tests/unit/test_domain_blocker.py
Normal file
181
enterprise/tests/unit/test_domain_blocker.py
Normal file
@ -0,0 +1,181 @@
|
||||
"""Unit tests for DomainBlocker class."""
|
||||
|
||||
import pytest
|
||||
from server.auth.domain_blocker import DomainBlocker
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def domain_blocker():
|
||||
"""Create a DomainBlocker instance for testing."""
|
||||
return DomainBlocker()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'blocked_domains,expected',
|
||||
[
|
||||
(['colsch.us', 'other-domain.com'], True),
|
||||
(['example.com'], True),
|
||||
([], False),
|
||||
],
|
||||
)
|
||||
def test_is_active(domain_blocker, blocked_domains, expected):
|
||||
"""Test that is_active returns correct value based on blocked domains configuration."""
|
||||
# Arrange
|
||||
domain_blocker.blocked_domains = blocked_domains
|
||||
|
||||
# Act
|
||||
result = domain_blocker.is_active()
|
||||
|
||||
# Assert
|
||||
assert result == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'email,expected_domain',
|
||||
[
|
||||
('user@example.com', 'example.com'),
|
||||
('test@colsch.us', 'colsch.us'),
|
||||
('user.name@other-domain.com', 'other-domain.com'),
|
||||
('USER@EXAMPLE.COM', 'example.com'), # Case insensitive
|
||||
('user@EXAMPLE.COM', 'example.com'),
|
||||
(' user@example.com ', 'example.com'), # Whitespace handling
|
||||
],
|
||||
)
|
||||
def test_extract_domain_valid_emails(domain_blocker, email, expected_domain):
|
||||
"""Test that _extract_domain correctly extracts and normalizes domains from valid emails."""
|
||||
# Act
|
||||
result = domain_blocker._extract_domain(email)
|
||||
|
||||
# Assert
|
||||
assert result == expected_domain
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'email,expected',
|
||||
[
|
||||
(None, None),
|
||||
('', None),
|
||||
('invalid-email', None),
|
||||
('user@', None), # Empty domain after @
|
||||
('no-at-sign', None),
|
||||
],
|
||||
)
|
||||
def test_extract_domain_invalid_emails(domain_blocker, email, expected):
|
||||
"""Test that _extract_domain returns None for invalid email formats."""
|
||||
# Act
|
||||
result = domain_blocker._extract_domain(email)
|
||||
|
||||
# Assert
|
||||
assert result == expected
|
||||
|
||||
|
||||
def test_is_domain_blocked_when_inactive(domain_blocker):
|
||||
"""Test that is_domain_blocked returns False when blocking is not active."""
|
||||
# Arrange
|
||||
domain_blocker.blocked_domains = []
|
||||
|
||||
# Act
|
||||
result = domain_blocker.is_domain_blocked('user@colsch.us')
|
||||
|
||||
# Assert
|
||||
assert result is False
|
||||
|
||||
|
||||
def test_is_domain_blocked_with_none_email(domain_blocker):
|
||||
"""Test that is_domain_blocked returns False when email is None."""
|
||||
# Arrange
|
||||
domain_blocker.blocked_domains = ['colsch.us']
|
||||
|
||||
# Act
|
||||
result = domain_blocker.is_domain_blocked(None)
|
||||
|
||||
# Assert
|
||||
assert result is False
|
||||
|
||||
|
||||
def test_is_domain_blocked_with_empty_email(domain_blocker):
|
||||
"""Test that is_domain_blocked returns False when email is empty."""
|
||||
# Arrange
|
||||
domain_blocker.blocked_domains = ['colsch.us']
|
||||
|
||||
# Act
|
||||
result = domain_blocker.is_domain_blocked('')
|
||||
|
||||
# Assert
|
||||
assert result is False
|
||||
|
||||
|
||||
def test_is_domain_blocked_with_invalid_email(domain_blocker):
|
||||
"""Test that is_domain_blocked returns False when email format is invalid."""
|
||||
# Arrange
|
||||
domain_blocker.blocked_domains = ['colsch.us']
|
||||
|
||||
# Act
|
||||
result = domain_blocker.is_domain_blocked('invalid-email')
|
||||
|
||||
# Assert
|
||||
assert result is False
|
||||
|
||||
|
||||
def test_is_domain_blocked_domain_not_blocked(domain_blocker):
|
||||
"""Test that is_domain_blocked returns False when domain is not in blocked list."""
|
||||
# Arrange
|
||||
domain_blocker.blocked_domains = ['colsch.us', 'other-domain.com']
|
||||
|
||||
# Act
|
||||
result = domain_blocker.is_domain_blocked('user@example.com')
|
||||
|
||||
# Assert
|
||||
assert result is False
|
||||
|
||||
|
||||
def test_is_domain_blocked_domain_blocked(domain_blocker):
|
||||
"""Test that is_domain_blocked returns True when domain is in blocked list."""
|
||||
# Arrange
|
||||
domain_blocker.blocked_domains = ['colsch.us', 'other-domain.com']
|
||||
|
||||
# Act
|
||||
result = domain_blocker.is_domain_blocked('user@colsch.us')
|
||||
|
||||
# Assert
|
||||
assert result is True
|
||||
|
||||
|
||||
def test_is_domain_blocked_case_insensitive(domain_blocker):
|
||||
"""Test that is_domain_blocked performs case-insensitive domain matching."""
|
||||
# Arrange
|
||||
domain_blocker.blocked_domains = ['colsch.us']
|
||||
|
||||
# Act
|
||||
result = domain_blocker.is_domain_blocked('user@COLSCH.US')
|
||||
|
||||
# Assert
|
||||
assert result is True
|
||||
|
||||
|
||||
def test_is_domain_blocked_multiple_blocked_domains(domain_blocker):
|
||||
"""Test that is_domain_blocked correctly checks against multiple blocked domains."""
|
||||
# Arrange
|
||||
domain_blocker.blocked_domains = ['colsch.us', 'other-domain.com', 'blocked.org']
|
||||
|
||||
# Act
|
||||
result1 = domain_blocker.is_domain_blocked('user@other-domain.com')
|
||||
result2 = domain_blocker.is_domain_blocked('user@blocked.org')
|
||||
result3 = domain_blocker.is_domain_blocked('user@allowed.com')
|
||||
|
||||
# Assert
|
||||
assert result1 is True
|
||||
assert result2 is True
|
||||
assert result3 is False
|
||||
|
||||
|
||||
def test_is_domain_blocked_with_whitespace(domain_blocker):
|
||||
"""Test that is_domain_blocked handles emails with whitespace correctly."""
|
||||
# Arrange
|
||||
domain_blocker.blocked_domains = ['colsch.us']
|
||||
|
||||
# Act
|
||||
result = domain_blocker.is_domain_blocked(' user@colsch.us ')
|
||||
|
||||
# Assert
|
||||
assert result is True
|
||||
@ -5,7 +5,12 @@ import jwt
|
||||
import pytest
|
||||
from fastapi import Request
|
||||
from pydantic import SecretStr
|
||||
from server.auth.auth_error import BearerTokenError, CookieError, NoCredentialsError
|
||||
from server.auth.auth_error import (
|
||||
AuthError,
|
||||
BearerTokenError,
|
||||
CookieError,
|
||||
NoCredentialsError,
|
||||
)
|
||||
from server.auth.saas_user_auth import (
|
||||
SaasUserAuth,
|
||||
get_api_key_from_header,
|
||||
@ -647,3 +652,97 @@ def test_get_api_key_from_header_bearer_with_empty_token():
|
||||
# Assert that empty string from Bearer is returned (current behavior)
|
||||
# This tests the current implementation behavior
|
||||
assert api_key == ''
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_saas_user_auth_from_signed_token_blocked_domain(mock_config):
|
||||
"""Test that saas_user_auth_from_signed_token raises AuthError when email domain is blocked."""
|
||||
# Arrange
|
||||
access_payload = {
|
||||
'sub': 'test_user_id',
|
||||
'exp': int(time.time()) + 3600,
|
||||
'email': 'user@colsch.us',
|
||||
'email_verified': True,
|
||||
}
|
||||
access_token = jwt.encode(access_payload, 'access_secret', algorithm='HS256')
|
||||
|
||||
token_payload = {
|
||||
'access_token': access_token,
|
||||
'refresh_token': 'test_refresh_token',
|
||||
}
|
||||
signed_token = jwt.encode(token_payload, 'test_secret', algorithm='HS256')
|
||||
|
||||
with patch('server.auth.saas_user_auth.domain_blocker') as mock_domain_blocker:
|
||||
mock_domain_blocker.is_active.return_value = True
|
||||
mock_domain_blocker.is_domain_blocked.return_value = True
|
||||
|
||||
# Act & Assert
|
||||
with pytest.raises(AuthError) as exc_info:
|
||||
await saas_user_auth_from_signed_token(signed_token)
|
||||
|
||||
assert 'email domain is not allowed' in str(exc_info.value)
|
||||
mock_domain_blocker.is_domain_blocked.assert_called_once_with('user@colsch.us')
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_saas_user_auth_from_signed_token_allowed_domain(mock_config):
|
||||
"""Test that saas_user_auth_from_signed_token succeeds when email domain is not blocked."""
|
||||
# Arrange
|
||||
access_payload = {
|
||||
'sub': 'test_user_id',
|
||||
'exp': int(time.time()) + 3600,
|
||||
'email': 'user@example.com',
|
||||
'email_verified': True,
|
||||
}
|
||||
access_token = jwt.encode(access_payload, 'access_secret', algorithm='HS256')
|
||||
|
||||
token_payload = {
|
||||
'access_token': access_token,
|
||||
'refresh_token': 'test_refresh_token',
|
||||
}
|
||||
signed_token = jwt.encode(token_payload, 'test_secret', algorithm='HS256')
|
||||
|
||||
with patch('server.auth.saas_user_auth.domain_blocker') as mock_domain_blocker:
|
||||
mock_domain_blocker.is_active.return_value = True
|
||||
mock_domain_blocker.is_domain_blocked.return_value = False
|
||||
|
||||
# Act
|
||||
result = await saas_user_auth_from_signed_token(signed_token)
|
||||
|
||||
# Assert
|
||||
assert isinstance(result, SaasUserAuth)
|
||||
assert result.user_id == 'test_user_id'
|
||||
assert result.email == 'user@example.com'
|
||||
mock_domain_blocker.is_domain_blocked.assert_called_once_with(
|
||||
'user@example.com'
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_saas_user_auth_from_signed_token_domain_blocking_inactive(mock_config):
|
||||
"""Test that saas_user_auth_from_signed_token succeeds when domain blocking is not active."""
|
||||
# Arrange
|
||||
access_payload = {
|
||||
'sub': 'test_user_id',
|
||||
'exp': int(time.time()) + 3600,
|
||||
'email': 'user@colsch.us',
|
||||
'email_verified': True,
|
||||
}
|
||||
access_token = jwt.encode(access_payload, 'access_secret', algorithm='HS256')
|
||||
|
||||
token_payload = {
|
||||
'access_token': access_token,
|
||||
'refresh_token': 'test_refresh_token',
|
||||
}
|
||||
signed_token = jwt.encode(token_payload, 'test_secret', algorithm='HS256')
|
||||
|
||||
with patch('server.auth.saas_user_auth.domain_blocker') as mock_domain_blocker:
|
||||
mock_domain_blocker.is_active.return_value = False
|
||||
|
||||
# Act
|
||||
result = await saas_user_auth_from_signed_token(signed_token)
|
||||
|
||||
# Assert
|
||||
assert isinstance(result, SaasUserAuth)
|
||||
assert result.user_id == 'test_user_id'
|
||||
mock_domain_blocker.is_domain_blocked.assert_not_called()
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
from unittest.mock import AsyncMock, patch
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from server.auth.token_manager import TokenManager, create_encryption_utility
|
||||
@ -246,3 +246,103 @@ async def test_refresh(token_manager):
|
||||
mock_keycloak.return_value.a_refresh_token.assert_called_once_with(
|
||||
'test_refresh_token'
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_disable_keycloak_user_success(token_manager):
|
||||
"""Test successful disabling of a Keycloak user account."""
|
||||
# Arrange
|
||||
user_id = 'test_user_id'
|
||||
email = 'user@colsch.us'
|
||||
mock_user = {
|
||||
'id': user_id,
|
||||
'username': 'testuser',
|
||||
'email': email,
|
||||
'emailVerified': True,
|
||||
}
|
||||
|
||||
with patch('server.auth.token_manager.get_keycloak_admin') as mock_get_admin:
|
||||
mock_admin = MagicMock()
|
||||
mock_admin.a_get_user = AsyncMock(return_value=mock_user)
|
||||
mock_admin.a_update_user = AsyncMock()
|
||||
mock_get_admin.return_value = mock_admin
|
||||
|
||||
# Act
|
||||
await token_manager.disable_keycloak_user(user_id, email)
|
||||
|
||||
# Assert
|
||||
mock_admin.a_get_user.assert_called_once_with(user_id)
|
||||
mock_admin.a_update_user.assert_called_once_with(
|
||||
user_id=user_id,
|
||||
payload={
|
||||
'enabled': False,
|
||||
'username': 'testuser',
|
||||
'email': email,
|
||||
'emailVerified': True,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_disable_keycloak_user_without_email(token_manager):
|
||||
"""Test disabling Keycloak user without providing email."""
|
||||
# Arrange
|
||||
user_id = 'test_user_id'
|
||||
mock_user = {
|
||||
'id': user_id,
|
||||
'username': 'testuser',
|
||||
'email': 'user@example.com',
|
||||
'emailVerified': False,
|
||||
}
|
||||
|
||||
with patch('server.auth.token_manager.get_keycloak_admin') as mock_get_admin:
|
||||
mock_admin = MagicMock()
|
||||
mock_admin.a_get_user = AsyncMock(return_value=mock_user)
|
||||
mock_admin.a_update_user = AsyncMock()
|
||||
mock_get_admin.return_value = mock_admin
|
||||
|
||||
# Act
|
||||
await token_manager.disable_keycloak_user(user_id)
|
||||
|
||||
# Assert
|
||||
mock_admin.a_get_user.assert_called_once_with(user_id)
|
||||
mock_admin.a_update_user.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_disable_keycloak_user_not_found(token_manager):
|
||||
"""Test disabling Keycloak user when user is not found."""
|
||||
# Arrange
|
||||
user_id = 'nonexistent_user_id'
|
||||
email = 'user@colsch.us'
|
||||
|
||||
with patch('server.auth.token_manager.get_keycloak_admin') as mock_get_admin:
|
||||
mock_admin = MagicMock()
|
||||
mock_admin.a_get_user = AsyncMock(return_value=None)
|
||||
mock_get_admin.return_value = mock_admin
|
||||
|
||||
# Act
|
||||
await token_manager.disable_keycloak_user(user_id, email)
|
||||
|
||||
# Assert
|
||||
mock_admin.a_get_user.assert_called_once_with(user_id)
|
||||
mock_admin.a_update_user.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_disable_keycloak_user_exception_handling(token_manager):
|
||||
"""Test that disable_keycloak_user handles exceptions gracefully without raising."""
|
||||
# Arrange
|
||||
user_id = 'test_user_id'
|
||||
email = 'user@colsch.us'
|
||||
|
||||
with patch('server.auth.token_manager.get_keycloak_admin') as mock_get_admin:
|
||||
mock_admin = MagicMock()
|
||||
mock_admin.a_get_user = AsyncMock(side_effect=Exception('Connection error'))
|
||||
mock_get_admin.return_value = mock_admin
|
||||
|
||||
# Act & Assert - should not raise exception
|
||||
await token_manager.disable_keycloak_user(user_id, email)
|
||||
|
||||
# Verify the method was called
|
||||
mock_admin.a_get_user.assert_called_once_with(user_id)
|
||||
|
||||
@ -18,6 +18,8 @@
|
||||
"i18next/no-literal-string": "error",
|
||||
"unused-imports/no-unused-imports": "error",
|
||||
"prettier/prettier": ["error"],
|
||||
// Enforce using optional chaining (?.) instead of && chains for null/undefined checks
|
||||
"@typescript-eslint/prefer-optional-chain": "error",
|
||||
// Resolves https://stackoverflow.com/questions/59265981/typescript-eslint-missing-file-extension-ts-import-extensions/59268871#59268871
|
||||
"import/extensions": [
|
||||
"error",
|
||||
|
||||
@ -5,7 +5,7 @@ import { MemoryRouter } from "react-router";
|
||||
import { AgentStatus } from "#/components/features/controls/agent-status";
|
||||
import { AgentState } from "#/types/agent-state";
|
||||
import { useAgentState } from "#/hooks/use-agent-state";
|
||||
import { useConversationStore } from "#/state/conversation-store";
|
||||
import { useConversationStore } from "#/stores/conversation-store";
|
||||
|
||||
vi.mock("#/hooks/use-agent-state");
|
||||
|
||||
|
||||
@ -12,7 +12,7 @@ import GitService from "#/api/git-service/git-service.api";
|
||||
import { GitRepository } from "#/types/git";
|
||||
import { RepositoryMicroagent } from "#/types/microagent-management";
|
||||
import { Conversation } from "#/api/open-hands.types";
|
||||
import { useMicroagentManagementStore } from "#/state/microagent-management-store";
|
||||
import { useMicroagentManagementStore } from "#/stores/microagent-management-store";
|
||||
|
||||
// Mock hooks
|
||||
const mockUseUserProviders = vi.fn();
|
||||
|
||||
@ -6,7 +6,7 @@ import { InteractiveChatBox } from "#/components/features/chat/interactive-chat-
|
||||
import { renderWithProviders } from "../../test-utils";
|
||||
import { AgentState } from "#/types/agent-state";
|
||||
import { useAgentState } from "#/hooks/use-agent-state";
|
||||
import { useConversationStore } from "#/state/conversation-store";
|
||||
import { useConversationStore } from "#/stores/conversation-store";
|
||||
|
||||
vi.mock("#/hooks/use-agent-state", () => ({
|
||||
useAgentState: vi.fn(),
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { act, screen } from "@testing-library/react";
|
||||
import { renderWithProviders } from "test-utils";
|
||||
import { vi, describe, afterEach, it, expect } from "vitest";
|
||||
import { Command, useCommandStore } from "#/state/command-store";
|
||||
import { Command, useCommandStore } from "#/stores/command-store";
|
||||
import Terminal from "#/components/features/terminal/terminal";
|
||||
|
||||
const renderTerminal = (commands: Command[] = []) => {
|
||||
|
||||
@ -12,7 +12,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { http, HttpResponse } from "msw";
|
||||
import { useOptimisticUserMessageStore } from "#/stores/optimistic-user-message-store";
|
||||
import { useBrowserStore } from "#/stores/browser-store";
|
||||
import { useCommandStore } from "#/state/command-store";
|
||||
import { useCommandStore } from "#/stores/command-store";
|
||||
import {
|
||||
createMockMessageEvent,
|
||||
createMockUserMessageEvent,
|
||||
@ -453,18 +453,10 @@ describe("Conversation WebSocket Handler", () => {
|
||||
|
||||
// Set up MSW to mock both the HTTP API and WebSocket connection
|
||||
mswServer.use(
|
||||
http.get("/api/v1/events/count", ({ request }) => {
|
||||
const url = new URL(request.url);
|
||||
const conversationIdParam = url.searchParams.get(
|
||||
"conversation_id__eq",
|
||||
);
|
||||
|
||||
if (conversationIdParam === conversationId) {
|
||||
return HttpResponse.json(expectedEventCount);
|
||||
}
|
||||
|
||||
return HttpResponse.json(0);
|
||||
}),
|
||||
http.get(
|
||||
`http://localhost:3000/api/conversations/${conversationId}/events/count`,
|
||||
() => HttpResponse.json(expectedEventCount),
|
||||
),
|
||||
wsLink.addEventListener("connection", ({ client, server }) => {
|
||||
server.connect();
|
||||
// Send all history events
|
||||
@ -520,18 +512,10 @@ describe("Conversation WebSocket Handler", () => {
|
||||
|
||||
// Set up MSW to mock both the HTTP API and WebSocket connection
|
||||
mswServer.use(
|
||||
http.get("/api/v1/events/count", ({ request }) => {
|
||||
const url = new URL(request.url);
|
||||
const conversationIdParam = url.searchParams.get(
|
||||
"conversation_id__eq",
|
||||
);
|
||||
|
||||
if (conversationIdParam === conversationId) {
|
||||
return HttpResponse.json(0);
|
||||
}
|
||||
|
||||
return HttpResponse.json(0);
|
||||
}),
|
||||
http.get(
|
||||
`http://localhost:3000/api/conversations/${conversationId}/events/count`,
|
||||
() => HttpResponse.json(0),
|
||||
),
|
||||
wsLink.addEventListener("connection", ({ server }) => {
|
||||
server.connect();
|
||||
// No events sent for empty history
|
||||
@ -577,18 +561,10 @@ describe("Conversation WebSocket Handler", () => {
|
||||
|
||||
// Set up MSW to mock both the HTTP API and WebSocket connection
|
||||
mswServer.use(
|
||||
http.get("/api/v1/events/count", ({ request }) => {
|
||||
const url = new URL(request.url);
|
||||
const conversationIdParam = url.searchParams.get(
|
||||
"conversation_id__eq",
|
||||
);
|
||||
|
||||
if (conversationIdParam === conversationId) {
|
||||
return HttpResponse.json(expectedEventCount);
|
||||
}
|
||||
|
||||
return HttpResponse.json(0);
|
||||
}),
|
||||
http.get(
|
||||
`http://localhost:3000/api/conversations/${conversationId}/events/count`,
|
||||
() => HttpResponse.json(expectedEventCount),
|
||||
),
|
||||
wsLink.addEventListener("connection", ({ client, server }) => {
|
||||
server.connect();
|
||||
// Send all history events
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
/* eslint-disable max-classes-per-file */
|
||||
import { beforeAll, describe, expect, it, vi, afterEach } from "vitest";
|
||||
import { useTerminal } from "#/hooks/use-terminal";
|
||||
import { Command, useCommandStore } from "#/state/command-store";
|
||||
import { Command, useCommandStore } from "#/stores/command-store";
|
||||
import { renderWithProviders } from "../../test-utils";
|
||||
|
||||
// Mock the WsClient context
|
||||
@ -43,6 +42,11 @@ describe("useTerminal", () => {
|
||||
write: vi.fn(),
|
||||
writeln: vi.fn(),
|
||||
dispose: vi.fn(),
|
||||
element: document.createElement("div"),
|
||||
}));
|
||||
|
||||
const mockFitAddon = vi.hoisted(() => ({
|
||||
fit: vi.fn(),
|
||||
}));
|
||||
|
||||
beforeAll(() => {
|
||||
@ -68,6 +72,15 @@ describe("useTerminal", () => {
|
||||
writeln = mockTerminal.writeln;
|
||||
|
||||
dispose = mockTerminal.dispose;
|
||||
|
||||
element = mockTerminal.element;
|
||||
},
|
||||
}));
|
||||
|
||||
// mock FitAddon
|
||||
vi.mock("@xterm/addon-fit", () => ({
|
||||
FitAddon: class {
|
||||
fit = mockFitAddon.fit;
|
||||
},
|
||||
}));
|
||||
});
|
||||
@ -96,4 +109,18 @@ describe("useTerminal", () => {
|
||||
expect(mockTerminal.writeln).toHaveBeenNthCalledWith(1, "echo hello");
|
||||
expect(mockTerminal.writeln).toHaveBeenNthCalledWith(2, "hello");
|
||||
});
|
||||
|
||||
it("should not call fit() when terminal.element is null", () => {
|
||||
// Temporarily set element to null to simulate terminal not being opened
|
||||
const originalElement = mockTerminal.element;
|
||||
mockTerminal.element = null as unknown as HTMLDivElement;
|
||||
|
||||
renderWithProviders(<TestTerminalComponent />);
|
||||
|
||||
// fit() should not be called because terminal.element is null
|
||||
expect(mockFitAddon.fit).not.toHaveBeenCalled();
|
||||
|
||||
// Restore original element
|
||||
mockTerminal.element = originalElement;
|
||||
});
|
||||
});
|
||||
|
||||
@ -72,7 +72,7 @@ describe("Content", () => {
|
||||
|
||||
await waitFor(() => {
|
||||
expect(provider).toHaveValue("OpenHands");
|
||||
expect(model).toHaveValue("claude-sonnet-4-20250514");
|
||||
expect(model).toHaveValue("claude-opus-4-5-20251101");
|
||||
|
||||
expect(apiKey).toHaveValue("");
|
||||
expect(apiKey).toHaveProperty("placeholder", "");
|
||||
@ -190,7 +190,7 @@ describe("Content", () => {
|
||||
const agent = screen.getByTestId("agent-input");
|
||||
const condensor = screen.getByTestId("enable-memory-condenser-switch");
|
||||
|
||||
expect(model).toHaveValue("openhands/claude-sonnet-4-20250514");
|
||||
expect(model).toHaveValue("openhands/claude-opus-4-5-20251101");
|
||||
expect(baseUrl).toHaveValue("");
|
||||
expect(apiKey).toHaveValue("");
|
||||
expect(apiKey).toHaveProperty("placeholder", "");
|
||||
@ -910,6 +910,162 @@ describe("Form submission", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("View persistence after saving advanced settings", () => {
|
||||
it("should remain on Advanced view after saving when memory condenser is disabled", async () => {
|
||||
// Arrange: Start with default settings (basic view)
|
||||
const getSettingsSpy = vi.spyOn(SettingsService, "getSettings");
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
});
|
||||
const saveSettingsSpy = vi.spyOn(SettingsService, "saveSettings");
|
||||
saveSettingsSpy.mockResolvedValue(true);
|
||||
|
||||
renderLlmSettingsScreen();
|
||||
await screen.findByTestId("llm-settings-screen");
|
||||
|
||||
// Verify we start in basic view
|
||||
expect(screen.getByTestId("llm-settings-form-basic")).toBeInTheDocument();
|
||||
|
||||
// Act: User manually switches to Advanced view
|
||||
const advancedSwitch = screen.getByTestId("advanced-settings-switch");
|
||||
await userEvent.click(advancedSwitch);
|
||||
await screen.findByTestId("llm-settings-form-advanced");
|
||||
|
||||
// User disables memory condenser (advanced-only setting)
|
||||
const condenserSwitch = screen.getByTestId(
|
||||
"enable-memory-condenser-switch",
|
||||
);
|
||||
expect(condenserSwitch).toBeChecked();
|
||||
await userEvent.click(condenserSwitch);
|
||||
expect(condenserSwitch).not.toBeChecked();
|
||||
|
||||
// Mock the updated settings that will be returned after save
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
enable_default_condenser: false, // Now disabled
|
||||
});
|
||||
|
||||
// User saves settings
|
||||
const submitButton = screen.getByTestId("submit-button");
|
||||
await userEvent.click(submitButton);
|
||||
|
||||
// Assert: View should remain on Advanced after save
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByTestId("llm-settings-form-advanced"),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByTestId("llm-settings-form-basic"),
|
||||
).not.toBeInTheDocument();
|
||||
expect(advancedSwitch).toBeChecked();
|
||||
});
|
||||
});
|
||||
|
||||
it("should remain on Advanced view after saving when condenser max size is customized", async () => {
|
||||
// Arrange: Start with default settings
|
||||
const getSettingsSpy = vi.spyOn(SettingsService, "getSettings");
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
});
|
||||
const saveSettingsSpy = vi.spyOn(SettingsService, "saveSettings");
|
||||
saveSettingsSpy.mockResolvedValue(true);
|
||||
|
||||
renderLlmSettingsScreen();
|
||||
await screen.findByTestId("llm-settings-screen");
|
||||
|
||||
// Act: User manually switches to Advanced view
|
||||
const advancedSwitch = screen.getByTestId("advanced-settings-switch");
|
||||
await userEvent.click(advancedSwitch);
|
||||
await screen.findByTestId("llm-settings-form-advanced");
|
||||
|
||||
// User sets custom condenser max size (advanced-only setting)
|
||||
const condenserMaxSizeInput = screen.getByTestId(
|
||||
"condenser-max-size-input",
|
||||
);
|
||||
await userEvent.clear(condenserMaxSizeInput);
|
||||
await userEvent.type(condenserMaxSizeInput, "200");
|
||||
|
||||
// Mock the updated settings that will be returned after save
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
condenser_max_size: 200, // Custom value
|
||||
});
|
||||
|
||||
// User saves settings
|
||||
const submitButton = screen.getByTestId("submit-button");
|
||||
await userEvent.click(submitButton);
|
||||
|
||||
// Assert: View should remain on Advanced after save
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByTestId("llm-settings-form-advanced"),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByTestId("llm-settings-form-basic"),
|
||||
).not.toBeInTheDocument();
|
||||
expect(advancedSwitch).toBeChecked();
|
||||
});
|
||||
});
|
||||
|
||||
it("should remain on Advanced view after saving when search API key is set", async () => {
|
||||
// Arrange: Start with default settings (non-SaaS mode to show search API key field)
|
||||
const getConfigSpy = vi.spyOn(OptionService, "getConfig");
|
||||
getConfigSpy.mockResolvedValue({
|
||||
APP_MODE: "oss",
|
||||
GITHUB_CLIENT_ID: "fake-github-client-id",
|
||||
POSTHOG_CLIENT_KEY: "fake-posthog-client-key",
|
||||
FEATURE_FLAGS: {
|
||||
ENABLE_BILLING: false,
|
||||
HIDE_LLM_SETTINGS: false,
|
||||
ENABLE_JIRA: false,
|
||||
ENABLE_JIRA_DC: false,
|
||||
ENABLE_LINEAR: false,
|
||||
},
|
||||
});
|
||||
|
||||
const getSettingsSpy = vi.spyOn(SettingsService, "getSettings");
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
search_api_key: "", // Default empty value
|
||||
});
|
||||
const saveSettingsSpy = vi.spyOn(SettingsService, "saveSettings");
|
||||
saveSettingsSpy.mockResolvedValue(true);
|
||||
|
||||
renderLlmSettingsScreen();
|
||||
await screen.findByTestId("llm-settings-screen");
|
||||
|
||||
// Act: User manually switches to Advanced view
|
||||
const advancedSwitch = screen.getByTestId("advanced-settings-switch");
|
||||
await userEvent.click(advancedSwitch);
|
||||
await screen.findByTestId("llm-settings-form-advanced");
|
||||
|
||||
// User sets search API key (advanced-only setting)
|
||||
const searchApiKeyInput = screen.getByTestId("search-api-key-input");
|
||||
await userEvent.type(searchApiKeyInput, "test-search-api-key");
|
||||
|
||||
// Mock the updated settings that will be returned after save
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
search_api_key: "test-search-api-key", // Now set
|
||||
});
|
||||
|
||||
// User saves settings
|
||||
const submitButton = screen.getByTestId("submit-button");
|
||||
await userEvent.click(submitButton);
|
||||
|
||||
// Assert: View should remain on Advanced after save
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByTestId("llm-settings-form-advanced"),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByTestId("llm-settings-form-basic"),
|
||||
).not.toBeInTheDocument();
|
||||
expect(advancedSwitch).toBeChecked();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Status toasts", () => {
|
||||
describe("Basic form", () => {
|
||||
it("should call displaySuccessToast when the settings are saved", async () => {
|
||||
|
||||
@ -2,7 +2,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { handleStatusMessage } from "#/services/actions";
|
||||
import { StatusMessage } from "#/types/message";
|
||||
import { queryClient } from "#/query-client-config";
|
||||
import { useStatusStore } from "#/state/status-store";
|
||||
import { useStatusStore } from "#/stores/status-store";
|
||||
import { trackError } from "#/utils/error-handler";
|
||||
|
||||
// Mock dependencies
|
||||
@ -12,7 +12,7 @@ vi.mock("#/query-client-config", () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("#/state/status-store", () => ({
|
||||
vi.mock("#/stores/status-store", () => ({
|
||||
useStatusStore: {
|
||||
getState: vi.fn(() => ({
|
||||
setCurStatusMessage: vi.fn(),
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import ActionType from "#/types/action-type";
|
||||
import { ActionMessage } from "#/types/message";
|
||||
import { useCommandStore } from "#/state/command-store";
|
||||
import { useCommandStore } from "#/stores/command-store";
|
||||
|
||||
const mockDispatch = vi.fn();
|
||||
const mockAppendInput = vi.fn();
|
||||
|
||||
@ -29,5 +29,75 @@ describe("hasAdvancedSettingsSet", () => {
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test("enable_default_condenser is disabled", () => {
|
||||
// Arrange
|
||||
const settings = {
|
||||
...DEFAULT_SETTINGS,
|
||||
enable_default_condenser: false,
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = hasAdvancedSettingsSet(settings);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test("condenser_max_size is customized above default", () => {
|
||||
// Arrange
|
||||
const settings = {
|
||||
...DEFAULT_SETTINGS,
|
||||
condenser_max_size: 200,
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = hasAdvancedSettingsSet(settings);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test("condenser_max_size is customized below default", () => {
|
||||
// Arrange
|
||||
const settings = {
|
||||
...DEFAULT_SETTINGS,
|
||||
condenser_max_size: 50,
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = hasAdvancedSettingsSet(settings);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test("search_api_key is set to non-empty value", () => {
|
||||
// Arrange
|
||||
const settings = {
|
||||
...DEFAULT_SETTINGS,
|
||||
search_api_key: "test-api-key-123",
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = hasAdvancedSettingsSet(settings);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test("search_api_key with whitespace is treated as set", () => {
|
||||
// Arrange
|
||||
const settings = {
|
||||
...DEFAULT_SETTINGS,
|
||||
search_api_key: " test-key ",
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = hasAdvancedSettingsSet(settings);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
362
frontend/package-lock.json
generated
362
frontend/package-lock.json
generated
@ -11,8 +11,8 @@
|
||||
"@heroui/react": "2.8.6",
|
||||
"@microlink/react-json-view": "^1.26.2",
|
||||
"@monaco-editor/react": "^4.7.0-rc.0",
|
||||
"@react-router/node": "^7.10.1",
|
||||
"@react-router/serve": "^7.10.1",
|
||||
"@react-router/node": "^7.11.0",
|
||||
"@react-router/serve": "^7.11.0",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@tanstack/react-query": "^5.90.12",
|
||||
"@uidotdev/usehooks": "^2.4.1",
|
||||
@ -28,16 +28,16 @@
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"i18next-http-backend": "^3.0.2",
|
||||
"isbot": "^5.1.32",
|
||||
"lucide-react": "^0.561.0",
|
||||
"lucide-react": "^0.562.0",
|
||||
"monaco-editor": "^0.55.1",
|
||||
"posthog-js": "^1.309.0",
|
||||
"posthog-js": "^1.309.1",
|
||||
"react": "^19.2.3",
|
||||
"react-dom": "^19.2.3",
|
||||
"react-hot-toast": "^2.6.0",
|
||||
"react-i18next": "^16.5.0",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-router": "^7.10.1",
|
||||
"react-router": "^7.11.0",
|
||||
"react-syntax-highlighter": "^16.1.0",
|
||||
"remark-breaks": "^4.0.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
@ -51,7 +51,7 @@
|
||||
"devDependencies": {
|
||||
"@mswjs/socket.io-binding": "^0.2.0",
|
||||
"@playwright/test": "^1.57.0",
|
||||
"@react-router/dev": "^7.10.1",
|
||||
"@react-router/dev": "^7.11.0",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"@tanstack/eslint-plugin-query": "^5.91.0",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
@ -85,7 +85,7 @@
|
||||
"tailwindcss": "^4.1.8",
|
||||
"typescript": "^5.9.3",
|
||||
"vite-plugin-svgr": "^4.5.0",
|
||||
"vite-tsconfig-paths": "^6.0.2",
|
||||
"vite-tsconfig-paths": "^6.0.3",
|
||||
"vitest": "^4.0.14"
|
||||
},
|
||||
"engines": {
|
||||
@ -192,6 +192,7 @@
|
||||
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.28.5",
|
||||
@ -731,6 +732,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
@ -777,6 +779,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
@ -2328,6 +2331,7 @@
|
||||
"version": "2.4.24",
|
||||
"resolved": "https://registry.npmjs.org/@heroui/system/-/system-2.4.24.tgz",
|
||||
"integrity": "sha512-9GKQgUc91otQfwmq6TLE72QKxtB341aK5NpBHS3gRoWYEuNN714Zl3OXwIZNvdXPJpsTaUo1ID1ibJU9tfgwdg==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@heroui/react-utils": "2.1.14",
|
||||
"@heroui/system-rsc": "2.3.21",
|
||||
@ -2407,6 +2411,7 @@
|
||||
"version": "2.4.24",
|
||||
"resolved": "https://registry.npmjs.org/@heroui/theme/-/theme-2.4.24.tgz",
|
||||
"integrity": "sha512-lL+anmY4GGWwKyTbJ2PEBZE4talIZ3hu4yGpku9TktCVG2nC2YTwiWQFJ+Jcbf8Cf9vuLzI1sla5bz2jUqiBRA==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@heroui/shared-utils": "2.1.12",
|
||||
"color": "^4.2.3",
|
||||
@ -3192,9 +3197,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@posthog/core": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.8.0.tgz",
|
||||
"integrity": "sha512-SfmG1EdbR+2zpQccgBUxM/snCROB9WGkY7VH1r9iaoTNqoaN9IkmIEA/07cZLY4DxVP8jt6Vdfe3s84xksac1g==",
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.8.1.tgz",
|
||||
"integrity": "sha512-jfzBtQIk9auRi/biO+G/gumK5KxqsD5wOr7XpYMROE/I3pazjP4zIziinp21iQuIQJMXrDvwt9Af3njgOGwtew==",
|
||||
"dependencies": {
|
||||
"cross-spawn": "^7.0.6"
|
||||
}
|
||||
@ -3872,11 +3877,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@react-router/dev": {
|
||||
"version": "7.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@react-router/dev/-/dev-7.10.1.tgz",
|
||||
"integrity": "sha512-kap9O8rTN6b3vxjd+0SGjhm5vqiAZHMmOX1Hc7Y4KXRVVdusn+0+hxs44cDSfbW6Z6fCLw6GXXe0Kr+DJIRezw==",
|
||||
"version": "7.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@react-router/dev/-/dev-7.11.0.tgz",
|
||||
"integrity": "sha512-g1ou5Zw3r4mCU0L+EXH4vRtAiyt8qz1JOvL1k+PW4rZ4+71h5nBy/fLgD7cg5BnzQZmjRO1PzCgpF5BIrlKYxQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.27.7",
|
||||
"@babel/generator": "^7.27.5",
|
||||
@ -3885,7 +3889,7 @@
|
||||
"@babel/preset-typescript": "^7.27.1",
|
||||
"@babel/traverse": "^7.27.7",
|
||||
"@babel/types": "^7.27.7",
|
||||
"@react-router/node": "7.10.1",
|
||||
"@react-router/node": "7.11.0",
|
||||
"@remix-run/node-fetch-server": "^0.9.0",
|
||||
"arg": "^5.0.1",
|
||||
"babel-dead-code-elimination": "^1.0.6",
|
||||
@ -3914,9 +3918,10 @@
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@react-router/serve": "^7.10.1",
|
||||
"@vitejs/plugin-rsc": "*",
|
||||
"react-router": "^7.10.1",
|
||||
"@react-router/serve": "^7.11.0",
|
||||
"@vitejs/plugin-rsc": "~0.5.7",
|
||||
"react-router": "^7.11.0",
|
||||
"react-server-dom-webpack": "^19.2.3",
|
||||
"typescript": "^5.1.0",
|
||||
"vite": "^5.1.0 || ^6.0.0 || ^7.0.0",
|
||||
"wrangler": "^3.28.2 || ^4.0.0"
|
||||
@ -3928,6 +3933,9 @@
|
||||
"@vitejs/plugin-rsc": {
|
||||
"optional": true
|
||||
},
|
||||
"react-server-dom-webpack": {
|
||||
"optional": true
|
||||
},
|
||||
"typescript": {
|
||||
"optional": true
|
||||
},
|
||||
@ -3936,33 +3944,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@react-router/express": {
|
||||
"version": "7.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@react-router/express/-/express-7.10.1.tgz",
|
||||
"integrity": "sha512-O7xjg6wWHfrsnPyVWgQG+tCamIE09SqLqtHwa1tAFzKPjcDpCw4S4+/OkJvNXLtBL60H3VhZ1r2OQgXBgGOMpw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@react-router/node": "7.10.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"express": "^4.17.1 || ^5",
|
||||
"react-router": "7.10.1",
|
||||
"typescript": "^5.1.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@react-router/node": {
|
||||
"version": "7.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@react-router/node/-/node-7.10.1.tgz",
|
||||
"integrity": "sha512-RLmjlR1zQu+ve8ibI0lu91pJrXGcmfkvsrQl7z/eTc5V5FZgl0OvQVWL5JDWBlBZyzdLMQQekUOX5WcPhCP1FQ==",
|
||||
"license": "MIT",
|
||||
"version": "7.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@react-router/node/-/node-7.11.0.tgz",
|
||||
"integrity": "sha512-11ha8EW+F7wTMmPz2pdi11LJxz2irtuksiCpunpZjtpPmYU37S+GGihG8vFeTa2xFPNunEaHNlfzKyzeYm570Q==",
|
||||
"dependencies": {
|
||||
"@mjackson/node-fetch-server": "^0.2.0"
|
||||
},
|
||||
@ -3970,7 +3955,7 @@
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react-router": "7.10.1",
|
||||
"react-router": "7.11.0",
|
||||
"typescript": "^5.1.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
@ -3980,18 +3965,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@react-router/serve": {
|
||||
"version": "7.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@react-router/serve/-/serve-7.10.1.tgz",
|
||||
"integrity": "sha512-qYco7sFpbRgoKJKsCgJmFBQwaLVsLv255K8vbPodnXe13YBEzV/ugIqRCYVz2hghvlPiEKgaHh2On0s/5npn6w==",
|
||||
"license": "MIT",
|
||||
"version": "7.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@react-router/serve/-/serve-7.11.0.tgz",
|
||||
"integrity": "sha512-U5Ht9PmUYF4Ti1ssaWlddLY4ZCbXBtHDGFU/u1h3VsHqleSdHsFuGAFrr/ZEuqTuEWp1CLqn2npEDAmlV9IUKQ==",
|
||||
"dependencies": {
|
||||
"@mjackson/node-fetch-server": "^0.2.0",
|
||||
"@react-router/express": "7.10.1",
|
||||
"@react-router/node": "7.10.1",
|
||||
"compression": "^1.7.4",
|
||||
"@react-router/express": "7.11.0",
|
||||
"@react-router/node": "7.11.0",
|
||||
"compression": "^1.8.1",
|
||||
"express": "^4.19.2",
|
||||
"get-port": "5.1.1",
|
||||
"morgan": "^1.10.0",
|
||||
"morgan": "^1.10.1",
|
||||
"source-map-support": "^0.5.21"
|
||||
},
|
||||
"bin": {
|
||||
@ -4001,7 +3985,28 @@
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react-router": "7.10.1"
|
||||
"react-router": "7.11.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-router/serve/node_modules/@react-router/express": {
|
||||
"version": "7.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@react-router/express/-/express-7.11.0.tgz",
|
||||
"integrity": "sha512-o5DeO9tqUrZcUWAgmPGgK4I/S6iFpqnj/e20xMGA04trk+90b9KAx9eqmRMgHERubVKANTM9gTDPduobQjeH1A==",
|
||||
"dependencies": {
|
||||
"@react-router/node": "7.11.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"express": "^4.17.1 || ^5",
|
||||
"react-router": "7.11.0",
|
||||
"typescript": "^5.1.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@react-stately/calendar": {
|
||||
@ -5122,6 +5127,7 @@
|
||||
"integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.21.3",
|
||||
"@svgr/babel-preset": "8.1.0",
|
||||
@ -5582,6 +5588,7 @@
|
||||
"integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.10.4",
|
||||
"@babel/runtime": "^7.12.5",
|
||||
@ -5759,6 +5766,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz",
|
||||
"integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==",
|
||||
"devOptional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~7.16.0"
|
||||
}
|
||||
@ -5774,6 +5782,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz",
|
||||
"integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.2.2"
|
||||
}
|
||||
@ -5784,6 +5793,7 @@
|
||||
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"@types/react": "^19.2.0"
|
||||
}
|
||||
@ -5824,6 +5834,7 @@
|
||||
"integrity": "sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.10.0",
|
||||
"@typescript-eslint/scope-manager": "7.18.0",
|
||||
@ -5881,6 +5892,7 @@
|
||||
"integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "7.18.0",
|
||||
"@typescript-eslint/types": "7.18.0",
|
||||
@ -6394,13 +6406,13 @@
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz",
|
||||
"integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/accepts": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
||||
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-types": "~2.1.34",
|
||||
"negotiator": "0.6.3"
|
||||
@ -6413,7 +6425,6 @@
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
|
||||
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
@ -6424,6 +6435,7 @@
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@ -6554,8 +6566,7 @@
|
||||
"node_modules/array-flatten": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
||||
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
|
||||
"license": "MIT"
|
||||
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
|
||||
},
|
||||
"node_modules/array-includes": {
|
||||
"version": "3.1.9",
|
||||
@ -6886,7 +6897,6 @@
|
||||
"version": "1.20.4",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
|
||||
"integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bytes": "~3.1.2",
|
||||
"content-type": "~1.0.5",
|
||||
@ -6910,7 +6920,6 @@
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
@ -6918,8 +6927,7 @@
|
||||
"node_modules/body-parser/node_modules/ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||
"license": "MIT"
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "2.0.2",
|
||||
@ -6964,6 +6972,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.9.0",
|
||||
"caniuse-lite": "^1.0.30001759",
|
||||
@ -7485,7 +7494,6 @@
|
||||
"version": "0.5.4",
|
||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
|
||||
"integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safe-buffer": "5.2.1"
|
||||
},
|
||||
@ -7497,7 +7505,6 @@
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
|
||||
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
@ -7513,7 +7520,6 @@
|
||||
"version": "0.7.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
||||
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
@ -7521,8 +7527,7 @@
|
||||
"node_modules/cookie-signature": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
|
||||
"integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
|
||||
"license": "MIT"
|
||||
"integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA=="
|
||||
},
|
||||
"node_modules/core-js": {
|
||||
"version": "3.47.0",
|
||||
@ -7656,7 +7661,8 @@
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/damerau-levenshtein": {
|
||||
"version": "1.0.8",
|
||||
@ -7866,7 +7872,6 @@
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
|
||||
"integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8",
|
||||
"npm": "1.2.8000 || >= 1.4.16"
|
||||
@ -8001,7 +8006,6 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
||||
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
@ -8354,8 +8358,7 @@
|
||||
"node_modules/escape-html": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
||||
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
|
||||
"license": "MIT"
|
||||
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
|
||||
},
|
||||
"node_modules/escape-string-regexp": {
|
||||
"version": "4.0.0",
|
||||
@ -8377,6 +8380,7 @@
|
||||
"deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.2.0",
|
||||
"@eslint-community/regexpp": "^4.6.1",
|
||||
@ -8500,6 +8504,7 @@
|
||||
"integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"eslint-config-prettier": "bin/cli.js"
|
||||
},
|
||||
@ -8580,6 +8585,7 @@
|
||||
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@rtsao/scc": "^1.1.0",
|
||||
"array-includes": "^3.1.9",
|
||||
@ -8671,6 +8677,7 @@
|
||||
"integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"aria-query": "^5.3.2",
|
||||
"array-includes": "^3.1.8",
|
||||
@ -8766,6 +8773,7 @@
|
||||
"integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"array-includes": "^3.1.8",
|
||||
"array.prototype.findlast": "^1.2.5",
|
||||
@ -8799,6 +8807,7 @@
|
||||
"integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
@ -9029,7 +9038,6 @@
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
|
||||
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
@ -9068,7 +9076,7 @@
|
||||
"version": "4.22.1",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
|
||||
"integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.8",
|
||||
"array-flatten": "1.1.1",
|
||||
@ -9114,7 +9122,6 @@
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
@ -9122,8 +9129,7 @@
|
||||
"node_modules/express/node_modules/ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||
"license": "MIT"
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
|
||||
},
|
||||
"node_modules/exsolve": {
|
||||
"version": "1.0.8",
|
||||
@ -9255,7 +9261,6 @@
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
|
||||
"integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "2.6.9",
|
||||
"encodeurl": "~2.0.0",
|
||||
@ -9273,7 +9278,6 @@
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
@ -9281,8 +9285,7 @@
|
||||
"node_modules/finalhandler/node_modules/ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||
"license": "MIT"
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
|
||||
},
|
||||
"node_modules/find-up": {
|
||||
"version": "5.0.0",
|
||||
@ -9395,7 +9398,6 @@
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
@ -9405,6 +9407,7 @@
|
||||
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.26.tgz",
|
||||
"integrity": "sha512-cPcIhgR42xBn1Uj+PzOyheMtZ73H927+uWPDVhUMqxy8UHt6Okavb6xIz9J/phFUHUj0OncR6UvMfJTXoc/LKA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"motion-dom": "^12.23.23",
|
||||
"motion-utils": "^12.23.6",
|
||||
@ -9431,7 +9434,6 @@
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
|
||||
"integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
@ -9996,7 +9998,6 @@
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
|
||||
"integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"depd": "~2.0.0",
|
||||
"inherits": "~2.0.4",
|
||||
@ -10074,6 +10075,7 @@
|
||||
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
|
||||
}
|
||||
],
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.28.4"
|
||||
},
|
||||
@ -10108,7 +10110,6 @@
|
||||
"version": "0.4.24",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
||||
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3"
|
||||
},
|
||||
@ -10226,7 +10227,6 @@
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
@ -10853,6 +10853,7 @@
|
||||
"integrity": "sha512-GtldT42B8+jefDUC4yUKAvsaOrH7PDHmZxZXNgF2xMmymjUbRYJvpAybZAKEmXDGTM0mCsz8duOa4vTm5AY2Kg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@acemir/cssom": "^0.9.28",
|
||||
"@asamuzakjp/dom-selector": "^6.7.6",
|
||||
@ -11460,9 +11461,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lucide-react": {
|
||||
"version": "0.561.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.561.0.tgz",
|
||||
"integrity": "sha512-Y59gMY38tl4/i0qewcqohPdEbieBy7SovpBL9IFebhc2mDd8x4PZSOsiFRkpPcOq6bj1r/mjH/Rk73gSlIJP2A==",
|
||||
"version": "0.562.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.562.0.tgz",
|
||||
"integrity": "sha512-82hOAu7y0dbVuFfmO4bYF1XEwYk/mEbM5E+b1jgci/udUBEE/R7LF5Ip0CCEmXe8AybRM8L+04eP+LGZeDvkiw==",
|
||||
"peerDependencies": {
|
||||
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
@ -11852,7 +11853,6 @@
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
||||
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
@ -11861,7 +11861,6 @@
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
|
||||
"integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
@ -11880,7 +11879,6 @@
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
|
||||
"integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
@ -12466,7 +12464,6 @@
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
|
||||
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"mime": "cli.js"
|
||||
},
|
||||
@ -12558,6 +12555,7 @@
|
||||
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz",
|
||||
"integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"dompurify": "3.2.7",
|
||||
"marked": "14.0.0"
|
||||
@ -12652,6 +12650,7 @@
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@inquirer/confirm": "^5.0.0",
|
||||
"@mswjs/interceptors": "^0.40.0",
|
||||
@ -12998,7 +12997,6 @@
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
||||
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ee-first": "1.1.1"
|
||||
},
|
||||
@ -13203,7 +13201,6 @@
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
@ -13247,8 +13244,7 @@
|
||||
"node_modules/path-to-regexp": {
|
||||
"version": "0.1.12",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
|
||||
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
|
||||
"license": "MIT"
|
||||
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="
|
||||
},
|
||||
"node_modules/path-type": {
|
||||
"version": "4.0.0",
|
||||
@ -13379,6 +13375,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
@ -13403,11 +13400,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/posthog-js": {
|
||||
"version": "1.309.0",
|
||||
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.309.0.tgz",
|
||||
"integrity": "sha512-SmFF0uKX3tNTgQOW4mR4shGLQ0YYG0FXyKTz13SbIH83/FtAJedppOIL7s0y9e7rjogBh6LsPekphhchs9Kh1Q==",
|
||||
"version": "1.309.1",
|
||||
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.309.1.tgz",
|
||||
"integrity": "sha512-JUJcQhYzNNKO0cgnSbowCsVi2RTu75XGZ2EmnTQti4tMGRCTOv/HCnZasdFniBGZ0rLugQkaScYca/84Ta2u5Q==",
|
||||
"dependencies": {
|
||||
"@posthog/core": "1.8.0",
|
||||
"@posthog/core": "1.8.1",
|
||||
"core-js": "^3.38.1",
|
||||
"fflate": "^0.4.8",
|
||||
"preact": "^10.19.3",
|
||||
@ -13440,6 +13437,7 @@
|
||||
"integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
},
|
||||
@ -13551,7 +13549,6 @@
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"forwarded": "0.2.0",
|
||||
"ipaddr.js": "1.9.1"
|
||||
@ -13580,7 +13577,6 @@
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
|
||||
"integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"side-channel": "^1.1.0"
|
||||
},
|
||||
@ -13616,7 +13612,6 @@
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
||||
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
@ -13625,7 +13620,6 @@
|
||||
"version": "2.5.3",
|
||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
|
||||
"integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bytes": "~3.1.2",
|
||||
"http-errors": "~2.0.1",
|
||||
@ -13640,6 +13634,7 @@
|
||||
"version": "19.2.3",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
|
||||
"integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@ -13688,6 +13683,7 @@
|
||||
"version": "19.2.3",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz",
|
||||
"integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.27.0"
|
||||
},
|
||||
@ -13797,10 +13793,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-router": {
|
||||
"version": "7.10.1",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.10.1.tgz",
|
||||
"integrity": "sha512-gHL89dRa3kwlUYtRQ+m8NmxGI6CgqN+k4XyGjwcFoQwwCWF6xXpOCUlDovkXClS0d0XJN/5q7kc5W3kiFEd0Yw==",
|
||||
"license": "MIT",
|
||||
"version": "7.11.0",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.11.0.tgz",
|
||||
"integrity": "sha512-uI4JkMmjbWCZc01WVP2cH7ZfSzH91JAZUDd7/nIprDgWxBV1TkkmLToFh7EbMTcMak8URFRa2YoBL/W8GWnCTQ==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"cookie": "^1.0.1",
|
||||
"set-cookie-parser": "^2.6.0"
|
||||
@ -14162,6 +14158,7 @@
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz",
|
||||
"integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.8"
|
||||
},
|
||||
@ -14365,10 +14362,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/send": {
|
||||
"version": "0.19.1",
|
||||
"resolved": "https://registry.npmjs.org/send/-/send-0.19.1.tgz",
|
||||
"integrity": "sha512-p4rRk4f23ynFEfcD9LA0xRYngj+IyGiEYyqqOak8kaN0TvNmuxC2dcVeBn62GpCeR2CpWqyHCNScTP91QbAVFg==",
|
||||
"license": "MIT",
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
|
||||
"integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==",
|
||||
"dependencies": {
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
@ -14376,13 +14372,13 @@
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"etag": "~1.8.1",
|
||||
"fresh": "0.5.2",
|
||||
"http-errors": "2.0.0",
|
||||
"fresh": "~0.5.2",
|
||||
"http-errors": "~2.0.1",
|
||||
"mime": "1.6.0",
|
||||
"ms": "2.1.3",
|
||||
"on-finished": "2.4.1",
|
||||
"on-finished": "~2.4.1",
|
||||
"range-parser": "~1.2.1",
|
||||
"statuses": "2.0.1"
|
||||
"statuses": "~2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
@ -14392,7 +14388,6 @@
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
@ -14400,122 +14395,22 @@
|
||||
"node_modules/send/node_modules/debug/node_modules/ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/send/node_modules/http-errors": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
|
||||
"integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"depd": "2.0.0",
|
||||
"inherits": "2.0.4",
|
||||
"setprototypeof": "1.2.0",
|
||||
"statuses": "2.0.1",
|
||||
"toidentifier": "1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/send/node_modules/statuses": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
|
||||
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
|
||||
},
|
||||
"node_modules/serve-static": {
|
||||
"version": "1.16.2",
|
||||
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
|
||||
"integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
|
||||
"license": "MIT",
|
||||
"version": "1.16.3",
|
||||
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz",
|
||||
"integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==",
|
||||
"dependencies": {
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"parseurl": "~1.3.3",
|
||||
"send": "0.19.0"
|
||||
"send": "~0.19.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/serve-static/node_modules/debug": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/serve-static/node_modules/debug/node_modules/ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/serve-static/node_modules/http-errors": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
|
||||
"integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"depd": "2.0.0",
|
||||
"inherits": "2.0.4",
|
||||
"setprototypeof": "1.2.0",
|
||||
"statuses": "2.0.1",
|
||||
"toidentifier": "1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/serve-static/node_modules/send": {
|
||||
"version": "0.19.0",
|
||||
"resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
|
||||
"integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
"destroy": "1.2.0",
|
||||
"encodeurl": "~1.0.2",
|
||||
"escape-html": "~1.0.3",
|
||||
"etag": "~1.8.1",
|
||||
"fresh": "0.5.2",
|
||||
"http-errors": "2.0.0",
|
||||
"mime": "1.6.0",
|
||||
"ms": "2.1.3",
|
||||
"on-finished": "2.4.1",
|
||||
"range-parser": "~1.2.1",
|
||||
"statuses": "2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/serve-static/node_modules/send/node_modules/encodeurl": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
|
||||
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/serve-static/node_modules/statuses": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
|
||||
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/set-cookie-parser": {
|
||||
"version": "2.7.2",
|
||||
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
|
||||
@ -14574,8 +14469,7 @@
|
||||
"node_modules/setprototypeof": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
|
||||
"license": "ISC"
|
||||
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
|
||||
},
|
||||
"node_modules/shebang-command": {
|
||||
"version": "2.0.0",
|
||||
@ -15260,6 +15154,7 @@
|
||||
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.4.0.tgz",
|
||||
"integrity": "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/dcastil"
|
||||
@ -15386,6 +15281,7 @@
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@ -15440,7 +15336,6 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
}
|
||||
@ -15596,7 +15491,6 @@
|
||||
"version": "1.6.18",
|
||||
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
|
||||
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"media-typer": "0.3.0",
|
||||
"mime-types": "~2.1.24"
|
||||
@ -15689,6 +15583,7 @@
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@ -15814,7 +15709,6 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
@ -15935,7 +15829,6 @@
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
|
||||
"integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
@ -15996,6 +15889,7 @@
|
||||
"version": "7.3.0",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.0.tgz",
|
||||
"integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.27.0",
|
||||
"fdir": "^6.5.0",
|
||||
@ -16111,9 +16005,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite-tsconfig-paths": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-6.0.2.tgz",
|
||||
"integrity": "sha512-c06LOO8fWB5RuEPpEIHXU9t7Dt4DoiPIljnKws9UygIaQo6PoFKawTftz5/QVcO+6pOs/HHWycnq4UrZkWVYnQ==",
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-6.0.3.tgz",
|
||||
"integrity": "sha512-7bL7FPX/DSviaZGYUKowWF1AiDVWjMjxNbE8lyaVGDezkedWqfGhlnQ4BZXre0ZN5P4kAgIJfAlgFDVyjrCIyg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"debug": "^4.1.1",
|
||||
@ -16165,6 +16059,7 @@
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@ -16177,6 +16072,7 @@
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.16.tgz",
|
||||
"integrity": "sha512-E4t7DJ9pESL6E3I8nFjPa4xGUd3PmiWDLsDztS2qXSJWfHtbQnwAWylaBvSNY48I3vr8PTqIZlyK8TE3V3CA4Q==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@vitest/expect": "4.0.16",
|
||||
"@vitest/mocker": "4.0.16",
|
||||
|
||||
@ -10,8 +10,8 @@
|
||||
"@heroui/react": "2.8.6",
|
||||
"@microlink/react-json-view": "^1.26.2",
|
||||
"@monaco-editor/react": "^4.7.0-rc.0",
|
||||
"@react-router/node": "^7.10.1",
|
||||
"@react-router/serve": "^7.10.1",
|
||||
"@react-router/node": "^7.11.0",
|
||||
"@react-router/serve": "^7.11.0",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@tanstack/react-query": "^5.90.12",
|
||||
"@uidotdev/usehooks": "^2.4.1",
|
||||
@ -27,16 +27,16 @@
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"i18next-http-backend": "^3.0.2",
|
||||
"isbot": "^5.1.32",
|
||||
"lucide-react": "^0.561.0",
|
||||
"lucide-react": "^0.562.0",
|
||||
"monaco-editor": "^0.55.1",
|
||||
"posthog-js": "^1.309.0",
|
||||
"posthog-js": "^1.309.1",
|
||||
"react": "^19.2.3",
|
||||
"react-dom": "^19.2.3",
|
||||
"react-hot-toast": "^2.6.0",
|
||||
"react-i18next": "^16.5.0",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-router": "^7.10.1",
|
||||
"react-router": "^7.11.0",
|
||||
"react-syntax-highlighter": "^16.1.0",
|
||||
"remark-breaks": "^4.0.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
@ -82,7 +82,7 @@
|
||||
"devDependencies": {
|
||||
"@mswjs/socket.io-binding": "^0.2.0",
|
||||
"@playwright/test": "^1.57.0",
|
||||
"@react-router/dev": "^7.10.1",
|
||||
"@react-router/dev": "^7.11.0",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"@tanstack/eslint-plugin-query": "^5.91.0",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
@ -116,7 +116,7 @@
|
||||
"tailwindcss": "^4.1.8",
|
||||
"typescript": "^5.9.3",
|
||||
"vite-plugin-svgr": "^4.5.0",
|
||||
"vite-tsconfig-paths": "^6.0.2",
|
||||
"vite-tsconfig-paths": "^6.0.3",
|
||||
"vitest": "^4.0.14"
|
||||
},
|
||||
"packageManager": "npm@10.5.0",
|
||||
|
||||
@ -12,10 +12,9 @@ export function BrowserPanel() {
|
||||
reset();
|
||||
}, [conversationId, reset]);
|
||||
|
||||
const imgSrc =
|
||||
screenshotSrc && screenshotSrc.startsWith("data:image/png;base64,")
|
||||
? screenshotSrc
|
||||
: `data:image/png;base64,${screenshotSrc || ""}`;
|
||||
const imgSrc = screenshotSrc?.startsWith("data:image/png;base64,")
|
||||
? screenshotSrc
|
||||
: `data:image/png;base64,${screenshotSrc ?? ""}`;
|
||||
|
||||
return (
|
||||
<div className="h-full w-full flex flex-col text-neutral-400">
|
||||
|
||||
@ -5,7 +5,7 @@ import { I18nKey } from "#/i18n/declaration";
|
||||
import CodeTagIcon from "#/icons/code-tag.svg?react";
|
||||
import ChevronDownSmallIcon from "#/icons/chevron-down-small.svg?react";
|
||||
import LessonPlanIcon from "#/icons/lesson-plan.svg?react";
|
||||
import { useConversationStore } from "#/state/conversation-store";
|
||||
import { useConversationStore } from "#/stores/conversation-store";
|
||||
import { ChangeAgentContextMenu } from "./change-agent-context-menu";
|
||||
import { cn } from "#/utils/utils";
|
||||
import { USE_PLANNING_AGENT } from "#/utils/feature-flags";
|
||||
|
||||
@ -38,7 +38,7 @@ import {
|
||||
import { useUnifiedUploadFiles } from "#/hooks/mutation/use-unified-upload-files";
|
||||
import { useConfig } from "#/hooks/query/use-config";
|
||||
import { validateFiles } from "#/utils/file-validation";
|
||||
import { useConversationStore } from "#/state/conversation-store";
|
||||
import { useConversationStore } from "#/stores/conversation-store";
|
||||
import ConfirmationModeEnabled from "./confirmation-mode-enabled";
|
||||
import {
|
||||
isV0Event,
|
||||
|
||||
@ -4,7 +4,7 @@ import { Suggestions } from "#/components/features/suggestions/suggestions";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import BuildIt from "#/icons/build-it.svg?react";
|
||||
import { SUGGESTIONS } from "#/utils/suggestions";
|
||||
import { useConversationStore } from "#/state/conversation-store";
|
||||
import { useConversationStore } from "#/stores/conversation-store";
|
||||
|
||||
interface ChatSuggestionsProps {
|
||||
onSuggestionsClick: (value: string) => void;
|
||||
|
||||
@ -3,7 +3,7 @@ import { DragOver } from "../drag-over";
|
||||
import { UploadedFiles } from "../uploaded-files";
|
||||
import { ChatInputRow } from "./chat-input-row";
|
||||
import { ChatInputActions } from "./chat-input-actions";
|
||||
import { useConversationStore } from "#/state/conversation-store";
|
||||
import { useConversationStore } from "#/stores/conversation-store";
|
||||
import { cn } from "#/utils/utils";
|
||||
|
||||
interface ChatInputContainerProps {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { useConversationStore } from "#/state/conversation-store";
|
||||
import { useConversationStore } from "#/stores/conversation-store";
|
||||
|
||||
interface ChatInputFieldProps {
|
||||
chatInputRef: React.RefObject<HTMLDivElement | null>;
|
||||
|
||||
@ -8,7 +8,7 @@ import { useChatSubmission } from "#/hooks/chat/use-chat-submission";
|
||||
import { ChatInputGrip } from "./components/chat-input-grip";
|
||||
import { ChatInputContainer } from "./components/chat-input-container";
|
||||
import { HiddenFileInput } from "./components/hidden-file-input";
|
||||
import { useConversationStore } from "#/state/conversation-store";
|
||||
import { useConversationStore } from "#/stores/conversation-store";
|
||||
|
||||
export interface CustomChatInputProps {
|
||||
disabled?: boolean;
|
||||
|
||||
@ -140,7 +140,7 @@ const getTaskTrackingObservationContent = (
|
||||
content += "\n\n**Task List:** Empty";
|
||||
}
|
||||
|
||||
if (event.content && event.content.trim()) {
|
||||
if (event.content?.trim()) {
|
||||
content += `\n\n**Result:** ${event.content.trim()}`;
|
||||
}
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@ import { CustomChatInput } from "./custom-chat-input";
|
||||
import { AgentState } from "#/types/agent-state";
|
||||
import { useActiveConversation } from "#/hooks/query/use-active-conversation";
|
||||
import { GitControlBar } from "./git-control-bar";
|
||||
import { useConversationStore } from "#/state/conversation-store";
|
||||
import { useConversationStore } from "#/stores/conversation-store";
|
||||
import { useAgentState } from "#/hooks/use-agent-state";
|
||||
import { processFiles, processImages } from "#/utils/file-processing";
|
||||
import { useSubConversationTaskPolling } from "#/hooks/query/use-sub-conversation-task-polling";
|
||||
|
||||
@ -192,8 +192,7 @@ export const Messages: React.FC<MessagesProps> = React.memo(
|
||||
) => {
|
||||
const conversationInstructions = `Target file: ${target}\n\nDescription: ${query}\n\nTriggers: ${triggers.join(", ")}`;
|
||||
if (
|
||||
!conversation ||
|
||||
!conversation.selected_repository ||
|
||||
!conversation?.selected_repository ||
|
||||
!conversation.selected_branch ||
|
||||
!conversation.git_provider ||
|
||||
!selectedEventId
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { UploadedFile } from "./uploaded-file";
|
||||
import { UploadedImage } from "./uploaded-image";
|
||||
import { useConversationStore } from "#/state/conversation-store";
|
||||
import { useConversationStore } from "#/stores/conversation-store";
|
||||
|
||||
export function UploadedFiles() {
|
||||
const {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useEffect } from "react";
|
||||
import { useStatusStore } from "#/state/status-store";
|
||||
import { useStatusStore } from "#/stores/status-store";
|
||||
import { useActiveConversation } from "#/hooks/query/use-active-conversation";
|
||||
import { getStatusCode } from "#/utils/status";
|
||||
import { ChatStopButton } from "../chat/chat-stop-button";
|
||||
@ -9,7 +9,7 @@ import ClockIcon from "#/icons/u-clock-three.svg?react";
|
||||
import { ChatResumeAgentButton } from "../chat/chat-play-button";
|
||||
import { cn, isTaskPolling } from "#/utils/utils";
|
||||
import { AgentLoading } from "./agent-loading";
|
||||
import { useConversationStore } from "#/state/conversation-store";
|
||||
import { useConversationStore } from "#/stores/conversation-store";
|
||||
import CircleErrorIcon from "#/icons/circle-error.svg?react";
|
||||
import { useAgentState } from "#/hooks/use-agent-state";
|
||||
import { useUnifiedWebSocketStatus } from "#/hooks/use-unified-websocket-status";
|
||||
|
||||
@ -10,7 +10,7 @@ import {
|
||||
getCreatePRPrompt,
|
||||
getCreateNewBranchPrompt,
|
||||
} from "#/utils/utils";
|
||||
import { useConversationStore } from "#/state/conversation-store";
|
||||
import { useConversationStore } from "#/stores/conversation-store";
|
||||
|
||||
import ArrowUpIcon from "#/icons/u-arrow-up.svg?react";
|
||||
import ArrowDownIcon from "#/icons/u-arrow-down.svg?react";
|
||||
|
||||
@ -8,7 +8,7 @@ import PrStatusIcon from "#/icons/pr-status.svg?react";
|
||||
import DocumentIcon from "#/icons/document.svg?react";
|
||||
import WaterIcon from "#/icons/u-water.svg?react";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { useConversationStore } from "#/state/conversation-store";
|
||||
import { useConversationStore } from "#/stores/conversation-store";
|
||||
import { REPO_SUGGESTIONS } from "#/utils/suggestions/repo-suggestions";
|
||||
import { CONTEXT_MENU_ICON_TEXT_CLASSNAME } from "#/utils/constants";
|
||||
|
||||
|
||||
@ -20,7 +20,7 @@ export function ConversationPanelWrapper({
|
||||
return ReactDOM.createPortal(
|
||||
<div
|
||||
className={cn(
|
||||
"absolute h-full w-full left-0 top-0 z-[9999] bg-black/80 rounded-xl",
|
||||
"absolute h-full w-full left-0 top-0 z-[100] bg-black/80 rounded-xl",
|
||||
pathname === "/" && "bottom-0 top-0 md:top-3 md:bottom-3 h-auto",
|
||||
)}
|
||||
>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { useWindowSize } from "@uidotdev/usehooks";
|
||||
import { MobileLayout } from "./mobile-layout";
|
||||
import { DesktopLayout } from "./desktop-layout";
|
||||
import { useConversationStore } from "#/state/conversation-store";
|
||||
import { useConversationStore } from "#/stores/conversation-store";
|
||||
|
||||
export function ConversationMain() {
|
||||
const { width } = useWindowSize();
|
||||
|
||||
@ -7,7 +7,7 @@ import { TabContainer } from "./tab-container";
|
||||
import { TabContentArea } from "./tab-content-area";
|
||||
import { ConversationTabTitle } from "../conversation-tab-title";
|
||||
import Terminal from "#/components/features/terminal/terminal";
|
||||
import { useConversationStore } from "#/state/conversation-store";
|
||||
import { useConversationStore } from "#/stores/conversation-store";
|
||||
import { useConversationId } from "#/hooks/use-conversation-id";
|
||||
|
||||
// Lazy load all tab components
|
||||
|
||||
@ -16,7 +16,7 @@ import { VSCodeTooltipContent } from "./vscode-tooltip-content";
|
||||
import {
|
||||
useConversationStore,
|
||||
type ConversationTab,
|
||||
} from "#/state/conversation-store";
|
||||
} from "#/stores/conversation-store";
|
||||
import { ConversationTabsContextMenu } from "./conversation-tabs-context-menu";
|
||||
import { USE_PLANNING_AGENT } from "#/utils/feature-flags";
|
||||
import { useConversationId } from "#/hooks/use-conversation-id";
|
||||
|
||||
@ -75,7 +75,7 @@ export function GitProviderDropdown({
|
||||
}
|
||||
|
||||
// If no input value, show all providers
|
||||
if (!inputValue || !inputValue.trim()) {
|
||||
if (!inputValue?.trim()) {
|
||||
return providers;
|
||||
}
|
||||
|
||||
|
||||
@ -99,7 +99,7 @@ export function GitRepoDropdown({
|
||||
);
|
||||
|
||||
// If no input value, return all recent repos for this provider
|
||||
if (!inputValue || !inputValue.trim()) {
|
||||
if (!inputValue?.trim()) {
|
||||
return providerFilteredRepos;
|
||||
}
|
||||
|
||||
@ -139,7 +139,7 @@ export function GitRepoDropdown({
|
||||
baseRepositories = repositories;
|
||||
}
|
||||
// If no input value, show all repositories
|
||||
else if (!inputValue || !inputValue.trim()) {
|
||||
else if (!inputValue?.trim()) {
|
||||
baseRepositories = repositories;
|
||||
}
|
||||
// For URL inputs, use the processed search input for filtering
|
||||
@ -246,8 +246,7 @@ export function GitRepoDropdown({
|
||||
// Create sticky footer item for GitHub provider
|
||||
const stickyFooterItem = useMemo(() => {
|
||||
if (
|
||||
!config ||
|
||||
!config.APP_SLUG ||
|
||||
!config?.APP_SLUG ||
|
||||
provider !== ProviderOptions.github ||
|
||||
config.APP_MODE !== "saas"
|
||||
)
|
||||
|
||||
@ -45,7 +45,7 @@ export function DropdownItem<T>({
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
<li key={getItemKey(item)} {...itemProps}>
|
||||
<div className="flex items-center gap-2">
|
||||
{renderIcon && renderIcon(item)}
|
||||
{renderIcon?.(item)}
|
||||
<span className="font-medium">{getDisplayText(item)}</span>
|
||||
</div>
|
||||
</li>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { useMicroagentManagementStore } from "#/state/microagent-management-store";
|
||||
import { useMicroagentManagementStore } from "#/stores/microagent-management-store";
|
||||
import { GitRepository } from "#/types/git";
|
||||
|
||||
interface MicroagentManagementAddMicroagentButtonProps {
|
||||
|
||||
@ -3,7 +3,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { MicroagentManagementSidebar } from "./microagent-management-sidebar";
|
||||
import { MicroagentManagementMain } from "./microagent-management-main";
|
||||
import { MicroagentManagementUpsertMicroagentModal } from "./microagent-management-upsert-microagent-modal";
|
||||
import { useMicroagentManagementStore } from "#/state/microagent-management-store";
|
||||
import { useMicroagentManagementStore } from "#/stores/microagent-management-store";
|
||||
import { useCreateConversationAndSubscribeMultiple } from "#/hooks/use-create-conversation-and-subscribe-multiple";
|
||||
import {
|
||||
LearnThisRepoFormData,
|
||||
|
||||
@ -2,7 +2,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { BrandButton } from "../settings/brand-button";
|
||||
import { Loader } from "#/components/shared/loader";
|
||||
import { useMicroagentManagementStore } from "#/state/microagent-management-store";
|
||||
import { useMicroagentManagementStore } from "#/stores/microagent-management-store";
|
||||
|
||||
export function MicroagentManagementConversationStopped() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@ -2,7 +2,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { BrandButton } from "../settings/brand-button";
|
||||
import { Loader } from "#/components/shared/loader";
|
||||
import { useMicroagentManagementStore } from "#/state/microagent-management-store";
|
||||
import { useMicroagentManagementStore } from "#/stores/microagent-management-store";
|
||||
|
||||
export function MicroagentManagementError() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@ -5,7 +5,7 @@ import { ModalBackdrop } from "#/components/shared/modals/modal-backdrop";
|
||||
import { ModalBody } from "#/components/shared/modals/modal-body";
|
||||
import { BrandButton } from "../settings/brand-button";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { useMicroagentManagementStore } from "#/state/microagent-management-store";
|
||||
import { useMicroagentManagementStore } from "#/stores/microagent-management-store";
|
||||
import XIcon from "#/icons/x.svg?react";
|
||||
import { cn, getRepoMdCreatePrompt } from "#/utils/utils";
|
||||
import { LearnThisRepoFormData } from "#/types/microagent-management";
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { useMicroagentManagementStore } from "#/state/microagent-management-store";
|
||||
import { useMicroagentManagementStore } from "#/stores/microagent-management-store";
|
||||
import { GitRepository } from "#/types/git";
|
||||
|
||||
interface MicroagentManagementLearnThisRepoProps {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { useMicroagentManagementStore } from "#/state/microagent-management-store";
|
||||
import { useMicroagentManagementStore } from "#/stores/microagent-management-store";
|
||||
import { MicroagentManagementDefault } from "./microagent-management-default";
|
||||
import { MicroagentManagementOpeningPr } from "./microagent-management-opening-pr";
|
||||
import { MicroagentManagementReviewPr } from "./microagent-management-review-pr";
|
||||
|
||||
@ -3,7 +3,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { RepositoryMicroagent } from "#/types/microagent-management";
|
||||
import { Conversation } from "#/api/open-hands.types";
|
||||
import { useMicroagentManagementStore } from "#/state/microagent-management-store";
|
||||
import { useMicroagentManagementStore } from "#/stores/microagent-management-store";
|
||||
import { cn } from "#/utils/utils";
|
||||
import { GitRepository } from "#/types/git";
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { BrandButton } from "../settings/brand-button";
|
||||
import { Loader } from "#/components/shared/loader";
|
||||
import { useMicroagentManagementStore } from "#/state/microagent-management-store";
|
||||
import { useMicroagentManagementStore } from "#/stores/microagent-management-store";
|
||||
|
||||
export function MicroagentManagementOpeningPr() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@ -6,7 +6,7 @@ import { MicroagentManagementLearnThisRepo } from "./microagent-management-learn
|
||||
import { useRepositoryMicroagents } from "#/hooks/query/use-repository-microagents";
|
||||
import { useMicroagentManagementConversations } from "#/hooks/query/use-microagent-management-conversations";
|
||||
import { GitRepository } from "#/types/git";
|
||||
import { useMicroagentManagementStore } from "#/state/microagent-management-store";
|
||||
import { useMicroagentManagementStore } from "#/stores/microagent-management-store";
|
||||
import { cn } from "#/utils/utils";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@ import { I18nKey } from "#/i18n/declaration";
|
||||
import { BrandButton } from "../settings/brand-button";
|
||||
import { getProviderName, constructPullRequestUrl } from "#/utils/utils";
|
||||
import { Provider } from "#/types/settings";
|
||||
import { useMicroagentManagementStore } from "#/state/microagent-management-store";
|
||||
import { useMicroagentManagementStore } from "#/stores/microagent-management-store";
|
||||
|
||||
export function MicroagentManagementReviewPr() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@ -2,7 +2,7 @@ import { Tab, Tabs } from "@heroui/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { MicroagentManagementRepositories } from "./microagent-management-repositories";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { useMicroagentManagementStore } from "#/state/microagent-management-store";
|
||||
import { useMicroagentManagementStore } from "#/stores/microagent-management-store";
|
||||
|
||||
interface MicroagentManagementSidebarTabsProps {
|
||||
isSearchLoading?: boolean;
|
||||
|
||||
@ -6,7 +6,7 @@ import { MicroagentManagementSidebarTabs } from "./microagent-management-sidebar
|
||||
import { useGitRepositories } from "#/hooks/query/use-git-repositories";
|
||||
import { useSearchRepositories } from "#/hooks/query/use-search-repositories";
|
||||
import { GitProviderDropdown } from "#/components/features/home/git-provider-dropdown";
|
||||
import { useMicroagentManagementStore } from "#/state/microagent-management-store";
|
||||
import { useMicroagentManagementStore } from "#/stores/microagent-management-store";
|
||||
import { GitRepository } from "#/types/git";
|
||||
import { Provider } from "#/types/settings";
|
||||
import {
|
||||
|
||||
@ -5,7 +5,7 @@ import { ModalBackdrop } from "#/components/shared/modals/modal-backdrop";
|
||||
import { ModalBody } from "#/components/shared/modals/modal-body";
|
||||
import { BrandButton } from "../settings/brand-button";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { useMicroagentManagementStore } from "#/state/microagent-management-store";
|
||||
import { useMicroagentManagementStore } from "#/stores/microagent-management-store";
|
||||
import XIcon from "#/icons/x.svg?react";
|
||||
import { cn, extractRepositoryInfo } from "#/utils/utils";
|
||||
import { BadgeInput } from "#/components/shared/inputs/badge-input";
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Spinner } from "@heroui/react";
|
||||
import { useMicroagentManagementStore } from "#/state/microagent-management-store";
|
||||
import { useMicroagentManagementStore } from "#/stores/microagent-management-store";
|
||||
import { useRepositoryMicroagentContent } from "#/hooks/query/use-repository-microagent-content";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { extractRepositoryInfo } from "#/utils/utils";
|
||||
|
||||
@ -2,7 +2,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { BrandButton } from "../settings/brand-button";
|
||||
import { getProviderName, constructMicroagentUrl } from "#/utils/utils";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { useMicroagentManagementStore } from "#/state/microagent-management-store";
|
||||
import { useMicroagentManagementStore } from "#/stores/microagent-management-store";
|
||||
|
||||
export function MicroagentManagementViewMicroagentHeader() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { useMicroagentManagementStore } from "#/state/microagent-management-store";
|
||||
import { useMicroagentManagementStore } from "#/stores/microagent-management-store";
|
||||
import { MicroagentManagementViewMicroagentHeader } from "./microagent-management-view-microagent-header";
|
||||
import { MicroagentManagementViewMicroagentContent } from "./microagent-management-view-microagent-content";
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@ export const parseMessageFromEvent = (event: MessageEvent): string => {
|
||||
const message = event.llm_message;
|
||||
|
||||
// Safety check: ensure llm_message exists and has content
|
||||
if (!message || !message.content) {
|
||||
if (!message?.content) {
|
||||
return "";
|
||||
}
|
||||
|
||||
|
||||
@ -14,7 +14,7 @@ import { useEventStore } from "#/stores/use-event-store";
|
||||
import { useErrorMessageStore } from "#/stores/error-message-store";
|
||||
import { useOptimisticUserMessageStore } from "#/stores/optimistic-user-message-store";
|
||||
import { useV1ConversationStateStore } from "#/stores/v1-conversation-state-store";
|
||||
import { useCommandStore } from "#/state/command-store";
|
||||
import { useCommandStore } from "#/stores/command-store";
|
||||
import { useBrowserStore } from "#/stores/browser-store";
|
||||
import {
|
||||
isV1Event,
|
||||
@ -40,7 +40,7 @@ import type {
|
||||
V1SendMessageRequest,
|
||||
} from "#/api/conversation-service/v1-conversation-service.types";
|
||||
import EventService from "#/api/event-service/event-service.api";
|
||||
import { useConversationStore } from "#/state/conversation-store";
|
||||
import { useConversationStore } from "#/stores/conversation-store";
|
||||
import { isBudgetOrCreditError } from "#/utils/error-handler";
|
||||
import { useTracking } from "#/hooks/use-tracking";
|
||||
import { useReadConversationFile } from "#/hooks/mutation/use-read-conversation-file";
|
||||
|
||||
@ -4,7 +4,7 @@ import {
|
||||
clearEmptyContent,
|
||||
getTextContent,
|
||||
} from "#/components/features/chat/utils/chat-input.utils";
|
||||
import { useConversationStore } from "#/state/conversation-store";
|
||||
import { useConversationStore } from "#/stores/conversation-store";
|
||||
|
||||
/**
|
||||
* Hook for managing chat input content logic
|
||||
|
||||
@ -4,7 +4,7 @@ import { CHAT_INPUT } from "#/utils/constants";
|
||||
import {
|
||||
IMessageToSend,
|
||||
useConversationStore,
|
||||
} from "#/state/conversation-store";
|
||||
} from "#/stores/conversation-store";
|
||||
|
||||
/**
|
||||
* Hook for managing grip resize functionality
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import MicroagentManagementService from "#/ui/microagent-management-service/microagent-management-service.api";
|
||||
import MicroagentManagementService from "#/api/microagent-management-service/microagent-management-service.api";
|
||||
|
||||
export const useMicroagentManagementConversations = (
|
||||
selectedRepository: string,
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { useCallback, useEffect, RefObject, useRef } from "react";
|
||||
import { IMessageToSend } from "#/state/conversation-store";
|
||||
import { IMessageToSend } from "#/stores/conversation-store";
|
||||
import { EPS } from "#/utils/constants";
|
||||
import { getStyleHeightPx, setStyleHeightPx } from "#/utils/utils";
|
||||
import { useDragResize } from "./use-drag-resize";
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { useCallback } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { useConversationStore } from "#/state/conversation-store";
|
||||
import { useConversationStore } from "#/stores/conversation-store";
|
||||
import { useActiveConversation } from "#/hooks/query/use-active-conversation";
|
||||
import { useCreateConversation } from "#/hooks/mutation/use-create-conversation";
|
||||
import { displaySuccessToast } from "#/utils/custom-toast-handlers";
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { FitAddon } from "@xterm/addon-fit";
|
||||
import { Terminal } from "@xterm/xterm";
|
||||
import React from "react";
|
||||
import { Command, useCommandStore } from "#/state/command-store";
|
||||
import { Command, useCommandStore } from "#/stores/command-store";
|
||||
import { parseTerminalOutput } from "#/utils/parse-terminal-output";
|
||||
|
||||
/*
|
||||
@ -29,6 +29,47 @@ const renderCommand = (
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the terminal is ready for fit operations.
|
||||
* This prevents the "Cannot read properties of undefined (reading 'dimensions')" error
|
||||
* that occurs when fit() is called on a terminal that is hidden, disposed, or not fully initialized.
|
||||
*/
|
||||
const canFitTerminal = (
|
||||
terminalInstance: Terminal | null,
|
||||
fitAddonInstance: FitAddon | null,
|
||||
containerElement: HTMLDivElement | null,
|
||||
): boolean => {
|
||||
// Check terminal and fitAddon exist
|
||||
if (!terminalInstance || !fitAddonInstance) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check container element exists
|
||||
if (!containerElement) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check element is visible (not display: none)
|
||||
// When display is none, offsetParent is null (except for fixed/body elements)
|
||||
const computedStyle = window.getComputedStyle(containerElement);
|
||||
if (computedStyle.display === "none") {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check element has dimensions
|
||||
const { clientWidth, clientHeight } = containerElement;
|
||||
if (clientWidth === 0 || clientHeight === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check terminal has been opened (element property is set after open())
|
||||
if (!terminalInstance.element) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
// Create a persistent reference that survives component unmounts
|
||||
// This ensures terminal history is preserved when navigating away and back
|
||||
const persistentLastCommandIndex = { current: 0 };
|
||||
@ -39,6 +80,7 @@ export const useTerminal = () => {
|
||||
const fitAddon = React.useRef<FitAddon | null>(null);
|
||||
const ref = React.useRef<HTMLDivElement>(null);
|
||||
const lastCommandIndex = persistentLastCommandIndex; // Use the persistent reference
|
||||
const isDisposed = React.useRef(false);
|
||||
|
||||
const createTerminal = () =>
|
||||
new Terminal({
|
||||
@ -55,6 +97,15 @@ export const useTerminal = () => {
|
||||
},
|
||||
});
|
||||
|
||||
const fitTerminalSafely = React.useCallback(() => {
|
||||
if (isDisposed.current) {
|
||||
return;
|
||||
}
|
||||
if (canFitTerminal(terminal.current, fitAddon.current, ref.current)) {
|
||||
fitAddon.current!.fit();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const initializeTerminal = () => {
|
||||
if (terminal.current) {
|
||||
if (fitAddon.current) terminal.current.loadAddon(fitAddon.current);
|
||||
@ -62,13 +113,14 @@ export const useTerminal = () => {
|
||||
terminal.current.open(ref.current);
|
||||
// Hide cursor for read-only terminal using ANSI escape sequence
|
||||
terminal.current.write("\x1b[?25l");
|
||||
fitAddon.current?.fit();
|
||||
fitTerminalSafely();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize terminal and handle cleanup
|
||||
React.useEffect(() => {
|
||||
isDisposed.current = false;
|
||||
terminal.current = createTerminal();
|
||||
fitAddon.current = new FitAddon();
|
||||
|
||||
@ -91,6 +143,7 @@ export const useTerminal = () => {
|
||||
}
|
||||
|
||||
return () => {
|
||||
isDisposed.current = true;
|
||||
terminal.current?.dispose();
|
||||
lastCommandIndex.current = 0;
|
||||
};
|
||||
@ -118,7 +171,10 @@ export const useTerminal = () => {
|
||||
let resizeObserver: ResizeObserver | null = null;
|
||||
|
||||
resizeObserver = new ResizeObserver(() => {
|
||||
fitAddon.current?.fit();
|
||||
// Use requestAnimationFrame to debounce resize events and ensure DOM is ready
|
||||
requestAnimationFrame(() => {
|
||||
fitTerminalSafely();
|
||||
});
|
||||
});
|
||||
|
||||
if (ref.current) {
|
||||
@ -128,7 +184,7 @@ export const useTerminal = () => {
|
||||
return () => {
|
||||
resizeObserver?.disconnect();
|
||||
};
|
||||
}, []);
|
||||
}, [fitTerminalSafely]);
|
||||
|
||||
return ref;
|
||||
};
|
||||
|
||||
@ -436,6 +436,8 @@ export enum I18nKey {
|
||||
BUTTON$CREATE = "BUTTON$CREATE",
|
||||
BUTTON$DELETE = "BUTTON$DELETE",
|
||||
BUTTON$COPY_TO_CLIPBOARD = "BUTTON$COPY_TO_CLIPBOARD",
|
||||
BUTTON$HOME = "BUTTON$HOME",
|
||||
BUTTON$OPEN_IN_NEW_TAB = "BUTTON$OPEN_IN_NEW_TAB",
|
||||
BUTTON$REFRESH = "BUTTON$REFRESH",
|
||||
ERROR$REQUIRED_FIELD = "ERROR$REQUIRED_FIELD",
|
||||
PLANNER$EMPTY_MESSAGE = "PLANNER$EMPTY_MESSAGE",
|
||||
|
||||
@ -6975,6 +6975,38 @@
|
||||
"es": "Copiar al portapapeles",
|
||||
"tr": "Panoya Kopyala"
|
||||
},
|
||||
"BUTTON$HOME": {
|
||||
"en": "Home",
|
||||
"ja": "ホーム",
|
||||
"zh-CN": "主页",
|
||||
"zh-TW": "首頁",
|
||||
"ko-KR": "홈",
|
||||
"no": "Hjem",
|
||||
"it": "Home",
|
||||
"pt": "Início",
|
||||
"es": "Inicio",
|
||||
"ar": "الرئيسية",
|
||||
"fr": "Accueil",
|
||||
"tr": "Ana Sayfa",
|
||||
"de": "Startseite",
|
||||
"uk": "Головна"
|
||||
},
|
||||
"BUTTON$OPEN_IN_NEW_TAB": {
|
||||
"en": "Open in New Tab",
|
||||
"ja": "新しいタブで開く",
|
||||
"zh-CN": "在新标签页中打开",
|
||||
"zh-TW": "在新分頁中開啟",
|
||||
"ko-KR": "새 탭에서 열기",
|
||||
"no": "Åpne i ny fane",
|
||||
"it": "Apri in una nuova scheda",
|
||||
"pt": "Abrir em nova aba",
|
||||
"es": "Abrir en nueva pestaña",
|
||||
"ar": "فتح في علامة تبويب جديدة",
|
||||
"fr": "Ouvrir dans un nouvel onglet",
|
||||
"tr": "Yeni Sekmede Aç",
|
||||
"de": "In neuem Tab öffnen",
|
||||
"uk": "Відкрити в новій вкладці"
|
||||
},
|
||||
"BUTTON$REFRESH": {
|
||||
"en": "Refresh",
|
||||
"ja": "更新",
|
||||
|
||||
@ -34,7 +34,7 @@ export const SECRETS_HANDLERS = [
|
||||
|
||||
http.post("/api/secrets", async ({ request }) => {
|
||||
const body = (await request.json()) as CustomSecret;
|
||||
if (typeof body === "object" && body && body.name) {
|
||||
if (typeof body === "object" && body?.name) {
|
||||
secrets.set(body.name, body);
|
||||
return HttpResponse.json(true);
|
||||
}
|
||||
@ -48,7 +48,7 @@ export const SECRETS_HANDLERS = [
|
||||
|
||||
if (typeof id === "string" && typeof body === "object") {
|
||||
const secret = secrets.get(id);
|
||||
if (secret && body && body.name) {
|
||||
if (secret && body?.name) {
|
||||
const newSecret: CustomSecret = { ...secret, ...body };
|
||||
secrets.delete(id);
|
||||
secrets.set(body.name, newSecret);
|
||||
|
||||
@ -49,9 +49,11 @@ export const SETTINGS_HANDLERS = [
|
||||
"anthropic/claude-sonnet-4-20250514",
|
||||
"anthropic/claude-sonnet-4-5-20250929",
|
||||
"anthropic/claude-haiku-4-5-20251001",
|
||||
"anthropic/claude-opus-4-5-20251101",
|
||||
"openhands/claude-sonnet-4-20250514",
|
||||
"openhands/claude-sonnet-4-5-20250929",
|
||||
"openhands/claude-haiku-4-5-20251001",
|
||||
"openhands/claude-opus-4-5-20251101",
|
||||
"sambanova/Meta-Llama-3.1-8B-Instruct",
|
||||
]),
|
||||
),
|
||||
@ -134,7 +136,7 @@ export const SETTINGS_HANDLERS = [
|
||||
const providerTokensSet: Partial<Record<Provider, string | null>> =
|
||||
Object.fromEntries(
|
||||
Object.entries(rawTokens)
|
||||
.filter(([, val]) => val && val.token)
|
||||
.filter(([, val]) => val?.token)
|
||||
.map(([provider]) => [provider as Provider, ""]),
|
||||
);
|
||||
|
||||
|
||||
@ -3,8 +3,8 @@ import { useNavigate } from "react-router";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { useConversationId } from "#/hooks/use-conversation-id";
|
||||
import { useCommandStore } from "#/state/command-store";
|
||||
import { useConversationStore } from "#/state/conversation-store";
|
||||
import { useCommandStore } from "#/stores/command-store";
|
||||
import { useConversationStore } from "#/stores/conversation-store";
|
||||
import { useAgentStore } from "#/stores/agent-store";
|
||||
import { AgentState } from "#/types/agent-state";
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@ import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import LessonPlanIcon from "#/icons/lesson-plan.svg?react";
|
||||
import { useConversationStore } from "#/state/conversation-store";
|
||||
import { useConversationStore } from "#/stores/conversation-store";
|
||||
import { useScrollToBottom } from "#/hooks/use-scroll-to-bottom";
|
||||
import { MarkdownRenderer } from "#/components/features/markdown/markdown-renderer";
|
||||
import { useHandlePlanClick } from "#/hooks/use-handle-plan-click";
|
||||
|
||||
@ -65,6 +65,7 @@ function ServedApp() {
|
||||
type="button"
|
||||
onClick={() => window.open(fullUrl, "_blank")}
|
||||
className="text-sm"
|
||||
aria-label={t(I18nKey.BUTTON$OPEN_IN_NEW_TAB)}
|
||||
>
|
||||
<FaExternalLinkAlt className="w-4 h-4" />
|
||||
</button>
|
||||
@ -72,11 +73,17 @@ function ServedApp() {
|
||||
type="button"
|
||||
onClick={() => setRefreshKey((prev) => prev + 1)}
|
||||
className="text-sm"
|
||||
aria-label={t(I18nKey.BUTTON$REFRESH)}
|
||||
>
|
||||
<FaArrowRotateRight className="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
<button type="button" onClick={() => resetUrl()} className="text-sm">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => resetUrl()}
|
||||
className="text-sm"
|
||||
aria-label={t(I18nKey.BUTTON$HOME)}
|
||||
>
|
||||
<FaHome className="w-4 h-4" />
|
||||
</button>
|
||||
<div className="w-full flex">
|
||||
|
||||
@ -51,7 +51,7 @@ function VSCodeTab() {
|
||||
);
|
||||
}
|
||||
|
||||
if (error || (data && data.error) || !data?.url || iframeError) {
|
||||
if (error || data?.error || !data?.url || iframeError) {
|
||||
return (
|
||||
<div className="w-full h-full flex items-center text-center justify-center text-2xl text-tertiary-light">
|
||||
{iframeError ||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { trackError } from "#/utils/error-handler";
|
||||
import useMetricsStore from "#/stores/metrics-store";
|
||||
import { useStatusStore } from "#/state/status-store";
|
||||
import { useStatusStore } from "#/stores/status-store";
|
||||
import ActionType from "#/types/action-type";
|
||||
import {
|
||||
ActionMessage,
|
||||
@ -8,7 +8,7 @@ import {
|
||||
StatusMessage,
|
||||
} from "#/types/message";
|
||||
import { handleObservationMessage } from "./observations";
|
||||
import { useCommandStore } from "#/state/command-store";
|
||||
import { useCommandStore } from "#/stores/command-store";
|
||||
import { queryClient } from "#/query-client-config";
|
||||
import {
|
||||
ActionSecurityRisk,
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { ObservationMessage } from "#/types/message";
|
||||
import { useCommandStore } from "#/state/command-store";
|
||||
import { useCommandStore } from "#/stores/command-store";
|
||||
import ObservationType from "#/types/observation-type";
|
||||
import { useBrowserStore } from "#/stores/browser-store";
|
||||
import { useAgentStore } from "#/stores/agent-store";
|
||||
|
||||
@ -3,7 +3,7 @@ import { Settings } from "#/types/settings";
|
||||
export const LATEST_SETTINGS_VERSION = 5;
|
||||
|
||||
export const DEFAULT_SETTINGS: Settings = {
|
||||
llm_model: "openhands/claude-sonnet-4-20250514",
|
||||
llm_model: "openhands/claude-opus-4-5-20251101",
|
||||
llm_base_url: "",
|
||||
agent: "CodeActAgent",
|
||||
language: "en",
|
||||
|
||||
@ -16,7 +16,7 @@ import {
|
||||
* splitIsActuallyVersion(split) // returns true
|
||||
*/
|
||||
const splitIsActuallyVersion = (split: string[]) =>
|
||||
split[1] && split[1][0] && isNumber(split[1][0]);
|
||||
split[1]?.[0] && isNumber(split[1][0]);
|
||||
|
||||
/**
|
||||
* Given a model string, extract the provider and model name. Currently the supported separators are "/" and "."
|
||||
|
||||
@ -1,6 +1,48 @@
|
||||
import { DEFAULT_SETTINGS } from "#/services/settings";
|
||||
import { Settings } from "#/types/settings";
|
||||
|
||||
export const hasAdvancedSettingsSet = (settings: Partial<Settings>): boolean =>
|
||||
Object.keys(settings).length > 0 &&
|
||||
(!!settings.llm_base_url || settings.agent !== DEFAULT_SETTINGS.agent);
|
||||
/**
|
||||
* Determines if any advanced-only settings are configured.
|
||||
* Advanced-only settings are those that appear only in the Advanced Settings view
|
||||
* and not in the Basic Settings view.
|
||||
*
|
||||
* Advanced-only fields:
|
||||
* - llm_base_url: Custom base URL for LLM API
|
||||
* - agent: Custom agent selection (when not using default)
|
||||
* - enable_default_condenser: Memory condenser toggle (when disabled, as default is enabled)
|
||||
* - condenser_max_size: Custom condenser size (when different from default)
|
||||
* - search_api_key: Search API key (when set)
|
||||
*/
|
||||
export const hasAdvancedSettingsSet = (
|
||||
settings: Partial<Settings>,
|
||||
): boolean => {
|
||||
if (Object.keys(settings).length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for advanced-only settings that differ from defaults
|
||||
const hasBaseUrl =
|
||||
!!settings.llm_base_url && settings.llm_base_url.trim() !== "";
|
||||
const hasCustomAgent =
|
||||
settings.agent !== undefined && settings.agent !== DEFAULT_SETTINGS.agent;
|
||||
// Default is true, so only check if explicitly disabled
|
||||
const hasDisabledCondenser = settings.enable_default_condenser === false;
|
||||
// Check if condenser size differs from default (default is 120)
|
||||
const hasCustomCondenserSize =
|
||||
settings.condenser_max_size !== undefined &&
|
||||
settings.condenser_max_size !== null &&
|
||||
settings.condenser_max_size !== DEFAULT_SETTINGS.condenser_max_size;
|
||||
// Check if search API key is set (non-empty string)
|
||||
const hasSearchApiKey =
|
||||
settings.search_api_key !== undefined &&
|
||||
settings.search_api_key !== null &&
|
||||
settings.search_api_key.trim() !== "";
|
||||
|
||||
return (
|
||||
hasBaseUrl ||
|
||||
hasCustomAgent ||
|
||||
hasDisabledCondenser ||
|
||||
hasCustomCondenserSize ||
|
||||
hasSearchApiKey
|
||||
);
|
||||
};
|
||||
|
||||
@ -19,6 +19,7 @@ export const VERIFIED_MODELS = [
|
||||
"claude-haiku-4-5-20251001",
|
||||
"claude-opus-4-20250514",
|
||||
"claude-opus-4-1-20250805",
|
||||
"claude-opus-4-5-20251101",
|
||||
"gemini-2.5-pro",
|
||||
"o4-mini",
|
||||
"deepseek-chat",
|
||||
@ -80,6 +81,7 @@ export const VERIFIED_OPENHANDS_MODELS = [
|
||||
"gpt-5-mini-2025-08-07",
|
||||
"claude-opus-4-20250514",
|
||||
"claude-opus-4-1-20250805",
|
||||
"claude-opus-4-5-20251101",
|
||||
"gemini-2.5-pro",
|
||||
"o3",
|
||||
"o4-mini",
|
||||
@ -91,4 +93,4 @@ export const VERIFIED_OPENHANDS_MODELS = [
|
||||
];
|
||||
|
||||
// Default model for OpenHands provider
|
||||
export const DEFAULT_OPENHANDS_MODEL = "openhands/claude-sonnet-4-20250514";
|
||||
export const DEFAULT_OPENHANDS_MODEL = "openhands/claude-opus-4-5-20251101";
|
||||
|
||||
@ -478,7 +478,11 @@ class LiveStatusAppConversationService(AppConversationServiceBase):
|
||||
if sandbox.status in (None, SandboxStatus.ERROR):
|
||||
raise SandboxError(f'Sandbox status: {sandbox.status}')
|
||||
if sandbox.status == SandboxStatus.RUNNING:
|
||||
return
|
||||
# There are still bugs in the remote runtime - they report running while still just
|
||||
# starting resulting in a race condition. Manually check that it is actually
|
||||
# running.
|
||||
if await self._check_agent_server_alive(sandbox):
|
||||
return
|
||||
if sandbox.status != SandboxStatus.STARTING:
|
||||
raise SandboxError(f'Sandbox not startable: {sandbox.id}')
|
||||
|
||||
@ -491,9 +495,19 @@ class LiveStatusAppConversationService(AppConversationServiceBase):
|
||||
if sandbox.status not in (SandboxStatus.STARTING, SandboxStatus.RUNNING):
|
||||
raise SandboxError(f'Sandbox not startable: {sandbox.id}')
|
||||
if sandbox_info.status == SandboxStatus.RUNNING:
|
||||
return
|
||||
# There are still bugs in the remote runtime - they report running while still just
|
||||
# starting resulting in a race condition. Manually check that it is actually
|
||||
# running.
|
||||
if await self._check_agent_server_alive(sandbox_info):
|
||||
return
|
||||
raise SandboxError(f'Sandbox failed to start: {sandbox.id}')
|
||||
|
||||
async def _check_agent_server_alive(self, sandbox_info: SandboxInfo) -> bool:
|
||||
agent_server_url = self._get_agent_server_url(sandbox_info)
|
||||
url = f'{agent_server_url.rstrip("/")}/alive'
|
||||
response = await self.httpx_client.get(url)
|
||||
return response.is_success
|
||||
|
||||
def _get_agent_server_url(self, sandbox: SandboxInfo) -> str:
|
||||
"""Get agent server url for running sandbox."""
|
||||
exposed_urls = sandbox.exposed_urls
|
||||
|
||||
@ -44,6 +44,7 @@ from openhands.app_server.services.injector import InjectorState
|
||||
from openhands.app_server.user.specifiy_user_context import ADMIN, USER_CONTEXT_ATTR
|
||||
from openhands.app_server.user.user_context import UserContext
|
||||
from openhands.app_server.utils.sql_utils import Base, UtcDateTime
|
||||
from openhands.sdk.utils.paging import page_iterator
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
WEBHOOK_CALLBACK_VARIABLE = 'OH_WEBHOOKS_0_BASE_URL'
|
||||
@ -121,18 +122,9 @@ class RemoteSandboxService(SandboxService):
|
||||
_logger.error(f'HTTP error for URL {url}: {e}')
|
||||
raise
|
||||
|
||||
async def _to_sandbox_info(
|
||||
def _to_sandbox_info(
|
||||
self, stored: StoredRemoteSandbox, runtime: dict[str, Any] | None = None
|
||||
) -> SandboxInfo:
|
||||
# If we did not get passsed runtime data, load some
|
||||
if runtime is None:
|
||||
try:
|
||||
runtime = await self._get_runtime(stored.id)
|
||||
except Exception:
|
||||
_logger.exception(
|
||||
f'Error getting runtime: {stored.id}', stack_info=True
|
||||
)
|
||||
|
||||
):
|
||||
status = self._get_sandbox_status_from_runtime(runtime)
|
||||
|
||||
# Get session_api_key and exposed urls
|
||||
@ -232,6 +224,40 @@ class RemoteSandboxService(SandboxService):
|
||||
runtime_data = response.json()
|
||||
return runtime_data
|
||||
|
||||
async def _get_runtimes_batch(
|
||||
self, sandbox_ids: list[str]
|
||||
) -> dict[str, dict[str, Any]]:
|
||||
"""Get multiple runtimes in a single batch request.
|
||||
|
||||
Args:
|
||||
sandbox_ids: List of sandbox IDs to fetch
|
||||
|
||||
Returns:
|
||||
Dictionary mapping sandbox_id to runtime data
|
||||
"""
|
||||
if not sandbox_ids:
|
||||
return {}
|
||||
|
||||
# Build query parameters for the batch endpoint
|
||||
params = [('ids', sandbox_id) for sandbox_id in sandbox_ids]
|
||||
|
||||
response = await self._send_runtime_api_request(
|
||||
'GET',
|
||||
'/sessions/batch',
|
||||
params=params,
|
||||
)
|
||||
response.raise_for_status()
|
||||
batch_data = response.json()
|
||||
|
||||
# The batch endpoint should return a list of runtimes
|
||||
# Convert to a dictionary keyed by session_id for easy lookup
|
||||
runtimes_by_id = {}
|
||||
for runtime in batch_data:
|
||||
if runtime and 'session_id' in runtime:
|
||||
runtimes_by_id[runtime['session_id']] = runtime
|
||||
|
||||
return runtimes_by_id
|
||||
|
||||
async def _init_environment(
|
||||
self, sandbox_spec: SandboxSpecInfo, sandbox_id: str
|
||||
) -> dict[str, str]:
|
||||
@ -282,13 +308,15 @@ class RemoteSandboxService(SandboxService):
|
||||
if has_more:
|
||||
next_page_id = str(offset + limit)
|
||||
|
||||
# Convert stored callbacks to domain models
|
||||
items = await asyncio.gather(
|
||||
*[
|
||||
self._to_sandbox_info(stored_sandbox)
|
||||
for stored_sandbox in stored_sandboxes
|
||||
]
|
||||
)
|
||||
# Batch fetch runtime data for all sandboxes
|
||||
sandbox_ids = [stored_sandbox.id for stored_sandbox in stored_sandboxes]
|
||||
runtimes_by_id = await self._get_runtimes_batch(sandbox_ids)
|
||||
|
||||
# Convert stored sandboxes to domain models with runtime data
|
||||
items = [
|
||||
self._to_sandbox_info(stored_sandbox, runtimes_by_id.get(stored_sandbox.id))
|
||||
for stored_sandbox in stored_sandboxes
|
||||
]
|
||||
|
||||
return SandboxPage(items=items, next_page_id=next_page_id)
|
||||
|
||||
@ -297,12 +325,46 @@ class RemoteSandboxService(SandboxService):
|
||||
stored_sandbox = await self._get_stored_sandbox(sandbox_id)
|
||||
if stored_sandbox is None:
|
||||
return None
|
||||
return await self._to_sandbox_info(stored_sandbox)
|
||||
|
||||
runtime = None
|
||||
try:
|
||||
runtime = await self._get_runtime(stored_sandbox.id)
|
||||
except Exception:
|
||||
_logger.exception(
|
||||
f'Error getting runtime: {stored_sandbox.id}', stack_info=True
|
||||
)
|
||||
|
||||
return self._to_sandbox_info(stored_sandbox, runtime)
|
||||
|
||||
async def get_sandbox_by_session_api_key(
|
||||
self, session_api_key: str
|
||||
) -> Union[SandboxInfo, None]:
|
||||
"""Get a single sandbox by session API key."""
|
||||
# TODO: We should definitely refactor this and store the session_api_key in
|
||||
# the v1_remote_sandbox table
|
||||
try:
|
||||
response = await self._send_runtime_api_request(
|
||||
'GET',
|
||||
'/list',
|
||||
)
|
||||
response.raise_for_status()
|
||||
content = response.json()
|
||||
for runtime in content['runtimes']:
|
||||
if session_api_key == runtime['session_api_key']:
|
||||
query = await self._secure_select()
|
||||
query = query.filter(
|
||||
StoredRemoteSandbox.id == runtime.get('session_id')
|
||||
)
|
||||
result = await self.db_session.execute(query)
|
||||
sandbox = result.first()
|
||||
if sandbox is None:
|
||||
raise ValueError('sandbox_not_found')
|
||||
return self._to_sandbox_info(sandbox, runtime)
|
||||
except Exception:
|
||||
_logger.exception(
|
||||
'Error getting sandbox from session_api_key', stack_info=True
|
||||
)
|
||||
|
||||
# Get all stored sandboxes for the current user
|
||||
stmt = await self._secure_select()
|
||||
result = await self.db_session.execute(stmt)
|
||||
@ -313,7 +375,7 @@ class RemoteSandboxService(SandboxService):
|
||||
try:
|
||||
runtime = await self._get_runtime(stored_sandbox.id)
|
||||
if runtime and runtime.get('session_api_key') == session_api_key:
|
||||
return await self._to_sandbox_info(stored_sandbox, runtime)
|
||||
return self._to_sandbox_info(stored_sandbox, runtime)
|
||||
except Exception:
|
||||
# Continue checking other sandboxes if one fails
|
||||
continue
|
||||
@ -386,7 +448,7 @@ class RemoteSandboxService(SandboxService):
|
||||
# Hack - result doesn't contain this
|
||||
runtime_data['pod_status'] = 'pending'
|
||||
|
||||
return await self._to_sandbox_info(stored_sandbox, runtime_data)
|
||||
return self._to_sandbox_info(stored_sandbox, runtime_data)
|
||||
|
||||
except httpx.HTTPError as e:
|
||||
_logger.error(f'Failed to start sandbox: {e}')
|
||||
@ -454,6 +516,81 @@ class RemoteSandboxService(SandboxService):
|
||||
_logger.error(f'Error deleting sandbox {sandbox_id}: {e}')
|
||||
return False
|
||||
|
||||
async def pause_old_sandboxes(self, max_num_sandboxes: int) -> list[str]:
|
||||
"""Pause the oldest sandboxes if there are more than max_num_sandboxes running.
|
||||
In a multi user environment, this will pause sandboxes only for the current user.
|
||||
|
||||
Args:
|
||||
max_num_sandboxes: Maximum number of sandboxes to keep running
|
||||
|
||||
Returns:
|
||||
List of sandbox IDs that were paused
|
||||
"""
|
||||
if max_num_sandboxes <= 0:
|
||||
raise ValueError('max_num_sandboxes must be greater than 0')
|
||||
|
||||
response = await self._send_runtime_api_request(
|
||||
'GET',
|
||||
'/list',
|
||||
)
|
||||
content = response.json()
|
||||
running_session_ids = [
|
||||
runtime.get('session_id') for runtime in content['runtimes']
|
||||
]
|
||||
|
||||
query = await self._secure_select()
|
||||
query = query.filter(StoredRemoteSandbox.id.in_(running_session_ids)).order_by(
|
||||
StoredRemoteSandbox.created_at.desc()
|
||||
)
|
||||
running_sandboxes = list(await self.db_session.execute(query))
|
||||
|
||||
# If we're within the limit, no cleanup needed
|
||||
if len(running_sandboxes) <= max_num_sandboxes:
|
||||
return []
|
||||
|
||||
# Determine how many to pause
|
||||
num_to_pause = len(running_sandboxes) - max_num_sandboxes
|
||||
sandboxes_to_pause = running_sandboxes[:num_to_pause]
|
||||
|
||||
# Stop the oldest sandboxes
|
||||
paused_sandbox_ids = []
|
||||
for sandbox in sandboxes_to_pause:
|
||||
try:
|
||||
success = await self.pause_sandbox(sandbox.id)
|
||||
if success:
|
||||
paused_sandbox_ids.append(sandbox.id)
|
||||
except Exception:
|
||||
# Continue trying to pause other sandboxes even if one fails
|
||||
pass
|
||||
|
||||
return paused_sandbox_ids
|
||||
|
||||
async def batch_get_sandboxes(
|
||||
self, sandbox_ids: list[str]
|
||||
) -> list[SandboxInfo | None]:
|
||||
"""Get a batch of sandboxes, returning None for any which were not found."""
|
||||
if not sandbox_ids:
|
||||
return []
|
||||
query = await self._secure_select()
|
||||
query = query.filter(StoredRemoteSandbox.id.in_(sandbox_ids))
|
||||
stored_remote_sandboxes = await self.db_session.execute(query)
|
||||
stored_remote_sandboxes_by_id = {
|
||||
stored_remote_sandbox[0].id: stored_remote_sandbox[0]
|
||||
for stored_remote_sandbox in stored_remote_sandboxes
|
||||
}
|
||||
runtimes_by_id = await self._get_runtimes_batch(
|
||||
list(stored_remote_sandboxes_by_id)
|
||||
)
|
||||
results = []
|
||||
for sandbox_id in sandbox_ids:
|
||||
stored_remote_sandbox = stored_remote_sandboxes_by_id.get(sandbox_id)
|
||||
result = None
|
||||
if stored_remote_sandbox:
|
||||
runtime = runtimes_by_id.get(sandbox_id)
|
||||
result = self._to_sandbox_info(stored_remote_sandbox, runtime)
|
||||
results.append(result)
|
||||
return results
|
||||
|
||||
|
||||
def _build_service_url(url: str, service_name: str):
|
||||
scheme, host_and_path = url.split('://')
|
||||
@ -504,32 +641,26 @@ async def poll_agent_servers(api_url: str, api_key: str, sleep_interval: int):
|
||||
get_event_callback_service(state) as event_callback_service,
|
||||
get_httpx_client(state) as httpx_client,
|
||||
):
|
||||
page_id = None
|
||||
matches = 0
|
||||
while True:
|
||||
page = await app_conversation_info_service.search_app_conversation_info(
|
||||
page_id=page_id
|
||||
async for app_conversation_info in page_iterator(
|
||||
app_conversation_info_service.search_app_conversation_info
|
||||
):
|
||||
runtime = runtimes_by_sandbox_id.get(
|
||||
app_conversation_info.sandbox_id
|
||||
)
|
||||
for app_conversation_info in page.items:
|
||||
runtime = runtimes_by_sandbox_id.get(
|
||||
app_conversation_info.sandbox_id
|
||||
if runtime:
|
||||
matches += 1
|
||||
await refresh_conversation(
|
||||
app_conversation_info_service=app_conversation_info_service,
|
||||
event_service=event_service,
|
||||
event_callback_service=event_callback_service,
|
||||
app_conversation_info=app_conversation_info,
|
||||
runtime=runtime,
|
||||
httpx_client=httpx_client,
|
||||
)
|
||||
if runtime:
|
||||
matches += 1
|
||||
await refresh_conversation(
|
||||
app_conversation_info_service=app_conversation_info_service,
|
||||
event_service=event_service,
|
||||
event_callback_service=event_callback_service,
|
||||
app_conversation_info=app_conversation_info,
|
||||
runtime=runtime,
|
||||
httpx_client=httpx_client,
|
||||
)
|
||||
page_id = page.next_page_id
|
||||
if page_id is None:
|
||||
_logger.debug(
|
||||
f'Matched {len(runtimes_by_sandbox_id)} Runtimes with {matches} Conversations.'
|
||||
)
|
||||
break
|
||||
_logger.debug(
|
||||
f'Matched {len(runtimes_by_sandbox_id)} Runtimes with {matches} Conversations.'
|
||||
)
|
||||
|
||||
except Exception as exc:
|
||||
_logger.exception(
|
||||
@ -583,37 +714,29 @@ async def refresh_conversation(
|
||||
event_url = (
|
||||
f'{url}/api/conversations/{app_conversation_info.id.hex}/events/search'
|
||||
)
|
||||
page_id = None
|
||||
while True:
|
||||
|
||||
async def fetch_events_page(page_id: str | None = None) -> EventPage:
|
||||
"""Helper function to fetch a page of events from the agent server."""
|
||||
params: dict[str, str] = {}
|
||||
if page_id:
|
||||
params['page_id'] = page_id # type: ignore[unreachable]
|
||||
params['page_id'] = page_id
|
||||
response = await httpx_client.get(
|
||||
event_url,
|
||||
params=params,
|
||||
headers={'X-Session-API-Key': runtime['session_api_key']},
|
||||
)
|
||||
response.raise_for_status()
|
||||
page = EventPage.model_validate(response.json())
|
||||
return EventPage.model_validate(response.json())
|
||||
|
||||
to_process = []
|
||||
for event in page.items:
|
||||
existing = await event_service.get_event(event.id)
|
||||
if existing is None:
|
||||
await event_service.save_event(app_conversation_info.id, event)
|
||||
to_process.append(event)
|
||||
|
||||
for event in to_process:
|
||||
async for event in page_iterator(fetch_events_page):
|
||||
existing = await event_service.get_event(event.id)
|
||||
if existing is None:
|
||||
await event_service.save_event(app_conversation_info.id, event)
|
||||
await event_callback_service.execute_callbacks(
|
||||
app_conversation_info.id, event
|
||||
)
|
||||
|
||||
page_id = page.next_page_id
|
||||
if page_id is None:
|
||||
_logger.debug(
|
||||
f'Finished Refreshing Conversation {app_conversation_info.id}'
|
||||
)
|
||||
break
|
||||
_logger.debug(f'Finished Refreshing Conversation {app_conversation_info.id}')
|
||||
|
||||
except Exception as exc:
|
||||
_logger.exception(f'Error Refreshing Conversation: {exc}', stack_info=True)
|
||||
|
||||
@ -8,6 +8,7 @@ from openhands.app_server.sandbox.sandbox_models import (
|
||||
)
|
||||
from openhands.app_server.services.injector import Injector
|
||||
from openhands.sdk.utils.models import DiscriminatedUnionMixin
|
||||
from openhands.sdk.utils.paging import page_iterator
|
||||
|
||||
|
||||
class SandboxService(ABC):
|
||||
@ -71,7 +72,7 @@ class SandboxService(ABC):
|
||||
"""
|
||||
|
||||
async def pause_old_sandboxes(self, max_num_sandboxes: int) -> list[str]:
|
||||
"""Stop the oldest sandboxes if there are more than max_num_sandboxes running.
|
||||
"""Pause the oldest sandboxes if there are more than max_num_sandboxes running.
|
||||
In a multi user environment, this will pause sandboxes only for the current user.
|
||||
|
||||
Args:
|
||||
@ -83,24 +84,11 @@ class SandboxService(ABC):
|
||||
if max_num_sandboxes <= 0:
|
||||
raise ValueError('max_num_sandboxes must be greater than 0')
|
||||
|
||||
# Get all sandboxes (we'll search through all pages)
|
||||
all_sandboxes = []
|
||||
page_id = None
|
||||
|
||||
while True:
|
||||
page = await self.search_sandboxes(page_id=page_id, limit=100)
|
||||
all_sandboxes.extend(page.items)
|
||||
|
||||
if page.next_page_id is None:
|
||||
break
|
||||
page_id = page.next_page_id
|
||||
|
||||
# Filter to only running sandboxes
|
||||
running_sandboxes = [
|
||||
sandbox
|
||||
for sandbox in all_sandboxes
|
||||
if sandbox.status == SandboxStatus.RUNNING
|
||||
]
|
||||
# Get all running sandboxes (iterate through all pages)
|
||||
running_sandboxes = []
|
||||
async for sandbox in page_iterator(self.search_sandboxes, limit=100):
|
||||
if sandbox.status == SandboxStatus.RUNNING:
|
||||
running_sandboxes.append(sandbox)
|
||||
|
||||
# If we're within the limit, no cleanup needed
|
||||
if len(running_sandboxes) <= max_num_sandboxes:
|
||||
|
||||
@ -944,6 +944,23 @@ class AgentController:
|
||||
return
|
||||
else:
|
||||
raise LLMContextWindowExceedError()
|
||||
# Check if this is a tool call validation error that should be recoverable
|
||||
elif (
|
||||
isinstance(e, BadRequestError)
|
||||
and 'tool call validation failed' in error_str
|
||||
and (
|
||||
'missing properties' in error_str
|
||||
or 'missing required' in error_str
|
||||
)
|
||||
):
|
||||
# Handle tool call validation errors from Groq as recoverable errors
|
||||
self.event_stream.add_event(
|
||||
ErrorObservation(
|
||||
content=f'Tool call validation failed: {str(e)}. Please check the tool parameters and try again.',
|
||||
),
|
||||
EventSource.AGENT,
|
||||
)
|
||||
return
|
||||
else:
|
||||
raise e
|
||||
|
||||
|
||||
@ -50,7 +50,7 @@ class LLMConfig(BaseModel):
|
||||
completion_kwargs: Custom kwargs to pass to litellm.completion.
|
||||
"""
|
||||
|
||||
model: str = Field(default='claude-sonnet-4-20250514')
|
||||
model: str = Field(default='claude-opus-4-5-20251101')
|
||||
api_key: SecretStr | None = Field(default=None)
|
||||
base_url: str | None = Field(default=None)
|
||||
api_version: str | None = Field(default=None)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user