diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 21782ad1d8..3da4c3ee88 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -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
diff --git a/enterprise/Dockerfile b/enterprise/Dockerfile
index b0ca56a7f6..7b50748ffb 100644
--- a/enterprise/Dockerfile
+++ b/enterprise/Dockerfile
@@ -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
diff --git a/enterprise/enterprise_local/convert_to_env.py b/enterprise/enterprise_local/convert_to_env.py
index 54e2c5d71b..cbd04b6449 100644
--- a/enterprise/enterprise_local/convert_to_env.py
+++ b/enterprise/enterprise_local/convert_to_env.py
@@ -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')
diff --git a/enterprise/server/auth/constants.py b/enterprise/server/auth/constants.py
index 15d3b0f704..242237e93d 100644
--- a/enterprise/server/auth/constants.py
+++ b/enterprise/server/auth/constants.py
@@ -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()
+]
diff --git a/enterprise/server/auth/domain_blocker.py b/enterprise/server/auth/domain_blocker.py
new file mode 100644
index 0000000000..169545ae2d
--- /dev/null
+++ b/enterprise/server/auth/domain_blocker.py
@@ -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()
diff --git a/enterprise/server/auth/saas_user_auth.py b/enterprise/server/auth/saas_user_auth.py
index 2f399a74cf..b51d336997 100644
--- a/enterprise/server/auth/saas_user_auth.py
+++ b/enterprise/server/auth/saas_user_auth.py
@@ -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(
diff --git a/enterprise/server/auth/token_manager.py b/enterprise/server/auth/token_manager.py
index 0b873bc7fc..04bfae0767 100644
--- a/enterprise/server/auth/token_manager.py
+++ b/enterprise/server/auth/token_manager.py
@@ -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.
diff --git a/enterprise/server/constants.py b/enterprise/server/constants.py
index 74176d19a1..e2020ba2b9 100644
--- a/enterprise/server/constants.py
+++ b/enterprise/server/constants.py
@@ -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')
diff --git a/enterprise/server/routes/auth.py b/enterprise/server/routes/auth.py
index ba7aadb883..2ee50bbd2d 100644
--- a/enterprise/server/routes/auth.py
+++ b/enterprise/server/routes/auth.py
@@ -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)
diff --git a/enterprise/server/saas_nested_conversation_manager.py b/enterprise/server/saas_nested_conversation_manager.py
index d92e67b9ff..59c6d4e981 100644
--- a/enterprise/server/saas_nested_conversation_manager.py
+++ b/enterprise/server/saas_nested_conversation_manager.py
@@ -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
diff --git a/enterprise/storage/api_key_store.py b/enterprise/storage/api_key_store.py
index 693bfdb321..9714d7476a 100644
--- a/enterprise/storage/api_key_store.py
+++ b/enterprise/storage/api_key_store.py
@@ -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
diff --git a/enterprise/storage/saas_settings_store.py b/enterprise/storage/saas_settings_store.py
index cfcbec7583..fd64924263 100644
--- a/enterprise/storage/saas_settings_store.py
+++ b/enterprise/storage/saas_settings_store.py
@@ -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):
diff --git a/enterprise/tests/unit/test_api_key_store.py b/enterprise/tests/unit/test_api_key_store.py
index c1c6a98f3d..df0481937d 100644
--- a/enterprise/tests/unit/test_api_key_store.py
+++ b/enterprise/tests/unit/test_api_key_store.py
@@ -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):
diff --git a/enterprise/tests/unit/test_auth_routes.py b/enterprise/tests/unit/test_auth_routes.py
index bf74f0055c..d3e8f47fbe 100644
--- a/enterprise/tests/unit/test_auth_routes.py
+++ b/enterprise/tests/unit/test_auth_routes.py
@@ -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()
diff --git a/enterprise/tests/unit/test_domain_blocker.py b/enterprise/tests/unit/test_domain_blocker.py
new file mode 100644
index 0000000000..e199a80b9b
--- /dev/null
+++ b/enterprise/tests/unit/test_domain_blocker.py
@@ -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
diff --git a/enterprise/tests/unit/test_saas_user_auth.py b/enterprise/tests/unit/test_saas_user_auth.py
index d4ba902677..a518beb28e 100644
--- a/enterprise/tests/unit/test_saas_user_auth.py
+++ b/enterprise/tests/unit/test_saas_user_auth.py
@@ -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()
diff --git a/enterprise/tests/unit/test_token_manager_extended.py b/enterprise/tests/unit/test_token_manager_extended.py
index 744f208b02..c3b09434a3 100644
--- a/enterprise/tests/unit/test_token_manager_extended.py
+++ b/enterprise/tests/unit/test_token_manager_extended.py
@@ -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)
diff --git a/frontend/.eslintrc b/frontend/.eslintrc
index c89d89c857..3efd6aea69 100644
--- a/frontend/.eslintrc
+++ b/frontend/.eslintrc
@@ -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",
diff --git a/frontend/__tests__/components/features/conversation/agent-status.test.tsx b/frontend/__tests__/components/features/conversation/agent-status.test.tsx
index a121ed37a8..3bfb70be24 100644
--- a/frontend/__tests__/components/features/conversation/agent-status.test.tsx
+++ b/frontend/__tests__/components/features/conversation/agent-status.test.tsx
@@ -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");
diff --git a/frontend/__tests__/components/features/microagent-management/microagent-management.test.tsx b/frontend/__tests__/components/features/microagent-management/microagent-management.test.tsx
index afdb8e84ba..b256fa14d9 100644
--- a/frontend/__tests__/components/features/microagent-management/microagent-management.test.tsx
+++ b/frontend/__tests__/components/features/microagent-management/microagent-management.test.tsx
@@ -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();
diff --git a/frontend/__tests__/components/interactive-chat-box.test.tsx b/frontend/__tests__/components/interactive-chat-box.test.tsx
index 34ba1eaafd..cb164123c1 100644
--- a/frontend/__tests__/components/interactive-chat-box.test.tsx
+++ b/frontend/__tests__/components/interactive-chat-box.test.tsx
@@ -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(),
diff --git a/frontend/__tests__/components/terminal/terminal.test.tsx b/frontend/__tests__/components/terminal/terminal.test.tsx
index 15fb6357b2..ae25748a8b 100644
--- a/frontend/__tests__/components/terminal/terminal.test.tsx
+++ b/frontend/__tests__/components/terminal/terminal.test.tsx
@@ -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[] = []) => {
diff --git a/frontend/__tests__/conversation-websocket-handler.test.tsx b/frontend/__tests__/conversation-websocket-handler.test.tsx
index f922a8876c..d3df1676fa 100644
--- a/frontend/__tests__/conversation-websocket-handler.test.tsx
+++ b/frontend/__tests__/conversation-websocket-handler.test.tsx
@@ -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
diff --git a/frontend/__tests__/hooks/use-terminal.test.tsx b/frontend/__tests__/hooks/use-terminal.test.tsx
index 08144503b5..0e4761b21b 100644
--- a/frontend/__tests__/hooks/use-terminal.test.tsx
+++ b/frontend/__tests__/hooks/use-terminal.test.tsx
@@ -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();
+
+ // fit() should not be called because terminal.element is null
+ expect(mockFitAddon.fit).not.toHaveBeenCalled();
+
+ // Restore original element
+ mockTerminal.element = originalElement;
+ });
});
diff --git a/frontend/__tests__/routes/llm-settings.test.tsx b/frontend/__tests__/routes/llm-settings.test.tsx
index 8cb1239c0a..68e44d73e9 100644
--- a/frontend/__tests__/routes/llm-settings.test.tsx
+++ b/frontend/__tests__/routes/llm-settings.test.tsx
@@ -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 () => {
diff --git a/frontend/__tests__/services/actions.test.ts b/frontend/__tests__/services/actions.test.ts
index 44700aef2c..8054b999d1 100644
--- a/frontend/__tests__/services/actions.test.ts
+++ b/frontend/__tests__/services/actions.test.ts
@@ -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(),
diff --git a/frontend/__tests__/services/actions.test.tsx b/frontend/__tests__/services/actions.test.tsx
index c6e2ac76e2..555fd18caa 100644
--- a/frontend/__tests__/services/actions.test.tsx
+++ b/frontend/__tests__/services/actions.test.tsx
@@ -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();
diff --git a/frontend/__tests__/utils/has-advanced-settings-set.test.ts b/frontend/__tests__/utils/has-advanced-settings-set.test.ts
index 36c7a7b609..be928262d1 100644
--- a/frontend/__tests__/utils/has-advanced-settings-set.test.ts
+++ b/frontend/__tests__/utils/has-advanced-settings-set.test.ts
@@ -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);
+ });
});
});
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index e130cad40f..33717ced21 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -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",
diff --git a/frontend/package.json b/frontend/package.json
index 90636fed77..f08f6ea3b6 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -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",
diff --git a/frontend/src/ui/microagent-management-service/microagent-management-service.api.ts b/frontend/src/api/microagent-management-service/microagent-management-service.api.ts
similarity index 100%
rename from frontend/src/ui/microagent-management-service/microagent-management-service.api.ts
rename to frontend/src/api/microagent-management-service/microagent-management-service.api.ts
diff --git a/frontend/src/components/features/browser/browser.tsx b/frontend/src/components/features/browser/browser.tsx
index 8c3842edd4..95c8f1fa1a 100644
--- a/frontend/src/components/features/browser/browser.tsx
+++ b/frontend/src/components/features/browser/browser.tsx
@@ -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 (
diff --git a/frontend/src/components/features/chat/change-agent-button.tsx b/frontend/src/components/features/chat/change-agent-button.tsx
index 68a0bd2699..28de891db9 100644
--- a/frontend/src/components/features/chat/change-agent-button.tsx
+++ b/frontend/src/components/features/chat/change-agent-button.tsx
@@ -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";
diff --git a/frontend/src/components/features/chat/chat-interface.tsx b/frontend/src/components/features/chat/chat-interface.tsx
index f37bd59c26..84c269dac3 100644
--- a/frontend/src/components/features/chat/chat-interface.tsx
+++ b/frontend/src/components/features/chat/chat-interface.tsx
@@ -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,
diff --git a/frontend/src/components/features/chat/chat-suggestions.tsx b/frontend/src/components/features/chat/chat-suggestions.tsx
index 8abc5aa41c..1f9a9345ad 100644
--- a/frontend/src/components/features/chat/chat-suggestions.tsx
+++ b/frontend/src/components/features/chat/chat-suggestions.tsx
@@ -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;
diff --git a/frontend/src/components/features/chat/components/chat-input-container.tsx b/frontend/src/components/features/chat/components/chat-input-container.tsx
index acba3074f5..e950ce785b 100644
--- a/frontend/src/components/features/chat/components/chat-input-container.tsx
+++ b/frontend/src/components/features/chat/components/chat-input-container.tsx
@@ -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 {
diff --git a/frontend/src/components/features/chat/components/chat-input-field.tsx b/frontend/src/components/features/chat/components/chat-input-field.tsx
index 4c52b7980b..7a6fe4f52b 100644
--- a/frontend/src/components/features/chat/components/chat-input-field.tsx
+++ b/frontend/src/components/features/chat/components/chat-input-field.tsx
@@ -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
;
diff --git a/frontend/src/components/features/chat/custom-chat-input.tsx b/frontend/src/components/features/chat/custom-chat-input.tsx
index 92ec264a34..624457b35b 100644
--- a/frontend/src/components/features/chat/custom-chat-input.tsx
+++ b/frontend/src/components/features/chat/custom-chat-input.tsx
@@ -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;
diff --git a/frontend/src/components/features/chat/event-content-helpers/get-observation-content.ts b/frontend/src/components/features/chat/event-content-helpers/get-observation-content.ts
index 435a686918..11276a4e39 100644
--- a/frontend/src/components/features/chat/event-content-helpers/get-observation-content.ts
+++ b/frontend/src/components/features/chat/event-content-helpers/get-observation-content.ts
@@ -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()}`;
}
diff --git a/frontend/src/components/features/chat/interactive-chat-box.tsx b/frontend/src/components/features/chat/interactive-chat-box.tsx
index 56a4def14d..a2f1df8348 100644
--- a/frontend/src/components/features/chat/interactive-chat-box.tsx
+++ b/frontend/src/components/features/chat/interactive-chat-box.tsx
@@ -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";
diff --git a/frontend/src/components/features/chat/messages.tsx b/frontend/src/components/features/chat/messages.tsx
index 0d9032164d..6e68089b13 100644
--- a/frontend/src/components/features/chat/messages.tsx
+++ b/frontend/src/components/features/chat/messages.tsx
@@ -192,8 +192,7 @@ export const Messages: React.FC = 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
diff --git a/frontend/src/components/features/chat/uploaded-files.tsx b/frontend/src/components/features/chat/uploaded-files.tsx
index 78b0c3dc49..ced7849ef0 100644
--- a/frontend/src/components/features/chat/uploaded-files.tsx
+++ b/frontend/src/components/features/chat/uploaded-files.tsx
@@ -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 {
diff --git a/frontend/src/components/features/controls/agent-status.tsx b/frontend/src/components/features/controls/agent-status.tsx
index 675b5881a3..f62472bf95 100644
--- a/frontend/src/components/features/controls/agent-status.tsx
+++ b/frontend/src/components/features/controls/agent-status.tsx
@@ -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";
diff --git a/frontend/src/components/features/controls/git-tools-submenu.tsx b/frontend/src/components/features/controls/git-tools-submenu.tsx
index e55dd93952..97db554aba 100644
--- a/frontend/src/components/features/controls/git-tools-submenu.tsx
+++ b/frontend/src/components/features/controls/git-tools-submenu.tsx
@@ -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";
diff --git a/frontend/src/components/features/controls/macros-submenu.tsx b/frontend/src/components/features/controls/macros-submenu.tsx
index 8705e11959..b167501d0b 100644
--- a/frontend/src/components/features/controls/macros-submenu.tsx
+++ b/frontend/src/components/features/controls/macros-submenu.tsx
@@ -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";
diff --git a/frontend/src/components/features/conversation-panel/conversation-panel-wrapper.tsx b/frontend/src/components/features/conversation-panel/conversation-panel-wrapper.tsx
index 3ecb6d01a6..57bde06727 100644
--- a/frontend/src/components/features/conversation-panel/conversation-panel-wrapper.tsx
+++ b/frontend/src/components/features/conversation-panel/conversation-panel-wrapper.tsx
@@ -20,7 +20,7 @@ export function ConversationPanelWrapper({
return ReactDOM.createPortal(
diff --git a/frontend/src/components/features/conversation/conversation-main/conversation-main.tsx b/frontend/src/components/features/conversation/conversation-main/conversation-main.tsx
index d5c7ad3a0e..0c6d639b3b 100644
--- a/frontend/src/components/features/conversation/conversation-main/conversation-main.tsx
+++ b/frontend/src/components/features/conversation/conversation-main/conversation-main.tsx
@@ -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();
diff --git a/frontend/src/components/features/conversation/conversation-tabs/conversation-tab-content/conversation-tab-content.tsx b/frontend/src/components/features/conversation/conversation-tabs/conversation-tab-content/conversation-tab-content.tsx
index 39b68c9033..f9f2bef251 100644
--- a/frontend/src/components/features/conversation/conversation-tabs/conversation-tab-content/conversation-tab-content.tsx
+++ b/frontend/src/components/features/conversation/conversation-tabs/conversation-tab-content/conversation-tab-content.tsx
@@ -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
diff --git a/frontend/src/components/features/conversation/conversation-tabs/conversation-tabs.tsx b/frontend/src/components/features/conversation/conversation-tabs/conversation-tabs.tsx
index eedb9010e8..ed649f373c 100644
--- a/frontend/src/components/features/conversation/conversation-tabs/conversation-tabs.tsx
+++ b/frontend/src/components/features/conversation/conversation-tabs/conversation-tabs.tsx
@@ -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";
diff --git a/frontend/src/components/features/home/git-provider-dropdown/git-provider-dropdown.tsx b/frontend/src/components/features/home/git-provider-dropdown/git-provider-dropdown.tsx
index fdc9b21b00..c5ab171ca8 100644
--- a/frontend/src/components/features/home/git-provider-dropdown/git-provider-dropdown.tsx
+++ b/frontend/src/components/features/home/git-provider-dropdown/git-provider-dropdown.tsx
@@ -75,7 +75,7 @@ export function GitProviderDropdown({
}
// If no input value, show all providers
- if (!inputValue || !inputValue.trim()) {
+ if (!inputValue?.trim()) {
return providers;
}
diff --git a/frontend/src/components/features/home/git-repo-dropdown/git-repo-dropdown.tsx b/frontend/src/components/features/home/git-repo-dropdown/git-repo-dropdown.tsx
index 485f574f79..45b75bbd9f 100644
--- a/frontend/src/components/features/home/git-repo-dropdown/git-repo-dropdown.tsx
+++ b/frontend/src/components/features/home/git-repo-dropdown/git-repo-dropdown.tsx
@@ -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"
)
diff --git a/frontend/src/components/features/home/shared/dropdown-item.tsx b/frontend/src/components/features/home/shared/dropdown-item.tsx
index 08e22dc12b..36a0e25967 100644
--- a/frontend/src/components/features/home/shared/dropdown-item.tsx
+++ b/frontend/src/components/features/home/shared/dropdown-item.tsx
@@ -45,7 +45,7 @@ export function DropdownItem
({
// eslint-disable-next-line react/jsx-props-no-spreading
- {renderIcon && renderIcon(item)}
+ {renderIcon?.(item)}
{getDisplayText(item)}
diff --git a/frontend/src/components/features/microagent-management/microagent-management-add-microagent-button.tsx b/frontend/src/components/features/microagent-management/microagent-management-add-microagent-button.tsx
index 0fca5981c1..9bcb282ce8 100644
--- a/frontend/src/components/features/microagent-management/microagent-management-add-microagent-button.tsx
+++ b/frontend/src/components/features/microagent-management/microagent-management-add-microagent-button.tsx
@@ -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 {
diff --git a/frontend/src/components/features/microagent-management/microagent-management-content.tsx b/frontend/src/components/features/microagent-management/microagent-management-content.tsx
index dc16e1da98..fe50ea7849 100644
--- a/frontend/src/components/features/microagent-management/microagent-management-content.tsx
+++ b/frontend/src/components/features/microagent-management/microagent-management-content.tsx
@@ -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,
diff --git a/frontend/src/components/features/microagent-management/microagent-management-conversation-stopped.tsx b/frontend/src/components/features/microagent-management/microagent-management-conversation-stopped.tsx
index 817c31f15f..6c1eb627fd 100644
--- a/frontend/src/components/features/microagent-management/microagent-management-conversation-stopped.tsx
+++ b/frontend/src/components/features/microagent-management/microagent-management-conversation-stopped.tsx
@@ -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();
diff --git a/frontend/src/components/features/microagent-management/microagent-management-error.tsx b/frontend/src/components/features/microagent-management/microagent-management-error.tsx
index 5918d6f081..022a718736 100644
--- a/frontend/src/components/features/microagent-management/microagent-management-error.tsx
+++ b/frontend/src/components/features/microagent-management/microagent-management-error.tsx
@@ -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();
diff --git a/frontend/src/components/features/microagent-management/microagent-management-learn-this-repo-modal.tsx b/frontend/src/components/features/microagent-management/microagent-management-learn-this-repo-modal.tsx
index bbc46c8425..9bb1b40c6d 100644
--- a/frontend/src/components/features/microagent-management/microagent-management-learn-this-repo-modal.tsx
+++ b/frontend/src/components/features/microagent-management/microagent-management-learn-this-repo-modal.tsx
@@ -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";
diff --git a/frontend/src/components/features/microagent-management/microagent-management-learn-this-repo.tsx b/frontend/src/components/features/microagent-management/microagent-management-learn-this-repo.tsx
index dbb76c162a..82d09f2936 100644
--- a/frontend/src/components/features/microagent-management/microagent-management-learn-this-repo.tsx
+++ b/frontend/src/components/features/microagent-management/microagent-management-learn-this-repo.tsx
@@ -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 {
diff --git a/frontend/src/components/features/microagent-management/microagent-management-main.tsx b/frontend/src/components/features/microagent-management/microagent-management-main.tsx
index 6647d23121..c8405044f9 100644
--- a/frontend/src/components/features/microagent-management/microagent-management-main.tsx
+++ b/frontend/src/components/features/microagent-management/microagent-management-main.tsx
@@ -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";
diff --git a/frontend/src/components/features/microagent-management/microagent-management-microagent-card.tsx b/frontend/src/components/features/microagent-management/microagent-management-microagent-card.tsx
index 9da7f8383d..f8ac499f55 100644
--- a/frontend/src/components/features/microagent-management/microagent-management-microagent-card.tsx
+++ b/frontend/src/components/features/microagent-management/microagent-management-microagent-card.tsx
@@ -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";
diff --git a/frontend/src/components/features/microagent-management/microagent-management-opening-pr.tsx b/frontend/src/components/features/microagent-management/microagent-management-opening-pr.tsx
index f4a3beedc0..c9f3ad925d 100644
--- a/frontend/src/components/features/microagent-management/microagent-management-opening-pr.tsx
+++ b/frontend/src/components/features/microagent-management/microagent-management-opening-pr.tsx
@@ -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();
diff --git a/frontend/src/components/features/microagent-management/microagent-management-repo-microagents.tsx b/frontend/src/components/features/microagent-management/microagent-management-repo-microagents.tsx
index 868580550a..b2fc4464c0 100644
--- a/frontend/src/components/features/microagent-management/microagent-management-repo-microagents.tsx
+++ b/frontend/src/components/features/microagent-management/microagent-management-repo-microagents.tsx
@@ -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";
diff --git a/frontend/src/components/features/microagent-management/microagent-management-review-pr.tsx b/frontend/src/components/features/microagent-management/microagent-management-review-pr.tsx
index 1031b7202e..57f4ff129d 100644
--- a/frontend/src/components/features/microagent-management/microagent-management-review-pr.tsx
+++ b/frontend/src/components/features/microagent-management/microagent-management-review-pr.tsx
@@ -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();
diff --git a/frontend/src/components/features/microagent-management/microagent-management-sidebar-tabs.tsx b/frontend/src/components/features/microagent-management/microagent-management-sidebar-tabs.tsx
index 378f0ad7a1..d7a088c608 100644
--- a/frontend/src/components/features/microagent-management/microagent-management-sidebar-tabs.tsx
+++ b/frontend/src/components/features/microagent-management/microagent-management-sidebar-tabs.tsx
@@ -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;
diff --git a/frontend/src/components/features/microagent-management/microagent-management-sidebar.tsx b/frontend/src/components/features/microagent-management/microagent-management-sidebar.tsx
index 3ef952bb79..99719a3f86 100644
--- a/frontend/src/components/features/microagent-management/microagent-management-sidebar.tsx
+++ b/frontend/src/components/features/microagent-management/microagent-management-sidebar.tsx
@@ -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 {
diff --git a/frontend/src/components/features/microagent-management/microagent-management-upsert-microagent-modal.tsx b/frontend/src/components/features/microagent-management/microagent-management-upsert-microagent-modal.tsx
index 6e41583f02..1c5bf37c80 100644
--- a/frontend/src/components/features/microagent-management/microagent-management-upsert-microagent-modal.tsx
+++ b/frontend/src/components/features/microagent-management/microagent-management-upsert-microagent-modal.tsx
@@ -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";
diff --git a/frontend/src/components/features/microagent-management/microagent-management-view-microagent-content.tsx b/frontend/src/components/features/microagent-management/microagent-management-view-microagent-content.tsx
index 2994946731..d89ce2d199 100644
--- a/frontend/src/components/features/microagent-management/microagent-management-view-microagent-content.tsx
+++ b/frontend/src/components/features/microagent-management/microagent-management-view-microagent-content.tsx
@@ -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";
diff --git a/frontend/src/components/features/microagent-management/microagent-management-view-microagent-header.tsx b/frontend/src/components/features/microagent-management/microagent-management-view-microagent-header.tsx
index bf28adbabe..a60f055adb 100644
--- a/frontend/src/components/features/microagent-management/microagent-management-view-microagent-header.tsx
+++ b/frontend/src/components/features/microagent-management/microagent-management-view-microagent-header.tsx
@@ -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();
diff --git a/frontend/src/components/features/microagent-management/microagent-management-view-microagent.tsx b/frontend/src/components/features/microagent-management/microagent-management-view-microagent.tsx
index 2deaf1ef22..a7c6b6bdb8 100644
--- a/frontend/src/components/features/microagent-management/microagent-management-view-microagent.tsx
+++ b/frontend/src/components/features/microagent-management/microagent-management-view-microagent.tsx
@@ -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";
diff --git a/frontend/src/components/v1/chat/event-content-helpers/parse-message-from-event.ts b/frontend/src/components/v1/chat/event-content-helpers/parse-message-from-event.ts
index 17824a51c8..8e2a0cb253 100644
--- a/frontend/src/components/v1/chat/event-content-helpers/parse-message-from-event.ts
+++ b/frontend/src/components/v1/chat/event-content-helpers/parse-message-from-event.ts
@@ -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 "";
}
diff --git a/frontend/src/contexts/conversation-websocket-context.tsx b/frontend/src/contexts/conversation-websocket-context.tsx
index 0cf43b49ce..73bd2b365a 100644
--- a/frontend/src/contexts/conversation-websocket-context.tsx
+++ b/frontend/src/contexts/conversation-websocket-context.tsx
@@ -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";
diff --git a/frontend/src/hooks/chat/use-chat-input-logic.ts b/frontend/src/hooks/chat/use-chat-input-logic.ts
index d908882a40..21dc682fc9 100644
--- a/frontend/src/hooks/chat/use-chat-input-logic.ts
+++ b/frontend/src/hooks/chat/use-chat-input-logic.ts
@@ -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
diff --git a/frontend/src/hooks/chat/use-grip-resize.ts b/frontend/src/hooks/chat/use-grip-resize.ts
index b29f820399..a46d3fbe4f 100644
--- a/frontend/src/hooks/chat/use-grip-resize.ts
+++ b/frontend/src/hooks/chat/use-grip-resize.ts
@@ -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
diff --git a/frontend/src/hooks/query/use-microagent-management-conversations.ts b/frontend/src/hooks/query/use-microagent-management-conversations.ts
index 4c83ca2f75..947cbcf509 100644
--- a/frontend/src/hooks/query/use-microagent-management-conversations.ts
+++ b/frontend/src/hooks/query/use-microagent-management-conversations.ts
@@ -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,
diff --git a/frontend/src/hooks/use-auto-resize.ts b/frontend/src/hooks/use-auto-resize.ts
index 6a86784e0f..52546d78ec 100644
--- a/frontend/src/hooks/use-auto-resize.ts
+++ b/frontend/src/hooks/use-auto-resize.ts
@@ -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";
diff --git a/frontend/src/hooks/use-handle-plan-click.ts b/frontend/src/hooks/use-handle-plan-click.ts
index 9734bab8da..1507665812 100644
--- a/frontend/src/hooks/use-handle-plan-click.ts
+++ b/frontend/src/hooks/use-handle-plan-click.ts
@@ -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";
diff --git a/frontend/src/hooks/use-terminal.ts b/frontend/src/hooks/use-terminal.ts
index d01132a85f..caa2e42a15 100644
--- a/frontend/src/hooks/use-terminal.ts
+++ b/frontend/src/hooks/use-terminal.ts
@@ -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(null);
const ref = React.useRef(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;
};
diff --git a/frontend/src/i18n/declaration.ts b/frontend/src/i18n/declaration.ts
index 2f99b1aef6..1b330730d9 100644
--- a/frontend/src/i18n/declaration.ts
+++ b/frontend/src/i18n/declaration.ts
@@ -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",
diff --git a/frontend/src/i18n/translation.json b/frontend/src/i18n/translation.json
index fc4ca89dbc..a421de5ddf 100644
--- a/frontend/src/i18n/translation.json
+++ b/frontend/src/i18n/translation.json
@@ -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": "更新",
diff --git a/frontend/src/mocks/secrets-handlers.ts b/frontend/src/mocks/secrets-handlers.ts
index 3d5570943a..18c4dc98fd 100644
--- a/frontend/src/mocks/secrets-handlers.ts
+++ b/frontend/src/mocks/secrets-handlers.ts
@@ -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);
diff --git a/frontend/src/mocks/settings-handlers.ts b/frontend/src/mocks/settings-handlers.ts
index c08cd8dc36..00de1e9c5d 100644
--- a/frontend/src/mocks/settings-handlers.ts
+++ b/frontend/src/mocks/settings-handlers.ts
@@ -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> =
Object.fromEntries(
Object.entries(rawTokens)
- .filter(([, val]) => val && val.token)
+ .filter(([, val]) => val?.token)
.map(([provider]) => [provider as Provider, ""]),
);
diff --git a/frontend/src/routes/conversation.tsx b/frontend/src/routes/conversation.tsx
index ec19051530..0237878e1e 100644
--- a/frontend/src/routes/conversation.tsx
+++ b/frontend/src/routes/conversation.tsx
@@ -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";
diff --git a/frontend/src/routes/planner-tab.tsx b/frontend/src/routes/planner-tab.tsx
index 2e5af229ef..f17a0acbc5 100644
--- a/frontend/src/routes/planner-tab.tsx
+++ b/frontend/src/routes/planner-tab.tsx
@@ -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";
diff --git a/frontend/src/routes/served-tab.tsx b/frontend/src/routes/served-tab.tsx
index f2f6b26883..b6abb5b3d3 100644
--- a/frontend/src/routes/served-tab.tsx
+++ b/frontend/src/routes/served-tab.tsx
@@ -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)}
>
@@ -72,11 +73,17 @@ function ServedApp() {
type="button"
onClick={() => setRefreshKey((prev) => prev + 1)}
className="text-sm"
+ aria-label={t(I18nKey.BUTTON$REFRESH)}
>
-