diff --git a/.openhands/microagents/repo.md b/.openhands/microagents/repo.md
index ceb87bc2f7..cd3ef33074 100644
--- a/.openhands/microagents/repo.md
+++ b/.openhands/microagents/repo.md
@@ -63,7 +63,7 @@ Frontend:
- We use TanStack Query (fka React Query) for data fetching and cache management
- Data Access Layer: API client methods are located in `frontend/src/api` and should never be called directly from UI components - they must always be wrapped with TanStack Query
- Custom hooks are located in `frontend/src/hooks/query/` and `frontend/src/hooks/mutation/`
- - Query hooks should follow the pattern use[Resource] (e.g., `useConversationMicroagents`)
+ - Query hooks should follow the pattern use[Resource] (e.g., `useConversationSkills`)
- Mutation hooks should follow the pattern use[Action] (e.g., `useDeleteConversation`)
- Architecture rule: UI components → TanStack Query hooks → Data Access Layer (`frontend/src/api`) → API endpoints
diff --git a/Development.md b/Development.md
index bfa057efc1..421959a5ec 100644
--- a/Development.md
+++ b/Development.md
@@ -161,7 +161,7 @@ poetry run pytest ./tests/unit/test_*.py
To reduce build time (e.g., if no changes were made to the client-runtime component), you can use an existing Docker
container image by setting the SANDBOX_RUNTIME_CONTAINER_IMAGE environment variable to the desired Docker image.
-Example: `export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/openhands/runtime:0.62-nikolaik`
+Example: `export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/openhands/runtime:1.0-nikolaik`
## Develop inside Docker container
diff --git a/README.md b/README.md
index 9fabb37a6e..3928ed32d9 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@
-
+
diff --git a/containers/dev/compose.yml b/containers/dev/compose.yml
index c6168b094f..7ff5042081 100644
--- a/containers/dev/compose.yml
+++ b/containers/dev/compose.yml
@@ -12,7 +12,7 @@ services:
- SANDBOX_API_HOSTNAME=host.docker.internal
- DOCKER_HOST_ADDR=host.docker.internal
#
- - SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/openhands/runtime:0.62-nikolaik}
+ - SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/openhands/runtime:1.0-nikolaik}
- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234}
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
ports:
diff --git a/docker-compose.yml b/docker-compose.yml
index b663324625..d4aef552c0 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -7,7 +7,7 @@ services:
image: openhands:latest
container_name: openhands-app-${DATE:-}
environment:
- - SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-docker.openhands.dev/openhands/runtime:0.62-nikolaik}
+ - SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-docker.openhands.dev/openhands/runtime:1.0-nikolaik}
#- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234} # enable this only if you want a specific non-root sandbox user but you will have to manually adjust permissions of ~/.openhands for this user
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
ports:
diff --git a/enterprise/allhands-realm-github-provider.json.tmpl b/enterprise/allhands-realm-github-provider.json.tmpl
index 6cdaa34383..35ff5f0afc 100644
--- a/enterprise/allhands-realm-github-provider.json.tmpl
+++ b/enterprise/allhands-realm-github-provider.json.tmpl
@@ -721,6 +721,7 @@
"https://$WEB_HOST/oauth/keycloak/callback",
"https://$WEB_HOST/oauth/keycloak/offline/callback",
"https://$WEB_HOST/slack/keycloak-callback",
+ "https://$WEB_HOST/oauth/device/keycloak-callback",
"https://$WEB_HOST/api/email/verified",
"/realms/$KEYCLOAK_REALM_NAME/$KEYCLOAK_CLIENT_ID/*"
],
diff --git a/enterprise/migrations/versions/084_create_device_codes_table.py b/enterprise/migrations/versions/084_create_device_codes_table.py
new file mode 100644
index 0000000000..0898e09ef5
--- /dev/null
+++ b/enterprise/migrations/versions/084_create_device_codes_table.py
@@ -0,0 +1,49 @@
+"""Create device_codes table for OAuth 2.0 Device Flow
+
+Revision ID: 084
+Revises: 083
+Create Date: 2024-12-10 12:00:00.000000
+
+"""
+
+import sqlalchemy as sa
+from alembic import op
+
+# revision identifiers, used by Alembic.
+revision = '084'
+down_revision = '083'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+ """Create device_codes table for OAuth 2.0 Device Flow."""
+ op.create_table(
+ 'device_codes',
+ sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
+ sa.Column('device_code', sa.String(length=128), nullable=False),
+ sa.Column('user_code', sa.String(length=16), nullable=False),
+ sa.Column('status', sa.String(length=32), nullable=False),
+ sa.Column('keycloak_user_id', sa.String(length=255), nullable=True),
+ sa.Column('expires_at', sa.DateTime(timezone=True), nullable=False),
+ sa.Column('authorized_at', sa.DateTime(timezone=True), nullable=True),
+ # Rate limiting fields for RFC 8628 section 3.5 compliance
+ sa.Column('last_poll_time', sa.DateTime(timezone=True), nullable=True),
+ sa.Column('current_interval', sa.Integer(), nullable=False, default=5),
+ sa.PrimaryKeyConstraint('id'),
+ )
+
+ # Create indexes for efficient lookups
+ op.create_index(
+ 'ix_device_codes_device_code', 'device_codes', ['device_code'], unique=True
+ )
+ op.create_index(
+ 'ix_device_codes_user_code', 'device_codes', ['user_code'], unique=True
+ )
+
+
+def downgrade():
+ """Drop device_codes table."""
+ op.drop_index('ix_device_codes_user_code', table_name='device_codes')
+ op.drop_index('ix_device_codes_device_code', table_name='device_codes')
+ op.drop_table('device_codes')
diff --git a/enterprise/poetry.lock b/enterprise/poetry.lock
index 13645253c5..bd2c55c317 100644
--- a/enterprise/poetry.lock
+++ b/enterprise/poetry.lock
@@ -1,4 +1,4 @@
-# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand.
+# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand.
[[package]]
name = "aiofiles"
@@ -4624,14 +4624,14 @@ files = [
[[package]]
name = "lmnr"
-version = "0.7.20"
+version = "0.7.24"
description = "Python SDK for Laminar"
optional = false
python-versions = "<4,>=3.10"
groups = ["main"]
files = [
- {file = "lmnr-0.7.20-py3-none-any.whl", hash = "sha256:5f9fa7444e6f96c25e097f66484ff29e632bdd1de0e9346948bf5595f4a8af38"},
- {file = "lmnr-0.7.20.tar.gz", hash = "sha256:1f484cd618db2d71af65f90a0b8b36d20d80dc91a5138b811575c8677bf7c4fd"},
+ {file = "lmnr-0.7.24-py3-none-any.whl", hash = "sha256:ad780d4a62ece897048811f3368639c240a9329ab31027da8c96545137a3a08a"},
+ {file = "lmnr-0.7.24.tar.gz", hash = "sha256:aa6973f46fc4ba95c9061c1feceb58afc02eb43c9376c21e32545371ff6123d7"},
]
[package.dependencies]
@@ -4654,14 +4654,15 @@ tqdm = ">=4.0"
[package.extras]
alephalpha = ["opentelemetry-instrumentation-alephalpha (>=0.47.1)"]
-all = ["opentelemetry-instrumentation-alephalpha (>=0.47.1)", "opentelemetry-instrumentation-bedrock (>=0.47.1)", "opentelemetry-instrumentation-chromadb (>=0.47.1)", "opentelemetry-instrumentation-cohere (>=0.47.1)", "opentelemetry-instrumentation-crewai (>=0.47.1)", "opentelemetry-instrumentation-haystack (>=0.47.1)", "opentelemetry-instrumentation-lancedb (>=0.47.1)", "opentelemetry-instrumentation-langchain (>=0.47.1)", "opentelemetry-instrumentation-llamaindex (>=0.47.1)", "opentelemetry-instrumentation-marqo (>=0.47.1)", "opentelemetry-instrumentation-mcp (>=0.47.1)", "opentelemetry-instrumentation-milvus (>=0.47.1)", "opentelemetry-instrumentation-mistralai (>=0.47.1)", "opentelemetry-instrumentation-ollama (>=0.47.1)", "opentelemetry-instrumentation-pinecone (>=0.47.1)", "opentelemetry-instrumentation-qdrant (>=0.47.1)", "opentelemetry-instrumentation-replicate (>=0.47.1)", "opentelemetry-instrumentation-sagemaker (>=0.47.1)", "opentelemetry-instrumentation-together (>=0.47.1)", "opentelemetry-instrumentation-transformers (>=0.47.1)", "opentelemetry-instrumentation-vertexai (>=0.47.1)", "opentelemetry-instrumentation-watsonx (>=0.47.1)", "opentelemetry-instrumentation-weaviate (>=0.47.1)"]
+all = ["opentelemetry-instrumentation-alephalpha (>=0.47.1)", "opentelemetry-instrumentation-bedrock (>=0.47.1)", "opentelemetry-instrumentation-chromadb (>=0.47.1)", "opentelemetry-instrumentation-cohere (>=0.47.1)", "opentelemetry-instrumentation-crewai (>=0.47.1)", "opentelemetry-instrumentation-haystack (>=0.47.1)", "opentelemetry-instrumentation-lancedb (>=0.47.1)", "opentelemetry-instrumentation-langchain (>=0.47.1,<0.48.0)", "opentelemetry-instrumentation-llamaindex (>=0.47.1)", "opentelemetry-instrumentation-marqo (>=0.47.1)", "opentelemetry-instrumentation-mcp (>=0.47.1)", "opentelemetry-instrumentation-milvus (>=0.47.1)", "opentelemetry-instrumentation-mistralai (>=0.47.1)", "opentelemetry-instrumentation-ollama (>=0.47.1)", "opentelemetry-instrumentation-pinecone (>=0.47.1)", "opentelemetry-instrumentation-qdrant (>=0.47.1)", "opentelemetry-instrumentation-replicate (>=0.47.1)", "opentelemetry-instrumentation-sagemaker (>=0.47.1)", "opentelemetry-instrumentation-together (>=0.47.1)", "opentelemetry-instrumentation-transformers (>=0.47.1)", "opentelemetry-instrumentation-vertexai (>=0.47.1)", "opentelemetry-instrumentation-watsonx (>=0.47.1)", "opentelemetry-instrumentation-weaviate (>=0.47.1)"]
bedrock = ["opentelemetry-instrumentation-bedrock (>=0.47.1)"]
chromadb = ["opentelemetry-instrumentation-chromadb (>=0.47.1)"]
+claude-agent-sdk = ["lmnr-claude-code-proxy (>=0.1.0a5)"]
cohere = ["opentelemetry-instrumentation-cohere (>=0.47.1)"]
crewai = ["opentelemetry-instrumentation-crewai (>=0.47.1)"]
haystack = ["opentelemetry-instrumentation-haystack (>=0.47.1)"]
lancedb = ["opentelemetry-instrumentation-lancedb (>=0.47.1)"]
-langchain = ["opentelemetry-instrumentation-langchain (>=0.47.1)"]
+langchain = ["opentelemetry-instrumentation-langchain (>=0.47.1,<0.48.0)"]
llamaindex = ["opentelemetry-instrumentation-llamaindex (>=0.47.1)"]
marqo = ["opentelemetry-instrumentation-marqo (>=0.47.1)"]
mcp = ["opentelemetry-instrumentation-mcp (>=0.47.1)"]
@@ -5835,14 +5836,14 @@ llama = ["llama-index (>=0.12.29,<0.13.0)", "llama-index-core (>=0.12.29,<0.13.0
[[package]]
name = "openhands-agent-server"
-version = "1.5.2"
+version = "1.6.0"
description = "OpenHands Agent Server - REST/WebSocket interface for OpenHands AI Agent"
optional = false
python-versions = ">=3.12"
groups = ["main"]
files = [
- {file = "openhands_agent_server-1.5.2-py3-none-any.whl", hash = "sha256:7a368f61036f85446f566b9f6f9d6c7318684776cf2293daa5bce3ee19ac077d"},
- {file = "openhands_agent_server-1.5.2.tar.gz", hash = "sha256:dfaf5583dd71dae933643a8f8160156ce6fa7ed20db5cc3c45465b079bc576cd"},
+ {file = "openhands_agent_server-1.6.0-py3-none-any.whl", hash = "sha256:e6ae865ac3e7a96b234e10a0faad23f6210e025bbf7721cb66bc7a71d160848c"},
+ {file = "openhands_agent_server-1.6.0.tar.gz", hash = "sha256:44ce7694ae2d4bb0666d318ef13e6618bd4dc73022c60354839fe6130e67d02a"},
]
[package.dependencies]
@@ -5859,7 +5860,7 @@ wsproto = ">=1.2.0"
[[package]]
name = "openhands-ai"
-version = "0.62.0"
+version = "0.0.0-post.5687+7853b41ad"
description = "OpenHands: Code Less, Make More"
optional = false
python-versions = "^3.12,<3.14"
@@ -5901,9 +5902,9 @@ memory-profiler = "^0.61.0"
numpy = "*"
openai = "2.8.0"
openhands-aci = "0.3.2"
-openhands-agent-server = "1.5.2"
-openhands-sdk = "1.5.2"
-openhands-tools = "1.5.2"
+openhands-agent-server = "1.6.0"
+openhands-sdk = "1.6.0"
+openhands-tools = "1.6.0"
opentelemetry-api = "^1.33.1"
opentelemetry-exporter-otlp-proto-grpc = "^1.33.1"
pathspec = "^0.12.1"
@@ -5959,14 +5960,14 @@ url = ".."
[[package]]
name = "openhands-sdk"
-version = "1.5.2"
+version = "1.6.0"
description = "OpenHands SDK - Core functionality for building AI agents"
optional = false
python-versions = ">=3.12"
groups = ["main"]
files = [
- {file = "openhands_sdk-1.5.2-py3-none-any.whl", hash = "sha256:593430e9c8729e345fce3fca7e9a9a7ef084a08222d6ba42113e6ba5f6e9f15d"},
- {file = "openhands_sdk-1.5.2.tar.gz", hash = "sha256:798aa8f8ccd84b15deb418c4301d00f33da288bc1a8d41efa5cc47c10aaf3fd6"},
+ {file = "openhands_sdk-1.6.0-py3-none-any.whl", hash = "sha256:94d2f87fb35406373da6728ae2d88584137f9e9b67fa0e940444c72f2e44e7d3"},
+ {file = "openhands_sdk-1.6.0.tar.gz", hash = "sha256:f45742350e3874a7f5b08befc4a9d5adc7e4454f7ab5f8391c519eee3116090f"},
]
[package.dependencies]
@@ -5974,7 +5975,7 @@ deprecation = ">=2.1.0"
fastmcp = ">=2.11.3"
httpx = ">=0.27.0"
litellm = ">=1.80.7"
-lmnr = ">=0.7.20"
+lmnr = ">=0.7.24"
pydantic = ">=2.11.7"
python-frontmatter = ">=1.1.0"
python-json-logger = ">=3.3.0"
@@ -5986,14 +5987,14 @@ boto3 = ["boto3 (>=1.35.0)"]
[[package]]
name = "openhands-tools"
-version = "1.5.2"
+version = "1.6.0"
description = "OpenHands Tools - Runtime tools for AI agents"
optional = false
python-versions = ">=3.12"
groups = ["main"]
files = [
- {file = "openhands_tools-1.5.2-py3-none-any.whl", hash = "sha256:33e9c2af65aaa7b6b9a10b42d2fb11137e6b35e7ac02a4b9269ef37b5c79cc01"},
- {file = "openhands_tools-1.5.2.tar.gz", hash = "sha256:4644a24144fbdf630fb0edc303526b4add61b3fbe7a7434da73f231312c34846"},
+ {file = "openhands_tools-1.6.0-py3-none-any.whl", hash = "sha256:176556d44186536751b23fe052d3505492cc2afb8d52db20fb7a2cc0169cd57a"},
+ {file = "openhands_tools-1.6.0.tar.gz", hash = "sha256:d07ba31050fd4a7891a4c48388aa53ce9f703e17064ddbd59146d6c77e5980b3"},
]
[package.dependencies]
diff --git a/enterprise/saas_server.py b/enterprise/saas_server.py
index 4c3c7c49ba..96e19a9815 100644
--- a/enterprise/saas_server.py
+++ b/enterprise/saas_server.py
@@ -34,6 +34,7 @@ from server.routes.integration.jira_dc import jira_dc_integration_router # noqa
from server.routes.integration.linear import linear_integration_router # noqa: E402
from server.routes.integration.slack import slack_router # noqa: E402
from server.routes.mcp_patch import patch_mcp_server # noqa: E402
+from server.routes.oauth_device import oauth_device_router # noqa: E402
from server.routes.readiness import readiness_router # noqa: E402
from server.routes.user import saas_user_router # noqa: E402
@@ -60,6 +61,7 @@ base_app.mount('/internal/metrics', metrics_app())
base_app.include_router(readiness_router) # Add routes for readiness checks
base_app.include_router(api_router) # Add additional route for github auth
base_app.include_router(oauth_router) # Add additional route for oauth callback
+base_app.include_router(oauth_device_router) # Add OAuth 2.0 Device Flow routes
base_app.include_router(saas_user_router) # Add additional route SAAS user calls
base_app.include_router(
billing_router
diff --git a/enterprise/server/legacy_conversation_manager.py b/enterprise/server/legacy_conversation_manager.py
deleted file mode 100644
index 5c82b5b420..0000000000
--- a/enterprise/server/legacy_conversation_manager.py
+++ /dev/null
@@ -1,331 +0,0 @@
-from __future__ import annotations
-
-import time
-from dataclasses import dataclass, field
-
-import socketio
-from server.clustered_conversation_manager import ClusteredConversationManager
-from server.saas_nested_conversation_manager import SaasNestedConversationManager
-
-from openhands.core.config import LLMConfig, OpenHandsConfig
-from openhands.events.action import MessageAction
-from openhands.server.config.server_config import ServerConfig
-from openhands.server.conversation_manager.conversation_manager import (
- ConversationManager,
-)
-from openhands.server.data_models.agent_loop_info import AgentLoopInfo
-from openhands.server.monitoring import MonitoringListener
-from openhands.server.session.conversation import ServerConversation
-from openhands.storage.data_models.settings import Settings
-from openhands.storage.files import FileStore
-from openhands.utils.async_utils import wait_all
-
-_LEGACY_ENTRY_TIMEOUT_SECONDS = 3600
-
-
-@dataclass
-class LegacyCacheEntry:
- """Cache entry for legacy mode status."""
-
- is_legacy: bool
- timestamp: float
-
-
-@dataclass
-class LegacyConversationManager(ConversationManager):
- """
- Conversation manager for use while migrating - since existing conversations are not nested!
- Separate class from SaasNestedConversationManager so it can be easliy removed in a few weeks.
- (As of 2025-07-23)
- """
-
- sio: socketio.AsyncServer
- config: OpenHandsConfig
- server_config: ServerConfig
- file_store: FileStore
- conversation_manager: SaasNestedConversationManager
- legacy_conversation_manager: ClusteredConversationManager
- _legacy_cache: dict[str, LegacyCacheEntry] = field(default_factory=dict)
-
- async def __aenter__(self):
- await wait_all(
- [
- self.conversation_manager.__aenter__(),
- self.legacy_conversation_manager.__aenter__(),
- ]
- )
- return self
-
- async def __aexit__(self, exc_type, exc_value, traceback):
- await wait_all(
- [
- self.conversation_manager.__aexit__(exc_type, exc_value, traceback),
- self.legacy_conversation_manager.__aexit__(
- exc_type, exc_value, traceback
- ),
- ]
- )
-
- async def request_llm_completion(
- self,
- sid: str,
- service_id: str,
- llm_config: LLMConfig,
- messages: list[dict[str, str]],
- ) -> str:
- session = self.get_agent_session(sid)
- llm_registry = session.llm_registry
- return llm_registry.request_extraneous_completion(
- service_id, llm_config, messages
- )
-
- async def attach_to_conversation(
- self, sid: str, user_id: str | None = None
- ) -> ServerConversation | None:
- if await self.should_start_in_legacy_mode(sid):
- return await self.legacy_conversation_manager.attach_to_conversation(
- sid, user_id
- )
- return await self.conversation_manager.attach_to_conversation(sid, user_id)
-
- async def detach_from_conversation(self, conversation: ServerConversation):
- if await self.should_start_in_legacy_mode(conversation.sid):
- return await self.legacy_conversation_manager.detach_from_conversation(
- conversation
- )
- return await self.conversation_manager.detach_from_conversation(conversation)
-
- async def join_conversation(
- self,
- sid: str,
- connection_id: str,
- settings: Settings,
- user_id: str | None,
- ) -> AgentLoopInfo:
- if await self.should_start_in_legacy_mode(sid):
- return await self.legacy_conversation_manager.join_conversation(
- sid, connection_id, settings, user_id
- )
- return await self.conversation_manager.join_conversation(
- sid, connection_id, settings, user_id
- )
-
- def get_agent_session(self, sid: str):
- session = self.legacy_conversation_manager.get_agent_session(sid)
- if session is None:
- session = self.conversation_manager.get_agent_session(sid)
- return session
-
- async def get_running_agent_loops(
- self, user_id: str | None = None, filter_to_sids: set[str] | None = None
- ) -> set[str]:
- if filter_to_sids and len(filter_to_sids) == 1:
- sid = next(iter(filter_to_sids))
- if await self.should_start_in_legacy_mode(sid):
- return await self.legacy_conversation_manager.get_running_agent_loops(
- user_id, filter_to_sids
- )
- return await self.conversation_manager.get_running_agent_loops(
- user_id, filter_to_sids
- )
-
- # Get all running agent loops from both managers
- agent_loops, legacy_agent_loops = await wait_all(
- [
- self.conversation_manager.get_running_agent_loops(
- user_id, filter_to_sids
- ),
- self.legacy_conversation_manager.get_running_agent_loops(
- user_id, filter_to_sids
- ),
- ]
- )
-
- # Combine the results
- result = set()
- for sid in legacy_agent_loops:
- if await self.should_start_in_legacy_mode(sid):
- result.add(sid)
-
- for sid in agent_loops:
- if not await self.should_start_in_legacy_mode(sid):
- result.add(sid)
-
- return result
-
- async def is_agent_loop_running(self, sid: str) -> bool:
- return bool(await self.get_running_agent_loops(filter_to_sids={sid}))
-
- async def get_connections(
- self, user_id: str | None = None, filter_to_sids: set[str] | None = None
- ) -> dict[str, str]:
- if filter_to_sids and len(filter_to_sids) == 1:
- sid = next(iter(filter_to_sids))
- if await self.should_start_in_legacy_mode(sid):
- return await self.legacy_conversation_manager.get_connections(
- user_id, filter_to_sids
- )
- return await self.conversation_manager.get_connections(
- user_id, filter_to_sids
- )
- agent_loops, legacy_agent_loops = await wait_all(
- [
- self.conversation_manager.get_connections(user_id, filter_to_sids),
- self.legacy_conversation_manager.get_connections(
- user_id, filter_to_sids
- ),
- ]
- )
- legacy_agent_loops.update(agent_loops)
- return legacy_agent_loops
-
- async def maybe_start_agent_loop(
- self,
- sid: str,
- settings: Settings,
- user_id: str, # type: ignore[override]
- initial_user_msg: MessageAction | None = None,
- replay_json: str | None = None,
- ) -> AgentLoopInfo:
- if await self.should_start_in_legacy_mode(sid):
- return await self.legacy_conversation_manager.maybe_start_agent_loop(
- sid, settings, user_id, initial_user_msg, replay_json
- )
- return await self.conversation_manager.maybe_start_agent_loop(
- sid, settings, user_id, initial_user_msg, replay_json
- )
-
- async def send_to_event_stream(self, connection_id: str, data: dict):
- return await self.legacy_conversation_manager.send_to_event_stream(
- connection_id, data
- )
-
- async def send_event_to_conversation(self, sid: str, data: dict):
- if await self.should_start_in_legacy_mode(sid):
- await self.legacy_conversation_manager.send_event_to_conversation(sid, data)
- await self.conversation_manager.send_event_to_conversation(sid, data)
-
- async def disconnect_from_session(self, connection_id: str):
- return await self.legacy_conversation_manager.disconnect_from_session(
- connection_id
- )
-
- async def close_session(self, sid: str):
- if await self.should_start_in_legacy_mode(sid):
- await self.legacy_conversation_manager.close_session(sid)
- await self.conversation_manager.close_session(sid)
-
- async def get_agent_loop_info(
- self, user_id: str | None = None, filter_to_sids: set[str] | None = None
- ) -> list[AgentLoopInfo]:
- if filter_to_sids and len(filter_to_sids) == 1:
- sid = next(iter(filter_to_sids))
- if await self.should_start_in_legacy_mode(sid):
- return await self.legacy_conversation_manager.get_agent_loop_info(
- user_id, filter_to_sids
- )
- return await self.conversation_manager.get_agent_loop_info(
- user_id, filter_to_sids
- )
- agent_loops, legacy_agent_loops = await wait_all(
- [
- self.conversation_manager.get_agent_loop_info(user_id, filter_to_sids),
- self.legacy_conversation_manager.get_agent_loop_info(
- user_id, filter_to_sids
- ),
- ]
- )
-
- # Combine results
- result = []
- legacy_sids = set()
-
- # Add legacy agent loops
- for agent_loop in legacy_agent_loops:
- if await self.should_start_in_legacy_mode(agent_loop.conversation_id):
- result.append(agent_loop)
- legacy_sids.add(agent_loop.conversation_id)
-
- # Add non-legacy agent loops
- for agent_loop in agent_loops:
- if (
- agent_loop.conversation_id not in legacy_sids
- and not await self.should_start_in_legacy_mode(
- agent_loop.conversation_id
- )
- ):
- result.append(agent_loop)
-
- return result
-
- def _cleanup_expired_cache_entries(self):
- """Remove expired entries from the local cache."""
- current_time = time.time()
- expired_keys = [
- key
- for key, entry in self._legacy_cache.items()
- if current_time - entry.timestamp > _LEGACY_ENTRY_TIMEOUT_SECONDS
- ]
- for key in expired_keys:
- del self._legacy_cache[key]
-
- async def should_start_in_legacy_mode(self, conversation_id: str) -> bool:
- """
- Check if a conversation should run in legacy mode by directly checking the runtime.
- The /list method does not include stopped conversations even though the PVC for these
- may not yet have been deleted, so we need to check /sessions/{session_id} directly.
- """
- # Clean up expired entries periodically
- self._cleanup_expired_cache_entries()
-
- # First check the local cache
- if conversation_id in self._legacy_cache:
- cached_entry = self._legacy_cache[conversation_id]
- # Check if the cached value is still valid
- if time.time() - cached_entry.timestamp <= _LEGACY_ENTRY_TIMEOUT_SECONDS:
- return cached_entry.is_legacy
-
- # If not in cache or expired, check the runtime directly
- runtime = await self.conversation_manager._get_runtime(conversation_id)
- is_legacy = self.is_legacy_runtime(runtime)
-
- # Cache the result with current timestamp
- self._legacy_cache[conversation_id] = LegacyCacheEntry(is_legacy, time.time())
-
- return is_legacy
-
- def is_legacy_runtime(self, runtime: dict | None) -> bool:
- """
- Determine if a runtime is a legacy runtime based on its command.
-
- Args:
- runtime: The runtime dictionary or None if not found
-
- Returns:
- bool: True if this is a legacy runtime, False otherwise
- """
- if runtime is None:
- return False
- return 'openhands.server' not in runtime['command']
-
- @classmethod
- def get_instance(
- cls,
- sio: socketio.AsyncServer,
- config: OpenHandsConfig,
- file_store: FileStore,
- server_config: ServerConfig,
- monitoring_listener: MonitoringListener,
- ) -> ConversationManager:
- return LegacyConversationManager(
- sio=sio,
- config=config,
- server_config=server_config,
- file_store=file_store,
- conversation_manager=SaasNestedConversationManager.get_instance(
- sio, config, file_store, server_config, monitoring_listener
- ),
- legacy_conversation_manager=ClusteredConversationManager.get_instance(
- sio, config, file_store, server_config, monitoring_listener
- ),
- )
diff --git a/enterprise/server/middleware.py b/enterprise/server/middleware.py
index 2972c1ec38..54e3319595 100644
--- a/enterprise/server/middleware.py
+++ b/enterprise/server/middleware.py
@@ -152,17 +152,22 @@ class SetAuthCookieMiddleware:
return False
path = request.url.path
- is_api_that_should_attach = path.startswith('/api') and path not in (
+ ignore_paths = (
'/api/options/config',
'/api/keycloak/callback',
'/api/billing/success',
'/api/billing/cancel',
'/api/billing/customer-setup-success',
'/api/billing/stripe-webhook',
+ '/oauth/device/authorize',
+ '/oauth/device/token',
)
+ if path in ignore_paths:
+ return False
is_mcp = path.startswith('/mcp')
- return is_api_that_should_attach or is_mcp
+ is_api_route = path.startswith('/api')
+ return is_api_route or is_mcp
async def _logout(self, request: Request):
# Log out of keycloak - this prevents issues where you did not log in with the idp you believe you used
diff --git a/enterprise/server/routes/oauth_device.py b/enterprise/server/routes/oauth_device.py
new file mode 100644
index 0000000000..39ff9a4081
--- /dev/null
+++ b/enterprise/server/routes/oauth_device.py
@@ -0,0 +1,324 @@
+"""OAuth 2.0 Device Flow endpoints for CLI authentication."""
+
+from datetime import UTC, datetime, timedelta
+from typing import Optional
+
+from fastapi import APIRouter, Depends, Form, HTTPException, Request, status
+from fastapi.responses import JSONResponse
+from pydantic import BaseModel
+from storage.api_key_store import ApiKeyStore
+from storage.database import session_maker
+from storage.device_code_store import DeviceCodeStore
+
+from openhands.core.logger import openhands_logger as logger
+from openhands.server.user_auth import get_user_id
+
+# ---------------------------------------------------------------------------
+# Constants
+# ---------------------------------------------------------------------------
+
+DEVICE_CODE_EXPIRES_IN = 600 # 10 minutes
+DEVICE_TOKEN_POLL_INTERVAL = 5 # seconds
+
+API_KEY_NAME = 'Device Link Access Key'
+KEY_EXPIRATION_TIME = timedelta(days=1) # Key expires in 24 hours
+
+# ---------------------------------------------------------------------------
+# Models
+# ---------------------------------------------------------------------------
+
+
+class DeviceAuthorizationResponse(BaseModel):
+ device_code: str
+ user_code: str
+ verification_uri: str
+ verification_uri_complete: str
+ expires_in: int
+ interval: int
+
+
+class DeviceTokenResponse(BaseModel):
+ access_token: str # This will be the user's API key
+ token_type: str = 'Bearer'
+ expires_in: Optional[int] = None # API keys may not have expiration
+
+
+class DeviceTokenErrorResponse(BaseModel):
+ error: str
+ error_description: Optional[str] = None
+ interval: Optional[int] = None # Required for slow_down error
+
+
+# ---------------------------------------------------------------------------
+# Router + stores
+# ---------------------------------------------------------------------------
+
+oauth_device_router = APIRouter(prefix='/oauth/device')
+device_code_store = DeviceCodeStore(session_maker)
+
+
+# ---------------------------------------------------------------------------
+# Helpers
+# ---------------------------------------------------------------------------
+
+
+def _oauth_error(
+ status_code: int,
+ error: str,
+ description: str,
+ interval: Optional[int] = None,
+) -> JSONResponse:
+ """Return a JSON OAuth-style error response."""
+ return JSONResponse(
+ status_code=status_code,
+ content=DeviceTokenErrorResponse(
+ error=error,
+ error_description=description,
+ interval=interval,
+ ).model_dump(),
+ )
+
+
+# ---------------------------------------------------------------------------
+# Endpoints
+# ---------------------------------------------------------------------------
+
+
+@oauth_device_router.post('/authorize', response_model=DeviceAuthorizationResponse)
+async def device_authorization(
+ http_request: Request,
+) -> DeviceAuthorizationResponse:
+ """Start device flow by generating device and user codes."""
+ try:
+ device_code_entry = device_code_store.create_device_code(
+ expires_in=DEVICE_CODE_EXPIRES_IN,
+ )
+
+ base_url = str(http_request.base_url).rstrip('/')
+ verification_uri = f'{base_url}/oauth/device/verify'
+ verification_uri_complete = (
+ f'{verification_uri}?user_code={device_code_entry.user_code}'
+ )
+
+ logger.info(
+ 'Device authorization initiated',
+ extra={'user_code': device_code_entry.user_code},
+ )
+
+ return DeviceAuthorizationResponse(
+ device_code=device_code_entry.device_code,
+ user_code=device_code_entry.user_code,
+ verification_uri=verification_uri,
+ verification_uri_complete=verification_uri_complete,
+ expires_in=DEVICE_CODE_EXPIRES_IN,
+ interval=device_code_entry.current_interval,
+ )
+ except Exception as e:
+ logger.exception('Error in device authorization: %s', str(e))
+ raise HTTPException(
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+ detail='Internal server error',
+ ) from e
+
+
+@oauth_device_router.post('/token')
+async def device_token(device_code: str = Form(...)):
+ """Poll for a token until the user authorizes or the code expires."""
+ try:
+ device_code_entry = device_code_store.get_by_device_code(device_code)
+
+ if not device_code_entry:
+ return _oauth_error(
+ status.HTTP_400_BAD_REQUEST,
+ 'invalid_grant',
+ 'Invalid device code',
+ )
+
+ # Check rate limiting (RFC 8628 section 3.5)
+ is_too_fast, current_interval = device_code_entry.check_rate_limit()
+ if is_too_fast:
+ # Update poll time and increase interval
+ device_code_store.update_poll_time(device_code, increase_interval=True)
+ logger.warning(
+ 'Client polling too fast, returning slow_down error',
+ extra={
+ 'device_code': device_code[:8] + '...', # Log partial for privacy
+ 'new_interval': current_interval,
+ },
+ )
+ return _oauth_error(
+ status.HTTP_400_BAD_REQUEST,
+ 'slow_down',
+ f'Polling too frequently. Wait at least {current_interval} seconds between requests.',
+ interval=current_interval,
+ )
+
+ # Update poll time for successful rate limit check
+ device_code_store.update_poll_time(device_code, increase_interval=False)
+
+ if device_code_entry.is_expired():
+ return _oauth_error(
+ status.HTTP_400_BAD_REQUEST,
+ 'expired_token',
+ 'Device code has expired',
+ )
+
+ if device_code_entry.status == 'denied':
+ return _oauth_error(
+ status.HTTP_400_BAD_REQUEST,
+ 'access_denied',
+ 'User denied the authorization request',
+ )
+
+ if device_code_entry.status == 'pending':
+ return _oauth_error(
+ status.HTTP_400_BAD_REQUEST,
+ 'authorization_pending',
+ 'User has not yet completed authorization',
+ )
+
+ if device_code_entry.status == 'authorized':
+ # Retrieve the specific API key for this device using the user_code
+ api_key_store = ApiKeyStore.get_instance()
+ device_key_name = f'{API_KEY_NAME} ({device_code_entry.user_code})'
+ device_api_key = api_key_store.retrieve_api_key_by_name(
+ device_code_entry.keycloak_user_id, device_key_name
+ )
+
+ if not device_api_key:
+ logger.error(
+ 'No device API key found for authorized device',
+ extra={
+ 'user_id': device_code_entry.keycloak_user_id,
+ 'user_code': device_code_entry.user_code,
+ },
+ )
+ return _oauth_error(
+ status.HTTP_500_INTERNAL_SERVER_ERROR,
+ 'server_error',
+ 'API key not found',
+ )
+
+ # Return the API key as access_token
+ return DeviceTokenResponse(
+ access_token=device_api_key,
+ )
+
+ # Fallback for unexpected status values
+ logger.error(
+ 'Unknown device code status',
+ extra={'status': device_code_entry.status},
+ )
+ return _oauth_error(
+ status.HTTP_500_INTERNAL_SERVER_ERROR,
+ 'server_error',
+ 'Unknown device code status',
+ )
+
+ except Exception as e:
+ logger.exception('Error in device token: %s', str(e))
+ return _oauth_error(
+ status.HTTP_500_INTERNAL_SERVER_ERROR,
+ 'server_error',
+ 'Internal server error',
+ )
+
+
+@oauth_device_router.post('/verify-authenticated')
+async def device_verification_authenticated(
+ user_code: str = Form(...),
+ user_id: str = Depends(get_user_id),
+):
+ """Process device verification for authenticated users (called by frontend)."""
+ try:
+ if not user_id:
+ raise HTTPException(
+ status_code=status.HTTP_401_UNAUTHORIZED,
+ detail='Authentication required',
+ )
+
+ # Validate device code
+ device_code_entry = device_code_store.get_by_user_code(user_code)
+ if not device_code_entry:
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST,
+ detail='The device code is invalid or has expired.',
+ )
+
+ if not device_code_entry.is_pending():
+ raise HTTPException(
+ status_code=status.HTTP_400_BAD_REQUEST,
+ detail='This device code has already been processed.',
+ )
+
+ # First, authorize the device code
+ success = device_code_store.authorize_device_code(
+ user_code=user_code,
+ user_id=user_id,
+ )
+
+ if not success:
+ logger.error(
+ 'Failed to authorize device code',
+ extra={'user_code': user_code, 'user_id': user_id},
+ )
+ raise HTTPException(
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+ detail='Failed to authorize the device. Please try again.',
+ )
+
+ # Only create API key AFTER successful authorization
+ api_key_store = ApiKeyStore.get_instance()
+ try:
+ # Create a unique API key for this device using user_code in the name
+ device_key_name = f'{API_KEY_NAME} ({user_code})'
+ api_key_store.create_api_key(
+ user_id,
+ name=device_key_name,
+ expires_at=datetime.now(UTC) + KEY_EXPIRATION_TIME,
+ )
+ logger.info(
+ 'Created new device API key for user after successful authorization',
+ extra={'user_id': user_id, 'user_code': user_code},
+ )
+ except Exception as e:
+ logger.exception(
+ 'Failed to create device API key after authorization: %s', str(e)
+ )
+
+ # Clean up: revert the device authorization since API key creation failed
+ # This prevents the device from being in an authorized state without an API key
+ try:
+ device_code_store.deny_device_code(user_code)
+ logger.info(
+ 'Reverted device authorization due to API key creation failure',
+ extra={'user_code': user_code, 'user_id': user_id},
+ )
+ except Exception as cleanup_error:
+ logger.exception(
+ 'Failed to revert device authorization during cleanup: %s',
+ str(cleanup_error),
+ )
+
+ raise HTTPException(
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+ detail='Failed to create API key for device access.',
+ )
+
+ logger.info(
+ 'Device code authorized with API key successfully',
+ extra={'user_code': user_code, 'user_id': user_id},
+ )
+ return JSONResponse(
+ status_code=status.HTTP_200_OK,
+ content={'message': 'Device authorized successfully!'},
+ )
+
+ except HTTPException:
+ raise
+ except Exception as e:
+ logger.exception('Error in device verification: %s', str(e))
+ raise HTTPException(
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+ detail='An unexpected error occurred. Please try again.',
+ )
diff --git a/enterprise/server/saas_nested_conversation_manager.py b/enterprise/server/saas_nested_conversation_manager.py
index 0c5ece2675..d92e67b9ff 100644
--- a/enterprise/server/saas_nested_conversation_manager.py
+++ b/enterprise/server/saas_nested_conversation_manager.py
@@ -31,6 +31,7 @@ from openhands.events.event_store import EventStore
from openhands.events.serialization.event import event_to_dict
from openhands.integrations.provider import PROVIDER_TOKEN_TYPE, ProviderHandler
from openhands.runtime.impl.remote.remote_runtime import RemoteRuntime
+from openhands.runtime.plugins.vscode import VSCodeRequirement
from openhands.runtime.runtime_status import RuntimeStatus
from openhands.server.config.server_config import ServerConfig
from openhands.server.constants import ROOM_KEY
@@ -71,10 +72,13 @@ RUNTIME_CONVERSATION_URL = RUNTIME_URL_PATTERN + (
)
RUNTIME_USERNAME = os.getenv('RUNTIME_USERNAME')
+
SU_TO_USER = os.getenv('SU_TO_USER', 'false')
truthy = {'1', 'true', 't', 'yes', 'y', 'on'}
SU_TO_USER = str(SU_TO_USER.lower() in truthy).lower()
+DISABLE_VSCODE_PLUGIN = os.getenv('DISABLE_VSCODE_PLUGIN', 'false').lower() == 'true'
+
# Time in seconds before a Redis entry is considered expired if not refreshed
_REDIS_ENTRY_TIMEOUT_SECONDS = 300
@@ -799,6 +803,7 @@ class SaasNestedConversationManager(ConversationManager):
env_vars['INIT_GIT_IN_EMPTY_WORKSPACE'] = '1'
env_vars['ENABLE_V1'] = '0'
env_vars['SU_TO_USER'] = SU_TO_USER
+ env_vars['DISABLE_VSCODE_PLUGIN'] = str(DISABLE_VSCODE_PLUGIN).lower()
# We need this for LLM traces tracking to identify the source of the LLM calls
env_vars['WEB_HOST'] = WEB_HOST
@@ -814,11 +819,18 @@ class SaasNestedConversationManager(ConversationManager):
if self._runtime_container_image:
config.sandbox.runtime_container_image = self._runtime_container_image
+ plugins = [
+ plugin
+ for plugin in agent.sandbox_plugins
+ if not (DISABLE_VSCODE_PLUGIN and isinstance(plugin, VSCodeRequirement))
+ ]
+ logger.info(f'Loaded plugins for runtime {sid}: {plugins}')
+
runtime = RemoteRuntime(
config=config,
event_stream=None, # type: ignore[arg-type]
sid=sid,
- plugins=agent.sandbox_plugins,
+ plugins=plugins,
# env_vars=env_vars,
# status_callback: Callable[..., None] | None = None,
attach_to_existing=False,
diff --git a/enterprise/storage/api_key_store.py b/enterprise/storage/api_key_store.py
index 162ed415c1..693bfdb321 100644
--- a/enterprise/storage/api_key_store.py
+++ b/enterprise/storage/api_key_store.py
@@ -57,9 +57,15 @@ class ApiKeyStore:
return None
# Check if the key has expired
- if key_record.expires_at and key_record.expires_at < now:
- logger.info(f'API key has expired: {key_record.id}')
- return None
+ if key_record.expires_at:
+ # Handle timezone-naive datetime from database by assuming it's UTC
+ expires_at = key_record.expires_at
+ if expires_at.tzinfo is None:
+ expires_at = expires_at.replace(tzinfo=UTC)
+
+ if expires_at < now:
+ logger.info(f'API key has expired: {key_record.id}')
+ return None
# Update last_used_at timestamp
session.execute(
@@ -125,6 +131,33 @@ class ApiKeyStore:
return None
+ def retrieve_api_key_by_name(self, user_id: str, name: str) -> str | None:
+ """Retrieve an API key by name for a specific user."""
+ with self.session_maker() as session:
+ key_record = (
+ session.query(ApiKey)
+ .filter(ApiKey.user_id == user_id, ApiKey.name == name)
+ .first()
+ )
+ return key_record.key if key_record else None
+
+ def delete_api_key_by_name(self, user_id: str, name: str) -> bool:
+ """Delete an API key by name for a specific user."""
+ with self.session_maker() as session:
+ key_record = (
+ session.query(ApiKey)
+ .filter(ApiKey.user_id == user_id, ApiKey.name == name)
+ .first()
+ )
+
+ if not key_record:
+ return False
+
+ session.delete(key_record)
+ session.commit()
+
+ return True
+
@classmethod
def get_instance(cls) -> ApiKeyStore:
"""Get an instance of the ApiKeyStore."""
diff --git a/enterprise/storage/device_code.py b/enterprise/storage/device_code.py
new file mode 100644
index 0000000000..47e18b51bc
--- /dev/null
+++ b/enterprise/storage/device_code.py
@@ -0,0 +1,109 @@
+"""Device code storage model for OAuth 2.0 Device Flow."""
+
+from datetime import datetime, timezone
+from enum import Enum
+
+from sqlalchemy import Column, DateTime, Integer, String
+from storage.base import Base
+
+
+class DeviceCodeStatus(Enum):
+ """Status of a device code authorization request."""
+
+ PENDING = 'pending'
+ AUTHORIZED = 'authorized'
+ EXPIRED = 'expired'
+ DENIED = 'denied'
+
+
+class DeviceCode(Base):
+ """Device code for OAuth 2.0 Device Flow.
+
+ This stores the device codes issued during the device authorization flow,
+ along with their status and associated user information once authorized.
+ """
+
+ __tablename__ = 'device_codes'
+
+ id = Column(Integer, primary_key=True, autoincrement=True)
+ device_code = Column(String(128), unique=True, nullable=False, index=True)
+ user_code = Column(String(16), unique=True, nullable=False, index=True)
+ status = Column(String(32), nullable=False, default=DeviceCodeStatus.PENDING.value)
+
+ # Keycloak user ID who authorized the device (set during verification)
+ keycloak_user_id = Column(String(255), nullable=True)
+
+ # Timestamps
+ expires_at = Column(DateTime(timezone=True), nullable=False)
+ authorized_at = Column(DateTime(timezone=True), nullable=True)
+
+ # Rate limiting fields for RFC 8628 section 3.5 compliance
+ last_poll_time = Column(DateTime(timezone=True), nullable=True)
+ current_interval = Column(Integer, nullable=False, default=5)
+
+ def __repr__(self) -> str:
+ return f"
"
+
+ def is_expired(self) -> bool:
+ """Check if the device code has expired."""
+ now = datetime.now(timezone.utc)
+ return now > self.expires_at
+
+ def is_pending(self) -> bool:
+ """Check if the device code is still pending authorization."""
+ return self.status == DeviceCodeStatus.PENDING.value and not self.is_expired()
+
+ def is_authorized(self) -> bool:
+ """Check if the device code has been authorized."""
+ return self.status == DeviceCodeStatus.AUTHORIZED.value
+
+ def authorize(self, user_id: str) -> None:
+ """Mark the device code as authorized."""
+ self.status = DeviceCodeStatus.AUTHORIZED.value
+ self.keycloak_user_id = user_id # Set the Keycloak user ID during authorization
+ self.authorized_at = datetime.now(timezone.utc)
+
+ def deny(self) -> None:
+ """Mark the device code as denied."""
+ self.status = DeviceCodeStatus.DENIED.value
+
+ def expire(self) -> None:
+ """Mark the device code as expired."""
+ self.status = DeviceCodeStatus.EXPIRED.value
+
+ def check_rate_limit(self) -> tuple[bool, int]:
+ """Check if the client is polling too fast.
+
+ Returns:
+ tuple: (is_too_fast, current_interval)
+ - is_too_fast: True if client should receive slow_down error
+ - current_interval: Current polling interval to use
+ """
+ now = datetime.now(timezone.utc)
+
+ # If this is the first poll, allow it
+ if self.last_poll_time is None:
+ return False, self.current_interval
+
+ # Calculate time since last poll
+ time_since_last_poll = (now - self.last_poll_time).total_seconds()
+
+ # Check if polling too fast
+ if time_since_last_poll < self.current_interval:
+ # Increase interval for slow_down (RFC 8628 section 3.5)
+ new_interval = min(self.current_interval + 5, 60) # Cap at 60 seconds
+ return True, new_interval
+
+ return False, self.current_interval
+
+ def update_poll_time(self, increase_interval: bool = False) -> None:
+ """Update the last poll time and optionally increase the interval.
+
+ Args:
+ increase_interval: If True, increase the current interval for slow_down
+ """
+ self.last_poll_time = datetime.now(timezone.utc)
+
+ if increase_interval:
+ # Increase interval by 5 seconds, cap at 60 seconds (RFC 8628)
+ self.current_interval = min(self.current_interval + 5, 60)
diff --git a/enterprise/storage/device_code_store.py b/enterprise/storage/device_code_store.py
new file mode 100644
index 0000000000..de2fe29cc4
--- /dev/null
+++ b/enterprise/storage/device_code_store.py
@@ -0,0 +1,167 @@
+"""Device code store for OAuth 2.0 Device Flow."""
+
+import secrets
+import string
+from datetime import datetime, timedelta, timezone
+
+from sqlalchemy.exc import IntegrityError
+from storage.device_code import DeviceCode
+
+
+class DeviceCodeStore:
+ """Store for managing OAuth 2.0 device codes."""
+
+ def __init__(self, session_maker):
+ self.session_maker = session_maker
+
+ def generate_user_code(self) -> str:
+ """Generate a human-readable user code (8 characters, uppercase letters and digits)."""
+ # Use a mix of uppercase letters and digits, avoiding confusing characters
+ alphabet = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789' # No I, O, 0, 1
+ return ''.join(secrets.choice(alphabet) for _ in range(8))
+
+ def generate_device_code(self) -> str:
+ """Generate a secure device code (128 characters)."""
+ alphabet = string.ascii_letters + string.digits
+ return ''.join(secrets.choice(alphabet) for _ in range(128))
+
+ def create_device_code(
+ self,
+ expires_in: int = 600, # 10 minutes default
+ max_attempts: int = 10,
+ ) -> DeviceCode:
+ """Create a new device code entry.
+
+ Uses database constraints to ensure uniqueness, avoiding TOCTOU race conditions.
+ Retries on constraint violations until unique codes are generated.
+
+ Args:
+ expires_in: Expiration time in seconds
+ max_attempts: Maximum number of attempts to generate unique codes
+
+ Returns:
+ The created DeviceCode instance
+
+ Raises:
+ RuntimeError: If unable to generate unique codes after max_attempts
+ """
+ for attempt in range(max_attempts):
+ user_code = self.generate_user_code()
+ device_code = self.generate_device_code()
+ expires_at = datetime.now(timezone.utc) + timedelta(seconds=expires_in)
+
+ device_code_entry = DeviceCode(
+ device_code=device_code,
+ user_code=user_code,
+ keycloak_user_id=None, # Will be set during authorization
+ expires_at=expires_at,
+ )
+
+ try:
+ with self.session_maker() as session:
+ session.add(device_code_entry)
+ session.commit()
+ session.refresh(device_code_entry)
+ session.expunge(device_code_entry) # Detach from session cleanly
+ return device_code_entry
+ except IntegrityError:
+ # Constraint violation - codes already exist, retry with new codes
+ continue
+
+ raise RuntimeError(
+ f'Failed to generate unique device codes after {max_attempts} attempts'
+ )
+
+ def get_by_device_code(self, device_code: str) -> DeviceCode | None:
+ """Get device code entry by device code."""
+ with self.session_maker() as session:
+ result = (
+ session.query(DeviceCode).filter_by(device_code=device_code).first()
+ )
+ if result:
+ session.expunge(result) # Detach from session cleanly
+ return result
+
+ def get_by_user_code(self, user_code: str) -> DeviceCode | None:
+ """Get device code entry by user code."""
+ with self.session_maker() as session:
+ result = session.query(DeviceCode).filter_by(user_code=user_code).first()
+ if result:
+ session.expunge(result) # Detach from session cleanly
+ return result
+
+ def authorize_device_code(self, user_code: str, user_id: str) -> bool:
+ """Authorize a device code.
+
+ Args:
+ user_code: The user code to authorize
+ user_id: The user ID from Keycloak
+
+ Returns:
+ True if authorization was successful, False otherwise
+ """
+ with self.session_maker() as session:
+ device_code_entry = (
+ session.query(DeviceCode).filter_by(user_code=user_code).first()
+ )
+
+ if not device_code_entry:
+ return False
+
+ if not device_code_entry.is_pending():
+ return False
+
+ device_code_entry.authorize(user_id)
+ session.commit()
+
+ return True
+
+ def deny_device_code(self, user_code: str) -> bool:
+ """Deny a device code authorization.
+
+ Args:
+ user_code: The user code to deny
+
+ Returns:
+ True if denial was successful, False otherwise
+ """
+ with self.session_maker() as session:
+ device_code_entry = (
+ session.query(DeviceCode).filter_by(user_code=user_code).first()
+ )
+
+ if not device_code_entry:
+ return False
+
+ if not device_code_entry.is_pending():
+ return False
+
+ device_code_entry.deny()
+ session.commit()
+
+ return True
+
+ def update_poll_time(
+ self, device_code: str, increase_interval: bool = False
+ ) -> bool:
+ """Update the poll time for a device code and optionally increase interval.
+
+ Args:
+ device_code: The device code to update
+ increase_interval: If True, increase the polling interval for slow_down
+
+ Returns:
+ True if update was successful, False otherwise
+ """
+ with self.session_maker() as session:
+ device_code_entry = (
+ session.query(DeviceCode).filter_by(device_code=device_code).first()
+ )
+
+ if not device_code_entry:
+ return False
+
+ device_code_entry.update_poll_time(increase_interval)
+ session.commit()
+
+ return True
diff --git a/enterprise/storage/saas_settings_store.py b/enterprise/storage/saas_settings_store.py
index 6cbcb50802..cfcbec7583 100644
--- a/enterprise/storage/saas_settings_store.py
+++ b/enterprise/storage/saas_settings_store.py
@@ -94,6 +94,9 @@ 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/conftest.py b/enterprise/tests/unit/conftest.py
index 08516fd813..873f7b775f 100644
--- a/enterprise/tests/unit/conftest.py
+++ b/enterprise/tests/unit/conftest.py
@@ -12,6 +12,7 @@ from storage.base import Base
# Anything not loaded here may not have a table created for it.
from storage.billing_session import BillingSession
from storage.conversation_work import ConversationWork
+from storage.device_code import DeviceCode # noqa: F401
from storage.feedback import Feedback
from storage.github_app_installation import GithubAppInstallation
from storage.maintenance_task import MaintenanceTask, MaintenanceTaskStatus
diff --git a/enterprise/tests/unit/server/routes/test_oauth_device.py b/enterprise/tests/unit/server/routes/test_oauth_device.py
new file mode 100644
index 0000000000..53682e65f0
--- /dev/null
+++ b/enterprise/tests/unit/server/routes/test_oauth_device.py
@@ -0,0 +1,610 @@
+"""Unit tests for OAuth2 Device Flow endpoints."""
+
+from datetime import UTC, datetime, timedelta
+from unittest.mock import MagicMock, patch
+
+import pytest
+from fastapi import HTTPException, Request
+from fastapi.responses import JSONResponse
+from server.routes.oauth_device import (
+ device_authorization,
+ device_token,
+ device_verification_authenticated,
+)
+from storage.device_code import DeviceCode
+
+
+@pytest.fixture
+def mock_device_code_store():
+ """Mock device code store."""
+ return MagicMock()
+
+
+@pytest.fixture
+def mock_api_key_store():
+ """Mock API key store."""
+ return MagicMock()
+
+
+@pytest.fixture
+def mock_token_manager():
+ """Mock token manager."""
+ return MagicMock()
+
+
+@pytest.fixture
+def mock_request():
+ """Mock FastAPI request."""
+ request = MagicMock(spec=Request)
+ request.base_url = 'https://test.example.com/'
+ return request
+
+
+class TestDeviceAuthorization:
+ """Test device authorization endpoint."""
+
+ @patch('server.routes.oauth_device.device_code_store')
+ async def test_device_authorization_success(self, mock_store, mock_request):
+ """Test successful device authorization."""
+ mock_device = DeviceCode(
+ device_code='test-device-code-123',
+ user_code='ABC12345',
+ expires_at=datetime.now(UTC) + timedelta(minutes=10),
+ current_interval=5, # Default interval
+ )
+ mock_store.create_device_code.return_value = mock_device
+
+ result = await device_authorization(mock_request)
+
+ assert result.device_code == 'test-device-code-123'
+ assert result.user_code == 'ABC12345'
+ assert result.expires_in == 600
+ assert result.interval == 5 # Should match device's current_interval
+ assert 'verify' in result.verification_uri
+ assert 'ABC12345' in result.verification_uri_complete
+
+ @patch('server.routes.oauth_device.device_code_store')
+ async def test_device_authorization_with_increased_interval(
+ self, mock_store, mock_request
+ ):
+ """Test device authorization returns increased interval from rate limiting."""
+ mock_device = DeviceCode(
+ device_code='test-device-code-456',
+ user_code='XYZ98765',
+ expires_at=datetime.now(UTC) + timedelta(minutes=10),
+ current_interval=15, # Increased interval from previous rate limiting
+ )
+ mock_store.create_device_code.return_value = mock_device
+
+ result = await device_authorization(mock_request)
+
+ assert result.device_code == 'test-device-code-456'
+ assert result.user_code == 'XYZ98765'
+ assert result.expires_in == 600
+ assert result.interval == 15 # Should match device's increased current_interval
+ assert 'verify' in result.verification_uri
+ assert 'XYZ98765' in result.verification_uri_complete
+
+
+class TestDeviceToken:
+ """Test device token endpoint."""
+
+ @pytest.mark.parametrize(
+ 'device_exists,status,expected_error',
+ [
+ (False, None, 'invalid_grant'),
+ (True, 'expired', 'expired_token'),
+ (True, 'denied', 'access_denied'),
+ (True, 'pending', 'authorization_pending'),
+ ],
+ )
+ @patch('server.routes.oauth_device.device_code_store')
+ async def test_device_token_error_cases(
+ self, mock_store, device_exists, status, expected_error
+ ):
+ """Test various error cases for device token endpoint."""
+ device_code = 'test-device-code'
+
+ if device_exists:
+ mock_device = MagicMock()
+ mock_device.is_expired.return_value = status == 'expired'
+ mock_device.status = status
+ # Mock rate limiting - return False (not too fast) and default interval
+ mock_device.check_rate_limit.return_value = (False, 5)
+ mock_store.get_by_device_code.return_value = mock_device
+ mock_store.update_poll_time.return_value = True
+ else:
+ mock_store.get_by_device_code.return_value = None
+
+ result = await device_token(device_code=device_code)
+
+ assert isinstance(result, JSONResponse)
+ assert result.status_code == 400
+ # Check error in response content
+ content = result.body.decode()
+ assert expected_error in content
+
+ @patch('server.routes.oauth_device.ApiKeyStore')
+ @patch('server.routes.oauth_device.device_code_store')
+ async def test_device_token_success(self, mock_store, mock_api_key_class):
+ """Test successful device token retrieval."""
+ device_code = 'test-device-code'
+
+ # Mock authorized device
+ mock_device = MagicMock()
+ mock_device.is_expired.return_value = False
+ mock_device.status = 'authorized'
+ mock_device.keycloak_user_id = 'user-123'
+ mock_device.user_code = (
+ 'ABC12345' # Add user_code for device-specific API key lookup
+ )
+ # Mock rate limiting - return False (not too fast) and default interval
+ mock_device.check_rate_limit.return_value = (False, 5)
+ mock_store.get_by_device_code.return_value = mock_device
+ mock_store.update_poll_time.return_value = True
+
+ # Mock API key retrieval
+ mock_api_key_store = MagicMock()
+ mock_api_key_store.retrieve_api_key_by_name.return_value = 'test-api-key'
+ mock_api_key_class.get_instance.return_value = mock_api_key_store
+
+ result = await device_token(device_code=device_code)
+
+ # Check that result is a DeviceTokenResponse
+ assert result.access_token == 'test-api-key'
+ assert result.token_type == 'Bearer'
+
+ # Verify that the correct device-specific API key name was used
+ mock_api_key_store.retrieve_api_key_by_name.assert_called_once_with(
+ 'user-123', 'Device Link Access Key (ABC12345)'
+ )
+
+
+class TestDeviceVerificationAuthenticated:
+ """Test device verification authenticated endpoint."""
+
+ async def test_verification_unauthenticated_user(self):
+ """Test verification with unauthenticated user."""
+ with pytest.raises(HTTPException):
+ await device_verification_authenticated(user_code='ABC12345', user_id=None)
+
+ @patch('server.routes.oauth_device.ApiKeyStore')
+ @patch('server.routes.oauth_device.device_code_store')
+ async def test_verification_invalid_device_code(
+ self, mock_store, mock_api_key_class
+ ):
+ """Test verification with invalid device code."""
+ mock_store.get_by_user_code.return_value = None
+
+ with pytest.raises(HTTPException):
+ await device_verification_authenticated(
+ user_code='INVALID', user_id='user-123'
+ )
+
+ @patch('server.routes.oauth_device.ApiKeyStore')
+ @patch('server.routes.oauth_device.device_code_store')
+ async def test_verification_already_processed(self, mock_store, mock_api_key_class):
+ """Test verification with already processed device code."""
+ mock_device = MagicMock()
+ mock_device.is_pending.return_value = False
+ mock_store.get_by_user_code.return_value = mock_device
+
+ with pytest.raises(HTTPException):
+ await device_verification_authenticated(
+ user_code='ABC12345', user_id='user-123'
+ )
+
+ @patch('server.routes.oauth_device.ApiKeyStore')
+ @patch('server.routes.oauth_device.device_code_store')
+ async def test_verification_success(self, mock_store, mock_api_key_class):
+ """Test successful device verification."""
+ # Mock device code
+ mock_device = MagicMock()
+ mock_device.is_pending.return_value = True
+ mock_store.get_by_user_code.return_value = mock_device
+ mock_store.authorize_device_code.return_value = True
+
+ # Mock API key store
+ mock_api_key_store = MagicMock()
+ mock_api_key_class.get_instance.return_value = mock_api_key_store
+
+ result = await device_verification_authenticated(
+ user_code='ABC12345', user_id='user-123'
+ )
+
+ assert isinstance(result, JSONResponse)
+ assert result.status_code == 200
+ # Should NOT delete existing API keys (multiple devices allowed)
+ mock_api_key_store.delete_api_key_by_name.assert_not_called()
+ # Should create a new API key with device-specific name
+ mock_api_key_store.create_api_key.assert_called_once()
+ call_args = mock_api_key_store.create_api_key.call_args
+ assert call_args[1]['name'] == 'Device Link Access Key (ABC12345)'
+ mock_store.authorize_device_code.assert_called_once_with(
+ user_code='ABC12345', user_id='user-123'
+ )
+
+ @patch('server.routes.oauth_device.ApiKeyStore')
+ @patch('server.routes.oauth_device.device_code_store')
+ async def test_multiple_device_authentication(self, mock_store, mock_api_key_class):
+ """Test that multiple devices can authenticate simultaneously."""
+ # Mock API key store
+ mock_api_key_store = MagicMock()
+ mock_api_key_class.get_instance.return_value = mock_api_key_store
+
+ # Simulate two different devices
+ device1_code = 'ABC12345'
+ device2_code = 'XYZ67890'
+ user_id = 'user-123'
+
+ # Mock device codes
+ mock_device1 = MagicMock()
+ mock_device1.is_pending.return_value = True
+ mock_device2 = MagicMock()
+ mock_device2.is_pending.return_value = True
+
+ # Configure mock store to return appropriate device for each user_code
+ def get_by_user_code_side_effect(user_code):
+ if user_code == device1_code:
+ return mock_device1
+ elif user_code == device2_code:
+ return mock_device2
+ return None
+
+ mock_store.get_by_user_code.side_effect = get_by_user_code_side_effect
+ mock_store.authorize_device_code.return_value = True
+
+ # Authenticate first device
+ result1 = await device_verification_authenticated(
+ user_code=device1_code, user_id=user_id
+ )
+
+ # Authenticate second device
+ result2 = await device_verification_authenticated(
+ user_code=device2_code, user_id=user_id
+ )
+
+ # Both should succeed
+ assert isinstance(result1, JSONResponse)
+ assert result1.status_code == 200
+ assert isinstance(result2, JSONResponse)
+ assert result2.status_code == 200
+
+ # Should create two separate API keys with different names
+ assert mock_api_key_store.create_api_key.call_count == 2
+
+ # Check that each device got a unique API key name
+ call_args_list = mock_api_key_store.create_api_key.call_args_list
+ device1_name = call_args_list[0][1]['name']
+ device2_name = call_args_list[1][1]['name']
+
+ assert device1_name == f'Device Link Access Key ({device1_code})'
+ assert device2_name == f'Device Link Access Key ({device2_code})'
+ assert device1_name != device2_name # Ensure they're different
+
+ # Should NOT delete any existing API keys
+ mock_api_key_store.delete_api_key_by_name.assert_not_called()
+
+
+class TestDeviceTokenRateLimiting:
+ """Test rate limiting for device token polling (RFC 8628 section 3.5)."""
+
+ @patch('server.routes.oauth_device.device_code_store')
+ async def test_first_poll_allowed(self, mock_store):
+ """Test that the first poll is always allowed."""
+ # Create a device code with no previous poll time
+ mock_device = DeviceCode(
+ device_code='test_device_code',
+ user_code='ABC123',
+ status='pending',
+ expires_at=datetime.now(UTC) + timedelta(minutes=10),
+ last_poll_time=None, # First poll
+ current_interval=5,
+ )
+ mock_store.get_by_device_code.return_value = mock_device
+ mock_store.update_poll_time.return_value = True
+
+ device_code = 'test_device_code'
+ result = await device_token(device_code=device_code)
+
+ # Should return authorization_pending, not slow_down
+ assert isinstance(result, JSONResponse)
+ assert result.status_code == 400
+ content = result.body.decode()
+ assert 'authorization_pending' in content
+ assert 'slow_down' not in content
+
+ # Should update poll time without increasing interval
+ mock_store.update_poll_time.assert_called_with(
+ 'test_device_code', increase_interval=False
+ )
+
+ @patch('server.routes.oauth_device.device_code_store')
+ async def test_normal_polling_allowed(self, mock_store):
+ """Test that normal polling (respecting interval) is allowed."""
+ # Create a device code with last poll time 6 seconds ago (interval is 5)
+ last_poll = datetime.now(UTC) - timedelta(seconds=6)
+ mock_device = DeviceCode(
+ device_code='test_device_code',
+ user_code='ABC123',
+ status='pending',
+ expires_at=datetime.now(UTC) + timedelta(minutes=10),
+ last_poll_time=last_poll,
+ current_interval=5,
+ )
+ mock_store.get_by_device_code.return_value = mock_device
+ mock_store.update_poll_time.return_value = True
+
+ device_code = 'test_device_code'
+ result = await device_token(device_code=device_code)
+
+ # Should return authorization_pending, not slow_down
+ assert isinstance(result, JSONResponse)
+ assert result.status_code == 400
+ content = result.body.decode()
+ assert 'authorization_pending' in content
+ assert 'slow_down' not in content
+
+ # Should update poll time without increasing interval
+ mock_store.update_poll_time.assert_called_with(
+ 'test_device_code', increase_interval=False
+ )
+
+ @patch('server.routes.oauth_device.device_code_store')
+ async def test_fast_polling_returns_slow_down(self, mock_store):
+ """Test that polling too fast returns slow_down error."""
+ # Create a device code with last poll time 2 seconds ago (interval is 5)
+ last_poll = datetime.now(UTC) - timedelta(seconds=2)
+ mock_device = DeviceCode(
+ device_code='test_device_code',
+ user_code='ABC123',
+ status='pending',
+ expires_at=datetime.now(UTC) + timedelta(minutes=10),
+ last_poll_time=last_poll,
+ current_interval=5,
+ )
+ mock_store.get_by_device_code.return_value = mock_device
+ mock_store.update_poll_time.return_value = True
+
+ device_code = 'test_device_code'
+ result = await device_token(device_code=device_code)
+
+ # Should return slow_down error
+ assert isinstance(result, JSONResponse)
+ assert result.status_code == 400
+ content = result.body.decode()
+ assert 'slow_down' in content
+ assert 'interval' in content
+ assert '10' in content # New interval should be 5 + 5 = 10
+
+ # Should update poll time and increase interval
+ mock_store.update_poll_time.assert_called_with(
+ 'test_device_code', increase_interval=True
+ )
+
+ @patch('server.routes.oauth_device.device_code_store')
+ async def test_interval_increases_with_repeated_fast_polling(self, mock_store):
+ """Test that interval increases with repeated fast polling."""
+ # Create a device code with higher current interval from previous slow_down
+ last_poll = datetime.now(UTC) - timedelta(seconds=5) # 5 seconds ago
+ mock_device = DeviceCode(
+ device_code='test_device_code',
+ user_code='ABC123',
+ status='pending',
+ expires_at=datetime.now(UTC) + timedelta(minutes=10),
+ last_poll_time=last_poll,
+ current_interval=15, # Already increased from previous slow_down
+ )
+ mock_store.get_by_device_code.return_value = mock_device
+ mock_store.update_poll_time.return_value = True
+
+ device_code = 'test_device_code'
+ result = await device_token(device_code=device_code)
+
+ # Should return slow_down error with increased interval
+ assert isinstance(result, JSONResponse)
+ assert result.status_code == 400
+ content = result.body.decode()
+ assert 'slow_down' in content
+ assert '20' in content # New interval should be 15 + 5 = 20
+
+ # Should update poll time and increase interval
+ mock_store.update_poll_time.assert_called_with(
+ 'test_device_code', increase_interval=True
+ )
+
+ @patch('server.routes.oauth_device.device_code_store')
+ async def test_interval_caps_at_maximum(self, mock_store):
+ """Test that interval is capped at maximum value."""
+ # Create a device code with interval near maximum
+ last_poll = datetime.now(UTC) - timedelta(seconds=30)
+ mock_device = DeviceCode(
+ device_code='test_device_code',
+ user_code='ABC123',
+ status='pending',
+ expires_at=datetime.now(UTC) + timedelta(minutes=10),
+ last_poll_time=last_poll,
+ current_interval=58, # Near maximum of 60
+ )
+ mock_store.get_by_device_code.return_value = mock_device
+ mock_store.update_poll_time.return_value = True
+
+ device_code = 'test_device_code'
+ result = await device_token(device_code=device_code)
+
+ # Should return slow_down error with capped interval
+ assert isinstance(result, JSONResponse)
+ assert result.status_code == 400
+ content = result.body.decode()
+ assert 'slow_down' in content
+ assert '60' in content # Should be capped at 60, not 63
+
+ @patch('server.routes.oauth_device.device_code_store')
+ async def test_rate_limiting_with_authorized_device(self, mock_store):
+ """Test that rate limiting still applies to authorized devices."""
+ # Create an authorized device code with recent poll
+ last_poll = datetime.now(UTC) - timedelta(seconds=2)
+ mock_device = DeviceCode(
+ device_code='test_device_code',
+ user_code='ABC123',
+ status='authorized', # Device is authorized
+ keycloak_user_id='user123',
+ expires_at=datetime.now(UTC) + timedelta(minutes=10),
+ last_poll_time=last_poll,
+ current_interval=5,
+ )
+ mock_store.get_by_device_code.return_value = mock_device
+ mock_store.update_poll_time.return_value = True
+
+ device_code = 'test_device_code'
+ result = await device_token(device_code=device_code)
+
+ # Should still return slow_down error even for authorized device
+ assert isinstance(result, JSONResponse)
+ assert result.status_code == 400
+ content = result.body.decode()
+ assert 'slow_down' in content
+
+ # Should update poll time and increase interval
+ mock_store.update_poll_time.assert_called_with(
+ 'test_device_code', increase_interval=True
+ )
+
+
+class TestDeviceVerificationTransactionIntegrity:
+ """Test transaction integrity for device verification to prevent orphaned API keys."""
+
+ @patch('server.routes.oauth_device.ApiKeyStore')
+ @patch('server.routes.oauth_device.device_code_store')
+ async def test_authorization_failure_prevents_api_key_creation(
+ self, mock_store, mock_api_key_class
+ ):
+ """Test that if device authorization fails, no API key is created."""
+ # Mock device code
+ mock_device = MagicMock()
+ mock_device.is_pending.return_value = True
+ mock_store.get_by_user_code.return_value = mock_device
+ mock_store.authorize_device_code.return_value = False # Authorization fails
+
+ # Mock API key store
+ mock_api_key_store = MagicMock()
+ mock_api_key_class.get_instance.return_value = mock_api_key_store
+
+ # Should raise HTTPException due to authorization failure
+ with pytest.raises(HTTPException) as exc_info:
+ await device_verification_authenticated(
+ user_code='ABC12345', user_id='user-123'
+ )
+
+ assert exc_info.value.status_code == 500
+ assert 'Failed to authorize the device' in exc_info.value.detail
+
+ # API key should NOT be created since authorization failed
+ mock_api_key_store.create_api_key.assert_not_called()
+ mock_store.authorize_device_code.assert_called_once_with(
+ user_code='ABC12345', user_id='user-123'
+ )
+
+ @patch('server.routes.oauth_device.ApiKeyStore')
+ @patch('server.routes.oauth_device.device_code_store')
+ async def test_api_key_creation_failure_reverts_authorization(
+ self, mock_store, mock_api_key_class
+ ):
+ """Test that if API key creation fails after authorization, the authorization is reverted."""
+ # Mock device code
+ mock_device = MagicMock()
+ mock_device.is_pending.return_value = True
+ mock_store.get_by_user_code.return_value = mock_device
+ mock_store.authorize_device_code.return_value = True # Authorization succeeds
+ mock_store.deny_device_code.return_value = True # Cleanup succeeds
+
+ # Mock API key store to fail on creation
+ mock_api_key_store = MagicMock()
+ mock_api_key_store.create_api_key.side_effect = Exception('Database error')
+ mock_api_key_class.get_instance.return_value = mock_api_key_store
+
+ # Should raise HTTPException due to API key creation failure
+ with pytest.raises(HTTPException) as exc_info:
+ await device_verification_authenticated(
+ user_code='ABC12345', user_id='user-123'
+ )
+
+ assert exc_info.value.status_code == 500
+ assert 'Failed to create API key for device access' in exc_info.value.detail
+
+ # Authorization should have been attempted first
+ mock_store.authorize_device_code.assert_called_once_with(
+ user_code='ABC12345', user_id='user-123'
+ )
+
+ # API key creation should have been attempted after authorization
+ mock_api_key_store.create_api_key.assert_called_once()
+
+ # Authorization should be reverted due to API key creation failure
+ mock_store.deny_device_code.assert_called_once_with('ABC12345')
+
+ @patch('server.routes.oauth_device.ApiKeyStore')
+ @patch('server.routes.oauth_device.device_code_store')
+ async def test_api_key_creation_failure_cleanup_failure_logged(
+ self, mock_store, mock_api_key_class
+ ):
+ """Test that cleanup failure is logged but doesn't prevent the main error from being raised."""
+ # Mock device code
+ mock_device = MagicMock()
+ mock_device.is_pending.return_value = True
+ mock_store.get_by_user_code.return_value = mock_device
+ mock_store.authorize_device_code.return_value = True # Authorization succeeds
+ mock_store.deny_device_code.side_effect = Exception(
+ 'Cleanup failed'
+ ) # Cleanup fails
+
+ # Mock API key store to fail on creation
+ mock_api_key_store = MagicMock()
+ mock_api_key_store.create_api_key.side_effect = Exception('Database error')
+ mock_api_key_class.get_instance.return_value = mock_api_key_store
+
+ # Should still raise HTTPException for the original API key creation failure
+ with pytest.raises(HTTPException) as exc_info:
+ await device_verification_authenticated(
+ user_code='ABC12345', user_id='user-123'
+ )
+
+ assert exc_info.value.status_code == 500
+ assert 'Failed to create API key for device access' in exc_info.value.detail
+
+ # Both operations should have been attempted
+ mock_store.authorize_device_code.assert_called_once()
+ mock_api_key_store.create_api_key.assert_called_once()
+ mock_store.deny_device_code.assert_called_once_with('ABC12345')
+
+ @patch('server.routes.oauth_device.ApiKeyStore')
+ @patch('server.routes.oauth_device.device_code_store')
+ async def test_successful_flow_creates_api_key_after_authorization(
+ self, mock_store, mock_api_key_class
+ ):
+ """Test that in the successful flow, API key is created only after authorization."""
+ # Mock device code
+ mock_device = MagicMock()
+ mock_device.is_pending.return_value = True
+ mock_store.get_by_user_code.return_value = mock_device
+ mock_store.authorize_device_code.return_value = True # Authorization succeeds
+
+ # Mock API key store
+ mock_api_key_store = MagicMock()
+ mock_api_key_class.get_instance.return_value = mock_api_key_store
+
+ result = await device_verification_authenticated(
+ user_code='ABC12345', user_id='user-123'
+ )
+
+ assert isinstance(result, JSONResponse)
+ assert result.status_code == 200
+
+ # Verify the order: authorization first, then API key creation
+ mock_store.authorize_device_code.assert_called_once_with(
+ user_code='ABC12345', user_id='user-123'
+ )
+ mock_api_key_store.create_api_key.assert_called_once()
+
+ # No cleanup should be needed in successful case
+ mock_store.deny_device_code.assert_not_called()
diff --git a/enterprise/tests/unit/storage/test_device_code.py b/enterprise/tests/unit/storage/test_device_code.py
new file mode 100644
index 0000000000..0d2193075b
--- /dev/null
+++ b/enterprise/tests/unit/storage/test_device_code.py
@@ -0,0 +1,83 @@
+"""Unit tests for DeviceCode model."""
+
+from datetime import datetime, timedelta, timezone
+
+import pytest
+from storage.device_code import DeviceCode, DeviceCodeStatus
+
+
+class TestDeviceCode:
+ """Test cases for DeviceCode model."""
+
+ @pytest.fixture
+ def device_code(self):
+ """Create a test device code."""
+ return DeviceCode(
+ device_code='test-device-code-123',
+ user_code='ABC12345',
+ expires_at=datetime.now(timezone.utc) + timedelta(minutes=10),
+ )
+
+ @pytest.mark.parametrize(
+ 'expires_delta,expected',
+ [
+ (timedelta(minutes=5), False), # Future expiry
+ (timedelta(minutes=-5), True), # Past expiry
+ (timedelta(seconds=1), False), # Just future (not expired)
+ ],
+ )
+ def test_is_expired(self, expires_delta, expected):
+ """Test expiration check with various time deltas."""
+ device_code = DeviceCode(
+ device_code='test-device-code',
+ user_code='ABC12345',
+ expires_at=datetime.now(timezone.utc) + expires_delta,
+ )
+ assert device_code.is_expired() == expected
+
+ @pytest.mark.parametrize(
+ 'status,expired,expected',
+ [
+ (DeviceCodeStatus.PENDING.value, False, True),
+ (DeviceCodeStatus.PENDING.value, True, False),
+ (DeviceCodeStatus.AUTHORIZED.value, False, False),
+ (DeviceCodeStatus.DENIED.value, False, False),
+ ],
+ )
+ def test_is_pending(self, status, expired, expected):
+ """Test pending status check."""
+ expires_at = (
+ datetime.now(timezone.utc) - timedelta(minutes=1)
+ if expired
+ else datetime.now(timezone.utc) + timedelta(minutes=10)
+ )
+ device_code = DeviceCode(
+ device_code='test-device-code',
+ user_code='ABC12345',
+ status=status,
+ expires_at=expires_at,
+ )
+ assert device_code.is_pending() == expected
+
+ def test_authorize(self, device_code):
+ """Test device authorization."""
+ user_id = 'test-user-123'
+
+ device_code.authorize(user_id)
+
+ assert device_code.status == DeviceCodeStatus.AUTHORIZED.value
+ assert device_code.keycloak_user_id == user_id
+ assert device_code.authorized_at is not None
+ assert isinstance(device_code.authorized_at, datetime)
+
+ @pytest.mark.parametrize(
+ 'method,expected_status',
+ [
+ ('deny', DeviceCodeStatus.DENIED.value),
+ ('expire', DeviceCodeStatus.EXPIRED.value),
+ ],
+ )
+ def test_status_changes(self, device_code, method, expected_status):
+ """Test status change methods."""
+ getattr(device_code, method)()
+ assert device_code.status == expected_status
diff --git a/enterprise/tests/unit/storage/test_device_code_store.py b/enterprise/tests/unit/storage/test_device_code_store.py
new file mode 100644
index 0000000000..65a58cda8a
--- /dev/null
+++ b/enterprise/tests/unit/storage/test_device_code_store.py
@@ -0,0 +1,193 @@
+"""Unit tests for DeviceCodeStore."""
+
+from unittest.mock import MagicMock
+
+import pytest
+from sqlalchemy.exc import IntegrityError
+from storage.device_code import DeviceCode
+from storage.device_code_store import DeviceCodeStore
+
+
+@pytest.fixture
+def mock_session():
+ """Mock database session."""
+ session = MagicMock()
+ return session
+
+
+@pytest.fixture
+def mock_session_maker(mock_session):
+ """Mock session maker."""
+ session_maker = MagicMock()
+ session_maker.return_value.__enter__.return_value = mock_session
+ session_maker.return_value.__exit__.return_value = None
+ return session_maker
+
+
+@pytest.fixture
+def device_code_store(mock_session_maker):
+ """Create DeviceCodeStore instance."""
+ return DeviceCodeStore(mock_session_maker)
+
+
+class TestDeviceCodeStore:
+ """Test cases for DeviceCodeStore."""
+
+ def test_generate_user_code(self, device_code_store):
+ """Test user code generation."""
+ code = device_code_store.generate_user_code()
+
+ assert len(code) == 8
+ assert code.isupper()
+ # Should not contain confusing characters
+ assert not any(char in code for char in 'IO01')
+
+ def test_generate_device_code(self, device_code_store):
+ """Test device code generation."""
+ code = device_code_store.generate_device_code()
+
+ assert len(code) == 128
+ assert code.isalnum()
+
+ def test_create_device_code_success(self, device_code_store, mock_session):
+ """Test successful device code creation."""
+ # Mock successful creation (no IntegrityError)
+ mock_device_code = MagicMock(spec=DeviceCode)
+ mock_device_code.device_code = 'test-device-code-123'
+ mock_device_code.user_code = 'TESTCODE'
+
+ # Mock the session to return our mock device code after refresh
+ def mock_refresh(obj):
+ obj.device_code = mock_device_code.device_code
+ obj.user_code = mock_device_code.user_code
+
+ mock_session.refresh.side_effect = mock_refresh
+
+ result = device_code_store.create_device_code(expires_in=600)
+
+ assert isinstance(result, DeviceCode)
+ mock_session.add.assert_called_once()
+ mock_session.commit.assert_called_once()
+ mock_session.refresh.assert_called_once()
+ mock_session.expunge.assert_called_once()
+
+ def test_create_device_code_with_retries(
+ self, device_code_store, mock_session_maker
+ ):
+ """Test device code creation with constraint violation retries."""
+ mock_session = MagicMock()
+ mock_session_maker.return_value.__enter__.return_value = mock_session
+ mock_session_maker.return_value.__exit__.return_value = None
+
+ # First attempt fails with IntegrityError, second succeeds
+ mock_session.commit.side_effect = [IntegrityError('', '', ''), None]
+
+ mock_device_code = MagicMock(spec=DeviceCode)
+ mock_device_code.device_code = 'test-device-code-456'
+ mock_device_code.user_code = 'TESTCD2'
+
+ def mock_refresh(obj):
+ obj.device_code = mock_device_code.device_code
+ obj.user_code = mock_device_code.user_code
+
+ mock_session.refresh.side_effect = mock_refresh
+
+ store = DeviceCodeStore(mock_session_maker)
+ result = store.create_device_code(expires_in=600)
+
+ assert isinstance(result, DeviceCode)
+ assert mock_session.add.call_count == 2 # Two attempts
+ assert mock_session.commit.call_count == 2 # Two attempts
+
+ def test_create_device_code_max_attempts_exceeded(
+ self, device_code_store, mock_session_maker
+ ):
+ """Test device code creation failure after max attempts."""
+ mock_session = MagicMock()
+ mock_session_maker.return_value.__enter__.return_value = mock_session
+ mock_session_maker.return_value.__exit__.return_value = None
+
+ # All attempts fail with IntegrityError
+ mock_session.commit.side_effect = IntegrityError('', '', '')
+
+ store = DeviceCodeStore(mock_session_maker)
+
+ with pytest.raises(
+ RuntimeError,
+ match='Failed to generate unique device codes after 3 attempts',
+ ):
+ store.create_device_code(expires_in=600, max_attempts=3)
+
+ @pytest.mark.parametrize(
+ 'lookup_method,lookup_field',
+ [
+ ('get_by_device_code', 'device_code'),
+ ('get_by_user_code', 'user_code'),
+ ],
+ )
+ def test_lookup_methods(
+ self, device_code_store, mock_session, lookup_method, lookup_field
+ ):
+ """Test device code lookup methods."""
+ test_code = 'test-code-123'
+ mock_device_code = MagicMock()
+ mock_session.query.return_value.filter_by.return_value.first.return_value = (
+ mock_device_code
+ )
+
+ result = getattr(device_code_store, lookup_method)(test_code)
+
+ assert result == mock_device_code
+ mock_session.query.assert_called_once_with(DeviceCode)
+ mock_session.query.return_value.filter_by.assert_called_once_with(
+ **{lookup_field: test_code}
+ )
+
+ @pytest.mark.parametrize(
+ 'device_exists,is_pending,expected_result',
+ [
+ (True, True, True), # Success case
+ (False, True, False), # Device not found
+ (True, False, False), # Device not pending
+ ],
+ )
+ def test_authorize_device_code(
+ self,
+ device_code_store,
+ mock_session,
+ device_exists,
+ is_pending,
+ expected_result,
+ ):
+ """Test device code authorization."""
+ user_code = 'ABC12345'
+ user_id = 'test-user-123'
+
+ if device_exists:
+ mock_device = MagicMock()
+ mock_device.is_pending.return_value = is_pending
+ mock_session.query.return_value.filter_by.return_value.first.return_value = mock_device
+ else:
+ mock_session.query.return_value.filter_by.return_value.first.return_value = None
+
+ result = device_code_store.authorize_device_code(user_code, user_id)
+
+ assert result == expected_result
+ if expected_result:
+ mock_device.authorize.assert_called_once_with(user_id)
+ mock_session.commit.assert_called_once()
+
+ def test_deny_device_code(self, device_code_store, mock_session):
+ """Test device code denial."""
+ user_code = 'ABC12345'
+ mock_device = MagicMock()
+ mock_device.is_pending.return_value = True
+ mock_session.query.return_value.filter_by.return_value.first.return_value = (
+ mock_device
+ )
+
+ result = device_code_store.deny_device_code(user_code)
+
+ assert result is True
+ mock_device.deny.assert_called_once()
+ mock_session.commit.assert_called_once()
diff --git a/enterprise/tests/unit/test_api_key_store.py b/enterprise/tests/unit/test_api_key_store.py
index ea386cb69c..c1c6a98f3d 100644
--- a/enterprise/tests/unit/test_api_key_store.py
+++ b/enterprise/tests/unit/test_api_key_store.py
@@ -90,6 +90,50 @@ def test_validate_api_key_expired(api_key_store, mock_session):
mock_session.commit.assert_not_called()
+def test_validate_api_key_expired_timezone_naive(api_key_store, mock_session):
+ """Test validating an expired API key with timezone-naive datetime from database."""
+ # Setup
+ api_key = 'test-api-key'
+ mock_key_record = MagicMock()
+ # Simulate timezone-naive datetime as returned from database
+ mock_key_record.expires_at = datetime.now() - timedelta(days=1) # No UTC timezone
+ mock_key_record.id = 1
+ mock_session.query.return_value.filter.return_value.first.return_value = (
+ mock_key_record
+ )
+
+ # Execute
+ result = api_key_store.validate_api_key(api_key)
+
+ # Verify
+ assert result is None
+ mock_session.execute.assert_not_called()
+ mock_session.commit.assert_not_called()
+
+
+def test_validate_api_key_valid_timezone_naive(api_key_store, mock_session):
+ """Test validating a valid API key with timezone-naive datetime from database."""
+ # Setup
+ api_key = 'test-api-key'
+ user_id = 'test-user-123'
+ mock_key_record = MagicMock()
+ mock_key_record.user_id = user_id
+ # Simulate timezone-naive datetime as returned from database (future date)
+ mock_key_record.expires_at = datetime.now() + timedelta(days=1) # No UTC timezone
+ mock_key_record.id = 1
+ mock_session.query.return_value.filter.return_value.first.return_value = (
+ mock_key_record
+ )
+
+ # Execute
+ result = api_key_store.validate_api_key(api_key)
+
+ # Verify
+ assert result == user_id
+ mock_session.execute.assert_called_once()
+ mock_session.commit.assert_called_once()
+
+
def test_validate_api_key_not_found(api_key_store, mock_session):
"""Test validating a non-existent API key."""
# Setup
diff --git a/enterprise/tests/unit/test_legacy_conversation_manager.py b/enterprise/tests/unit/test_legacy_conversation_manager.py
deleted file mode 100644
index 55b424dabc..0000000000
--- a/enterprise/tests/unit/test_legacy_conversation_manager.py
+++ /dev/null
@@ -1,485 +0,0 @@
-import time
-from unittest.mock import AsyncMock, MagicMock, patch
-
-import pytest
-from server.legacy_conversation_manager import (
- _LEGACY_ENTRY_TIMEOUT_SECONDS,
- LegacyCacheEntry,
- LegacyConversationManager,
-)
-
-from openhands.core.config.openhands_config import OpenHandsConfig
-from openhands.server.config.server_config import ServerConfig
-from openhands.server.monitoring import MonitoringListener
-from openhands.storage.memory import InMemoryFileStore
-
-
-@pytest.fixture
-def mock_sio():
- """Create a mock SocketIO server."""
- return MagicMock()
-
-
-@pytest.fixture
-def mock_config():
- """Create a mock OpenHands config."""
- return MagicMock(spec=OpenHandsConfig)
-
-
-@pytest.fixture
-def mock_server_config():
- """Create a mock server config."""
- return MagicMock(spec=ServerConfig)
-
-
-@pytest.fixture
-def mock_file_store():
- """Create a mock file store."""
- return MagicMock(spec=InMemoryFileStore)
-
-
-@pytest.fixture
-def mock_monitoring_listener():
- """Create a mock monitoring listener."""
- return MagicMock(spec=MonitoringListener)
-
-
-@pytest.fixture
-def mock_conversation_manager():
- """Create a mock SaasNestedConversationManager."""
- mock_cm = MagicMock()
- mock_cm._get_runtime = AsyncMock()
- return mock_cm
-
-
-@pytest.fixture
-def mock_legacy_conversation_manager():
- """Create a mock ClusteredConversationManager."""
- return MagicMock()
-
-
-@pytest.fixture
-def legacy_manager(
- mock_sio,
- mock_config,
- mock_server_config,
- mock_file_store,
- mock_conversation_manager,
- mock_legacy_conversation_manager,
-):
- """Create a LegacyConversationManager instance for testing."""
- return LegacyConversationManager(
- sio=mock_sio,
- config=mock_config,
- server_config=mock_server_config,
- file_store=mock_file_store,
- conversation_manager=mock_conversation_manager,
- legacy_conversation_manager=mock_legacy_conversation_manager,
- )
-
-
-class TestLegacyCacheEntry:
- """Test the LegacyCacheEntry dataclass."""
-
- def test_cache_entry_creation(self):
- """Test creating a cache entry."""
- timestamp = time.time()
- entry = LegacyCacheEntry(is_legacy=True, timestamp=timestamp)
-
- assert entry.is_legacy is True
- assert entry.timestamp == timestamp
-
- def test_cache_entry_false(self):
- """Test creating a cache entry with False value."""
- timestamp = time.time()
- entry = LegacyCacheEntry(is_legacy=False, timestamp=timestamp)
-
- assert entry.is_legacy is False
- assert entry.timestamp == timestamp
-
-
-class TestLegacyConversationManagerCacheCleanup:
- """Test cache cleanup functionality."""
-
- def test_cleanup_expired_cache_entries_removes_expired(self, legacy_manager):
- """Test that expired entries are removed from cache."""
- current_time = time.time()
- expired_time = current_time - _LEGACY_ENTRY_TIMEOUT_SECONDS - 1
- valid_time = current_time - 100 # Well within timeout
-
- # Add both expired and valid entries
- legacy_manager._legacy_cache = {
- 'expired_conversation': LegacyCacheEntry(True, expired_time),
- 'valid_conversation': LegacyCacheEntry(False, valid_time),
- 'another_expired': LegacyCacheEntry(True, expired_time - 100),
- }
-
- legacy_manager._cleanup_expired_cache_entries()
-
- # Only valid entry should remain
- assert len(legacy_manager._legacy_cache) == 1
- assert 'valid_conversation' in legacy_manager._legacy_cache
- assert 'expired_conversation' not in legacy_manager._legacy_cache
- assert 'another_expired' not in legacy_manager._legacy_cache
-
- def test_cleanup_expired_cache_entries_keeps_valid(self, legacy_manager):
- """Test that valid entries are kept during cleanup."""
- current_time = time.time()
- valid_time = current_time - 100 # Well within timeout
-
- legacy_manager._legacy_cache = {
- 'valid_conversation_1': LegacyCacheEntry(True, valid_time),
- 'valid_conversation_2': LegacyCacheEntry(False, valid_time - 50),
- }
-
- legacy_manager._cleanup_expired_cache_entries()
-
- # Both entries should remain
- assert len(legacy_manager._legacy_cache) == 2
- assert 'valid_conversation_1' in legacy_manager._legacy_cache
- assert 'valid_conversation_2' in legacy_manager._legacy_cache
-
- def test_cleanup_expired_cache_entries_empty_cache(self, legacy_manager):
- """Test cleanup with empty cache."""
- legacy_manager._legacy_cache = {}
-
- legacy_manager._cleanup_expired_cache_entries()
-
- assert len(legacy_manager._legacy_cache) == 0
-
-
-class TestIsLegacyRuntime:
- """Test the is_legacy_runtime method."""
-
- def test_is_legacy_runtime_none(self, legacy_manager):
- """Test with None runtime."""
- result = legacy_manager.is_legacy_runtime(None)
- assert result is False
-
- def test_is_legacy_runtime_legacy_command(self, legacy_manager):
- """Test with legacy runtime command."""
- runtime = {'command': 'some_old_legacy_command'}
- result = legacy_manager.is_legacy_runtime(runtime)
- assert result is True
-
- def test_is_legacy_runtime_new_command(self, legacy_manager):
- """Test with new runtime command containing openhands.server."""
- runtime = {'command': 'python -m openhands.server.listen'}
- result = legacy_manager.is_legacy_runtime(runtime)
- assert result is False
-
- def test_is_legacy_runtime_partial_match(self, legacy_manager):
- """Test with command that partially matches but is still legacy."""
- runtime = {'command': 'openhands.client.start'}
- result = legacy_manager.is_legacy_runtime(runtime)
- assert result is True
-
- def test_is_legacy_runtime_empty_command(self, legacy_manager):
- """Test with empty command."""
- runtime = {'command': ''}
- result = legacy_manager.is_legacy_runtime(runtime)
- assert result is True
-
- def test_is_legacy_runtime_missing_command_key(self, legacy_manager):
- """Test with runtime missing command key."""
- runtime = {'other_key': 'value'}
- # This should raise a KeyError
- with pytest.raises(KeyError):
- legacy_manager.is_legacy_runtime(runtime)
-
-
-class TestShouldStartInLegacyMode:
- """Test the should_start_in_legacy_mode method."""
-
- @pytest.mark.asyncio
- async def test_cache_hit_valid_entry_legacy(self, legacy_manager):
- """Test cache hit with valid legacy entry."""
- conversation_id = 'test_conversation'
- current_time = time.time()
-
- # Add valid cache entry
- legacy_manager._legacy_cache[conversation_id] = LegacyCacheEntry(
- True, current_time - 100
- )
-
- result = await legacy_manager.should_start_in_legacy_mode(conversation_id)
-
- assert result is True
- # Should not call _get_runtime since we hit cache
- legacy_manager.conversation_manager._get_runtime.assert_not_called()
-
- @pytest.mark.asyncio
- async def test_cache_hit_valid_entry_non_legacy(self, legacy_manager):
- """Test cache hit with valid non-legacy entry."""
- conversation_id = 'test_conversation'
- current_time = time.time()
-
- # Add valid cache entry
- legacy_manager._legacy_cache[conversation_id] = LegacyCacheEntry(
- False, current_time - 100
- )
-
- result = await legacy_manager.should_start_in_legacy_mode(conversation_id)
-
- assert result is False
- # Should not call _get_runtime since we hit cache
- legacy_manager.conversation_manager._get_runtime.assert_not_called()
-
- @pytest.mark.asyncio
- async def test_cache_miss_legacy_runtime(self, legacy_manager):
- """Test cache miss with legacy runtime."""
- conversation_id = 'test_conversation'
- runtime = {'command': 'old_command'}
-
- legacy_manager.conversation_manager._get_runtime.return_value = runtime
-
- result = await legacy_manager.should_start_in_legacy_mode(conversation_id)
-
- assert result is True
- # Should call _get_runtime
- legacy_manager.conversation_manager._get_runtime.assert_called_once_with(
- conversation_id
- )
- # Should cache the result
- assert conversation_id in legacy_manager._legacy_cache
- assert legacy_manager._legacy_cache[conversation_id].is_legacy is True
-
- @pytest.mark.asyncio
- async def test_cache_miss_non_legacy_runtime(self, legacy_manager):
- """Test cache miss with non-legacy runtime."""
- conversation_id = 'test_conversation'
- runtime = {'command': 'python -m openhands.server.listen'}
-
- legacy_manager.conversation_manager._get_runtime.return_value = runtime
-
- result = await legacy_manager.should_start_in_legacy_mode(conversation_id)
-
- assert result is False
- # Should call _get_runtime
- legacy_manager.conversation_manager._get_runtime.assert_called_once_with(
- conversation_id
- )
- # Should cache the result
- assert conversation_id in legacy_manager._legacy_cache
- assert legacy_manager._legacy_cache[conversation_id].is_legacy is False
-
- @pytest.mark.asyncio
- async def test_cache_expired_entry(self, legacy_manager):
- """Test with expired cache entry."""
- conversation_id = 'test_conversation'
- expired_time = time.time() - _LEGACY_ENTRY_TIMEOUT_SECONDS - 1
- runtime = {'command': 'python -m openhands.server.listen'}
-
- # Add expired cache entry
- legacy_manager._legacy_cache[conversation_id] = LegacyCacheEntry(
- True,
- expired_time, # This should be considered expired
- )
-
- legacy_manager.conversation_manager._get_runtime.return_value = runtime
-
- result = await legacy_manager.should_start_in_legacy_mode(conversation_id)
-
- assert result is False # Runtime indicates non-legacy
- # Should call _get_runtime since cache is expired
- legacy_manager.conversation_manager._get_runtime.assert_called_once_with(
- conversation_id
- )
- # Should update cache with new result
- assert legacy_manager._legacy_cache[conversation_id].is_legacy is False
-
- @pytest.mark.asyncio
- async def test_cache_exactly_at_timeout(self, legacy_manager):
- """Test with cache entry exactly at timeout boundary."""
- conversation_id = 'test_conversation'
- timeout_time = time.time() - _LEGACY_ENTRY_TIMEOUT_SECONDS
- runtime = {'command': 'python -m openhands.server.listen'}
-
- # Add cache entry exactly at timeout
- legacy_manager._legacy_cache[conversation_id] = LegacyCacheEntry(
- True, timeout_time
- )
-
- legacy_manager.conversation_manager._get_runtime.return_value = runtime
-
- result = await legacy_manager.should_start_in_legacy_mode(conversation_id)
-
- # Should treat as expired and fetch from runtime
- assert result is False
- legacy_manager.conversation_manager._get_runtime.assert_called_once_with(
- conversation_id
- )
-
- @pytest.mark.asyncio
- async def test_runtime_returns_none(self, legacy_manager):
- """Test when runtime returns None."""
- conversation_id = 'test_conversation'
-
- legacy_manager.conversation_manager._get_runtime.return_value = None
-
- result = await legacy_manager.should_start_in_legacy_mode(conversation_id)
-
- assert result is False
- # Should cache the result
- assert conversation_id in legacy_manager._legacy_cache
- assert legacy_manager._legacy_cache[conversation_id].is_legacy is False
-
- @pytest.mark.asyncio
- async def test_cleanup_called_on_each_invocation(self, legacy_manager):
- """Test that cleanup is called on each invocation."""
- conversation_id = 'test_conversation'
- runtime = {'command': 'test'}
-
- legacy_manager.conversation_manager._get_runtime.return_value = runtime
-
- # Mock the cleanup method to verify it's called
- with patch.object(
- legacy_manager, '_cleanup_expired_cache_entries'
- ) as mock_cleanup:
- await legacy_manager.should_start_in_legacy_mode(conversation_id)
- mock_cleanup.assert_called_once()
-
- @pytest.mark.asyncio
- async def test_multiple_conversations_cached_independently(self, legacy_manager):
- """Test that multiple conversations are cached independently."""
- conv1 = 'conversation_1'
- conv2 = 'conversation_2'
-
- runtime1 = {'command': 'old_command'} # Legacy
- runtime2 = {'command': 'python -m openhands.server.listen'} # Non-legacy
-
- # Mock to return different runtimes based on conversation_id
- def mock_get_runtime(conversation_id):
- if conversation_id == conv1:
- return runtime1
- return runtime2
-
- legacy_manager.conversation_manager._get_runtime.side_effect = mock_get_runtime
-
- result1 = await legacy_manager.should_start_in_legacy_mode(conv1)
- result2 = await legacy_manager.should_start_in_legacy_mode(conv2)
-
- assert result1 is True
- assert result2 is False
-
- # Both should be cached
- assert conv1 in legacy_manager._legacy_cache
- assert conv2 in legacy_manager._legacy_cache
- assert legacy_manager._legacy_cache[conv1].is_legacy is True
- assert legacy_manager._legacy_cache[conv2].is_legacy is False
-
- @pytest.mark.asyncio
- async def test_cache_timestamp_updated_on_refresh(self, legacy_manager):
- """Test that cache timestamp is updated when entry is refreshed."""
- conversation_id = 'test_conversation'
- old_time = time.time() - _LEGACY_ENTRY_TIMEOUT_SECONDS - 1
- runtime = {'command': 'test'}
-
- # Add expired entry
- legacy_manager._legacy_cache[conversation_id] = LegacyCacheEntry(True, old_time)
- legacy_manager.conversation_manager._get_runtime.return_value = runtime
-
- # Record time before call
- before_call = time.time()
- await legacy_manager.should_start_in_legacy_mode(conversation_id)
- after_call = time.time()
-
- # Timestamp should be updated
- cached_entry = legacy_manager._legacy_cache[conversation_id]
- assert cached_entry.timestamp >= before_call
- assert cached_entry.timestamp <= after_call
-
-
-class TestLegacyConversationManagerIntegration:
- """Integration tests for LegacyConversationManager."""
-
- @pytest.mark.asyncio
- async def test_get_instance_creates_proper_manager(
- self,
- mock_sio,
- mock_config,
- mock_file_store,
- mock_server_config,
- mock_monitoring_listener,
- ):
- """Test that get_instance creates a properly configured manager."""
- with patch(
- 'server.legacy_conversation_manager.SaasNestedConversationManager'
- ) as mock_saas, patch(
- 'server.legacy_conversation_manager.ClusteredConversationManager'
- ) as mock_clustered:
- mock_saas.get_instance.return_value = MagicMock()
- mock_clustered.get_instance.return_value = MagicMock()
-
- manager = LegacyConversationManager.get_instance(
- mock_sio,
- mock_config,
- mock_file_store,
- mock_server_config,
- mock_monitoring_listener,
- )
-
- assert isinstance(manager, LegacyConversationManager)
- assert manager.sio == mock_sio
- assert manager.config == mock_config
- assert manager.file_store == mock_file_store
- assert manager.server_config == mock_server_config
-
- # Verify that both nested managers are created
- mock_saas.get_instance.assert_called_once()
- mock_clustered.get_instance.assert_called_once()
-
- def test_legacy_cache_initialized_empty(self, legacy_manager):
- """Test that legacy cache is initialized as empty dict."""
- assert isinstance(legacy_manager._legacy_cache, dict)
- assert len(legacy_manager._legacy_cache) == 0
-
-
-class TestEdgeCases:
- """Test edge cases and error scenarios."""
-
- @pytest.mark.asyncio
- async def test_get_runtime_raises_exception(self, legacy_manager):
- """Test behavior when _get_runtime raises an exception."""
- conversation_id = 'test_conversation'
-
- legacy_manager.conversation_manager._get_runtime.side_effect = Exception(
- 'Runtime error'
- )
-
- # Should propagate the exception
- with pytest.raises(Exception, match='Runtime error'):
- await legacy_manager.should_start_in_legacy_mode(conversation_id)
-
- @pytest.mark.asyncio
- async def test_very_large_cache(self, legacy_manager):
- """Test behavior with a large number of cache entries."""
- current_time = time.time()
-
- # Add many cache entries
- for i in range(1000):
- legacy_manager._legacy_cache[f'conversation_{i}'] = LegacyCacheEntry(
- i % 2 == 0, current_time - i
- )
-
- # This should work without issues
- await legacy_manager.should_start_in_legacy_mode('new_conversation')
-
- # Should have added one more entry
- assert len(legacy_manager._legacy_cache) == 1001
-
- def test_cleanup_with_concurrent_modifications(self, legacy_manager):
- """Test cleanup behavior when cache is modified during cleanup."""
- current_time = time.time()
- expired_time = current_time - _LEGACY_ENTRY_TIMEOUT_SECONDS - 1
-
- # Add expired entries
- legacy_manager._legacy_cache = {
- f'conversation_{i}': LegacyCacheEntry(True, expired_time) for i in range(10)
- }
-
- # This should work without raising exceptions
- legacy_manager._cleanup_expired_cache_entries()
-
- # All entries should be removed
- assert len(legacy_manager._legacy_cache) == 0
diff --git a/evaluation/README.md b/evaluation/README.md
index 694623f63d..b4a125b3fc 100644
--- a/evaluation/README.md
+++ b/evaluation/README.md
@@ -1,5 +1,10 @@
# Evaluation
+> [!WARNING]
+> **This directory is deprecated.** Our new benchmarks are located at [OpenHands/benchmarks](https://github.com/OpenHands/benchmarks).
+>
+> If you have already implemented a benchmark in this directory and would like to contribute it, we are happy to have the contribution. However, if you are starting anew, please use the new location.
+
This folder contains code and resources to run experiments and evaluations.
## For Benchmark Users
diff --git a/frontend/__tests__/components/conversation-tab-title.test.tsx b/frontend/__tests__/components/conversation-tab-title.test.tsx
new file mode 100644
index 0000000000..4e3a0aa0fe
--- /dev/null
+++ b/frontend/__tests__/components/conversation-tab-title.test.tsx
@@ -0,0 +1,149 @@
+import { render, screen, waitFor } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import { ConversationTabTitle } from "#/components/features/conversation/conversation-tabs/conversation-tab-title";
+import GitService from "#/api/git-service/git-service.api";
+import V1GitService from "#/api/git-service/v1-git-service.api";
+
+// Mock the services that the hook depends on
+vi.mock("#/api/git-service/git-service.api");
+vi.mock("#/api/git-service/v1-git-service.api");
+
+// Mock the hooks that useUnifiedGetGitChanges depends on
+vi.mock("#/hooks/use-conversation-id", () => ({
+ useConversationId: () => ({
+ conversationId: "test-conversation-id",
+ }),
+}));
+
+vi.mock("#/hooks/query/use-active-conversation", () => ({
+ useActiveConversation: () => ({
+ data: {
+ conversation_version: "V0",
+ url: null,
+ session_api_key: null,
+ selected_repository: null,
+ },
+ }),
+}));
+
+vi.mock("#/hooks/use-runtime-is-ready", () => ({
+ useRuntimeIsReady: () => true,
+}));
+
+vi.mock("#/utils/get-git-path", () => ({
+ getGitPath: () => "/workspace",
+}));
+
+describe("ConversationTabTitle", () => {
+ let queryClient: QueryClient;
+
+ beforeEach(() => {
+ queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ retry: false,
+ },
+ },
+ });
+
+ // Mock GitService methods
+ vi.mocked(GitService.getGitChanges).mockResolvedValue([]);
+ vi.mocked(V1GitService.getGitChanges).mockResolvedValue([]);
+ });
+
+ afterEach(() => {
+ vi.clearAllMocks();
+ queryClient.clear();
+ });
+
+ const renderWithProviders = (ui: React.ReactElement) => {
+ return render(
+ {ui} ,
+ );
+ };
+
+ describe("Rendering", () => {
+ it("should render the title", () => {
+ // Arrange
+ const title = "Test Title";
+
+ // Act
+ renderWithProviders(
+ ,
+ );
+
+ // Assert
+ expect(screen.getByText(title)).toBeInTheDocument();
+ });
+
+ it("should show refresh button when conversationKey is 'editor'", () => {
+ // Arrange
+ const title = "Changes";
+
+ // Act
+ renderWithProviders(
+ ,
+ );
+
+ // Assert
+ const refreshButton = screen.getByRole("button");
+ expect(refreshButton).toBeInTheDocument();
+ });
+
+ it("should not show refresh button when conversationKey is not 'editor'", () => {
+ // Arrange
+ const title = "Browser";
+
+ // Act
+ renderWithProviders(
+ ,
+ );
+
+ // Assert
+ expect(screen.queryByRole("button")).not.toBeInTheDocument();
+ });
+ });
+
+ describe("User Interactions", () => {
+ it("should call refetch and trigger GitService.getGitChanges when refresh button is clicked", async () => {
+ // Arrange
+ const user = userEvent.setup();
+ const title = "Changes";
+ const mockGitChanges: Array<{
+ path: string;
+ status: "M" | "A" | "D" | "R" | "U";
+ }> = [
+ { path: "file1.ts", status: "M" },
+ { path: "file2.ts", status: "A" },
+ ];
+
+ vi.mocked(GitService.getGitChanges).mockResolvedValue(mockGitChanges);
+
+ renderWithProviders(
+ ,
+ );
+
+ const refreshButton = screen.getByRole("button");
+
+ // Wait for initial query to complete
+ await waitFor(() => {
+ expect(GitService.getGitChanges).toHaveBeenCalled();
+ });
+
+ // Clear the mock to track refetch calls
+ vi.mocked(GitService.getGitChanges).mockClear();
+
+ // Act
+ await user.click(refreshButton);
+
+ // Assert - refetch should trigger another service call
+ await waitFor(() => {
+ expect(GitService.getGitChanges).toHaveBeenCalledWith(
+ "test-conversation-id",
+ );
+ });
+ });
+ });
+});
diff --git a/frontend/__tests__/components/features/conversation/agent-status.test.tsx b/frontend/__tests__/components/features/conversation/agent-status.test.tsx
new file mode 100644
index 0000000000..a121ed37a8
--- /dev/null
+++ b/frontend/__tests__/components/features/conversation/agent-status.test.tsx
@@ -0,0 +1,71 @@
+import { render, screen } from "@testing-library/react";
+import { describe, it, expect, vi } from "vitest";
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+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";
+
+vi.mock("#/hooks/use-agent-state");
+
+vi.mock("#/hooks/use-conversation-id", () => ({
+ useConversationId: () => ({ conversationId: "test-id" }),
+}));
+
+const wrapper = ({ children }: { children: React.ReactNode }) => (
+
+
+ {children}
+
+
+);
+
+const renderAgentStatus = ({
+ isPausing = false,
+}: { isPausing?: boolean } = {}) =>
+ render(
+ ,
+ { wrapper },
+ );
+
+describe("AgentStatus - isLoading logic", () => {
+ it("should show loading when curAgentState is INIT", () => {
+ vi.mocked(useAgentState).mockReturnValue({
+ curAgentState: AgentState.INIT,
+ });
+
+ renderAgentStatus();
+
+ expect(screen.getByTestId("agent-loading-spinner")).toBeInTheDocument();
+ });
+
+ it("should show loading when isPausing is true, even if shouldShownAgentLoading is false", () => {
+ vi.mocked(useAgentState).mockReturnValue({
+ curAgentState: AgentState.AWAITING_USER_INPUT,
+ });
+
+ renderAgentStatus({ isPausing: true });
+
+ expect(screen.getByTestId("agent-loading-spinner")).toBeInTheDocument();
+ });
+
+ it("should NOT update global shouldShownAgentLoading when only isPausing is true", () => {
+ vi.mocked(useAgentState).mockReturnValue({
+ curAgentState: AgentState.AWAITING_USER_INPUT,
+ });
+
+ renderAgentStatus({ isPausing: true });
+
+ // Loading spinner shows (because isPausing)
+ expect(screen.getByTestId("agent-loading-spinner")).toBeInTheDocument();
+
+ // But global state should be false (because shouldShownAgentLoading is false)
+ const { shouldShownAgentLoading } = useConversationStore.getState();
+ expect(shouldShownAgentLoading).toBe(false);
+ });
+});
diff --git a/frontend/__tests__/components/features/conversation/conversation-name.test.tsx b/frontend/__tests__/components/features/conversation/conversation-name.test.tsx
index 572ca590b1..41078b69cb 100644
--- a/frontend/__tests__/components/features/conversation/conversation-name.test.tsx
+++ b/frontend/__tests__/components/features/conversation/conversation-name.test.tsx
@@ -42,7 +42,7 @@ vi.mock("react-i18next", async () => {
BUTTON$EXPORT_CONVERSATION: "Export Conversation",
BUTTON$DOWNLOAD_VIA_VSCODE: "Download via VS Code",
BUTTON$SHOW_AGENT_TOOLS_AND_METADATA: "Show Agent Tools",
- CONVERSATION$SHOW_MICROAGENTS: "Show Microagents",
+ CONVERSATION$SHOW_SKILLS: "Show Skills",
BUTTON$DISPLAY_COST: "Display Cost",
COMMON$CLOSE_CONVERSATION_STOP_RUNTIME:
"Close Conversation (Stop Runtime)",
@@ -290,7 +290,7 @@ describe("ConversationNameContextMenu", () => {
onStop: vi.fn(),
onDisplayCost: vi.fn(),
onShowAgentTools: vi.fn(),
- onShowMicroagents: vi.fn(),
+ onShowSkills: vi.fn(),
onExportConversation: vi.fn(),
onDownloadViaVSCode: vi.fn(),
};
@@ -304,7 +304,7 @@ describe("ConversationNameContextMenu", () => {
expect(screen.getByTestId("stop-button")).toBeInTheDocument();
expect(screen.getByTestId("display-cost-button")).toBeInTheDocument();
expect(screen.getByTestId("show-agent-tools-button")).toBeInTheDocument();
- expect(screen.getByTestId("show-microagents-button")).toBeInTheDocument();
+ expect(screen.getByTestId("show-skills-button")).toBeInTheDocument();
expect(
screen.getByTestId("export-conversation-button"),
).toBeInTheDocument();
@@ -321,9 +321,7 @@ describe("ConversationNameContextMenu", () => {
expect(
screen.queryByTestId("show-agent-tools-button"),
).not.toBeInTheDocument();
- expect(
- screen.queryByTestId("show-microagents-button"),
- ).not.toBeInTheDocument();
+ expect(screen.queryByTestId("show-skills-button")).not.toBeInTheDocument();
expect(
screen.queryByTestId("export-conversation-button"),
).not.toBeInTheDocument();
@@ -410,19 +408,19 @@ describe("ConversationNameContextMenu", () => {
it("should call show microagents handler when show microagents button is clicked", async () => {
const user = userEvent.setup();
- const onShowMicroagents = vi.fn();
+ const onShowSkills = vi.fn();
renderWithProviders(
,
);
- const showMicroagentsButton = screen.getByTestId("show-microagents-button");
+ const showMicroagentsButton = screen.getByTestId("show-skills-button");
await user.click(showMicroagentsButton);
- expect(onShowMicroagents).toHaveBeenCalledTimes(1);
+ expect(onShowSkills).toHaveBeenCalledTimes(1);
});
it("should call export conversation handler when export conversation button is clicked", async () => {
@@ -519,7 +517,7 @@ describe("ConversationNameContextMenu", () => {
onStop: vi.fn(),
onDisplayCost: vi.fn(),
onShowAgentTools: vi.fn(),
- onShowMicroagents: vi.fn(),
+ onShowSkills: vi.fn(),
onExportConversation: vi.fn(),
onDownloadViaVSCode: vi.fn(),
};
@@ -541,8 +539,8 @@ describe("ConversationNameContextMenu", () => {
expect(screen.getByTestId("show-agent-tools-button")).toHaveTextContent(
"Show Agent Tools",
);
- expect(screen.getByTestId("show-microagents-button")).toHaveTextContent(
- "Show Microagents",
+ expect(screen.getByTestId("show-skills-button")).toHaveTextContent(
+ "Show Skills",
);
expect(screen.getByTestId("export-conversation-button")).toHaveTextContent(
"Export Conversation",
diff --git a/frontend/src/components/features/settings/mcp-settings/__tests__/mcp-server-form.validation.test.tsx b/frontend/__tests__/components/features/settings/mcp-settings/mcp-server-form.validation.test.tsx
similarity index 96%
rename from frontend/src/components/features/settings/mcp-settings/__tests__/mcp-server-form.validation.test.tsx
rename to frontend/__tests__/components/features/settings/mcp-settings/mcp-server-form.validation.test.tsx
index a2546ac15c..6b290c94b6 100644
--- a/frontend/src/components/features/settings/mcp-settings/__tests__/mcp-server-form.validation.test.tsx
+++ b/frontend/__tests__/components/features/settings/mcp-settings/mcp-server-form.validation.test.tsx
@@ -1,6 +1,6 @@
import { render, screen, fireEvent } from "@testing-library/react";
import { describe, it, expect, vi } from "vitest";
-import { MCPServerForm } from "../mcp-server-form";
+import { MCPServerForm } from "#/components/features/settings/mcp-settings/mcp-server-form";
// i18n mock
vi.mock("react-i18next", () => ({
diff --git a/frontend/src/components/features/settings/mcp-settings/__tests__/mcp-server-list.test.tsx b/frontend/__tests__/components/features/settings/mcp-settings/mcp-server-list.test.tsx
similarity index 98%
rename from frontend/src/components/features/settings/mcp-settings/__tests__/mcp-server-list.test.tsx
rename to frontend/__tests__/components/features/settings/mcp-settings/mcp-server-list.test.tsx
index 4e1c4fa986..9e75f24483 100644
--- a/frontend/src/components/features/settings/mcp-settings/__tests__/mcp-server-list.test.tsx
+++ b/frontend/__tests__/components/features/settings/mcp-settings/mcp-server-list.test.tsx
@@ -1,6 +1,6 @@
import { render, screen } from "@testing-library/react";
import { describe, it, expect, vi } from "vitest";
-import { MCPServerList } from "../mcp-server-list";
+import { MCPServerList } from "#/components/features/settings/mcp-settings/mcp-server-list";
// Mock react-i18next
vi.mock("react-i18next", () => ({
diff --git a/frontend/__tests__/components/modals/microagents/microagent-modal.test.tsx b/frontend/__tests__/components/modals/microagents/microagent-modal.test.tsx
deleted file mode 100644
index 858c07207d..0000000000
--- a/frontend/__tests__/components/modals/microagents/microagent-modal.test.tsx
+++ /dev/null
@@ -1,91 +0,0 @@
-import { screen } from "@testing-library/react";
-import userEvent from "@testing-library/user-event";
-import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
-import { renderWithProviders } from "test-utils";
-import { MicroagentsModal } from "#/components/features/conversation-panel/microagents-modal";
-import ConversationService from "#/api/conversation-service/conversation-service.api";
-import { AgentState } from "#/types/agent-state";
-import { useAgentState } from "#/hooks/use-agent-state";
-
-// Mock the agent state hook
-vi.mock("#/hooks/use-agent-state", () => ({
- useAgentState: vi.fn(),
-}));
-
-// Mock the conversation ID hook
-vi.mock("#/hooks/use-conversation-id", () => ({
- useConversationId: () => ({ conversationId: "test-conversation-id" }),
-}));
-
-describe("MicroagentsModal - Refresh Button", () => {
- const mockOnClose = vi.fn();
- const conversationId = "test-conversation-id";
-
- const defaultProps = {
- onClose: mockOnClose,
- conversationId,
- };
-
- const mockMicroagents = [
- {
- name: "Test Agent 1",
- type: "repo" as const,
- triggers: ["test", "example"],
- content: "This is test content for agent 1",
- },
- {
- name: "Test Agent 2",
- type: "knowledge" as const,
- triggers: ["help", "support"],
- content: "This is test content for agent 2",
- },
- ];
-
- beforeEach(() => {
- // Reset all mocks before each test
- vi.clearAllMocks();
-
- // Setup default mock for getMicroagents
- vi.spyOn(ConversationService, "getMicroagents").mockResolvedValue({
- microagents: mockMicroagents,
- });
-
- // Mock the agent state to return a ready state
- vi.mocked(useAgentState).mockReturnValue({
- curAgentState: AgentState.AWAITING_USER_INPUT,
- });
- });
-
- afterEach(() => {
- vi.restoreAllMocks();
- });
-
- describe("Refresh Button Rendering", () => {
- it("should render the refresh button with correct text and test ID", async () => {
- renderWithProviders( );
-
- // Wait for the component to load and render the refresh button
- const refreshButton = await screen.findByTestId("refresh-microagents");
- expect(refreshButton).toBeInTheDocument();
- expect(refreshButton).toHaveTextContent("BUTTON$REFRESH");
- });
- });
-
- describe("Refresh Button Functionality", () => {
- it("should call refetch when refresh button is clicked", async () => {
- const user = userEvent.setup();
- const refreshSpy = vi.spyOn(ConversationService, "getMicroagents");
-
- renderWithProviders( );
-
- // Wait for the component to load and render the refresh button
- const refreshButton = await screen.findByTestId("refresh-microagents");
-
- refreshSpy.mockClear();
-
- await user.click(refreshButton);
-
- expect(refreshSpy).toHaveBeenCalledTimes(1);
- });
- });
-});
diff --git a/frontend/__tests__/components/modals/skills/skill-modal.test.tsx b/frontend/__tests__/components/modals/skills/skill-modal.test.tsx
new file mode 100644
index 0000000000..33ab5098c8
--- /dev/null
+++ b/frontend/__tests__/components/modals/skills/skill-modal.test.tsx
@@ -0,0 +1,394 @@
+import { screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
+import { renderWithProviders } from "test-utils";
+import { SkillsModal } from "#/components/features/conversation-panel/skills-modal";
+import ConversationService from "#/api/conversation-service/conversation-service.api";
+import V1ConversationService from "#/api/conversation-service/v1-conversation-service.api";
+import { AgentState } from "#/types/agent-state";
+import { useAgentState } from "#/hooks/use-agent-state";
+import SettingsService from "#/api/settings-service/settings-service.api";
+
+// Mock the agent state hook
+vi.mock("#/hooks/use-agent-state", () => ({
+ useAgentState: vi.fn(),
+}));
+
+// Mock the conversation ID hook
+vi.mock("#/hooks/use-conversation-id", () => ({
+ useConversationId: () => ({ conversationId: "test-conversation-id" }),
+}));
+
+describe("SkillsModal - Refresh Button", () => {
+ const mockOnClose = vi.fn();
+ const conversationId = "test-conversation-id";
+
+ const defaultProps = {
+ onClose: mockOnClose,
+ conversationId,
+ };
+
+ const mockSkills = [
+ {
+ name: "Test Agent 1",
+ type: "repo" as const,
+ triggers: ["test", "example"],
+ content: "This is test content for agent 1",
+ },
+ {
+ name: "Test Agent 2",
+ type: "knowledge" as const,
+ triggers: ["help", "support"],
+ content: "This is test content for agent 2",
+ },
+ ];
+
+ beforeEach(() => {
+ // Reset all mocks before each test
+ vi.clearAllMocks();
+
+ // Setup default mock for getMicroagents (V0)
+ vi.spyOn(ConversationService, "getMicroagents").mockResolvedValue({
+ microagents: mockSkills,
+ });
+
+ // Mock the agent state to return a ready state
+ vi.mocked(useAgentState).mockReturnValue({
+ curAgentState: AgentState.AWAITING_USER_INPUT,
+ });
+ });
+
+ afterEach(() => {
+ vi.restoreAllMocks();
+ });
+
+ describe("Refresh Button Rendering", () => {
+ it("should render the refresh button with correct text and test ID", async () => {
+ renderWithProviders( );
+
+ // Wait for the component to load and render the refresh button
+ const refreshButton = await screen.findByTestId("refresh-skills");
+ expect(refreshButton).toBeInTheDocument();
+ expect(refreshButton).toHaveTextContent("BUTTON$REFRESH");
+ });
+ });
+
+ describe("Refresh Button Functionality", () => {
+ it("should call refetch when refresh button is clicked", async () => {
+ const user = userEvent.setup();
+ const refreshSpy = vi.spyOn(ConversationService, "getMicroagents");
+
+ renderWithProviders( );
+
+ // Wait for the component to load and render the refresh button
+ const refreshButton = await screen.findByTestId("refresh-skills");
+
+ // Clear previous calls to only track the click
+ refreshSpy.mockClear();
+
+ await user.click(refreshButton);
+
+ // Verify the refresh triggered a new API call
+ expect(refreshSpy).toHaveBeenCalled();
+ });
+ });
+});
+
+describe("useConversationSkills - V1 API Integration", () => {
+ const conversationId = "test-conversation-id";
+
+ const mockMicroagents = [
+ {
+ name: "V0 Test Agent",
+ type: "repo" as const,
+ triggers: ["v0"],
+ content: "V0 skill content",
+ },
+ ];
+
+ const mockSkills = [
+ {
+ name: "V1 Test Skill",
+ type: "knowledge" as const,
+ triggers: ["v1", "skill"],
+ content: "V1 skill content",
+ },
+ ];
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+
+ // Mock agent state
+ vi.mocked(useAgentState).mockReturnValue({
+ curAgentState: AgentState.AWAITING_USER_INPUT,
+ });
+ });
+
+ afterEach(() => {
+ vi.restoreAllMocks();
+ });
+
+ describe("V0 API Usage (v1_enabled: false)", () => {
+ it("should call v0 ConversationService.getMicroagents when v1_enabled is false", async () => {
+ // Arrange
+ const getMicroagentsSpy = vi
+ .spyOn(ConversationService, "getMicroagents")
+ .mockResolvedValue({ microagents: mockMicroagents });
+
+ vi.spyOn(SettingsService, "getSettings").mockResolvedValue({
+ v1_enabled: false,
+ llm_model: "test-model",
+ llm_base_url: "",
+ agent: "test-agent",
+ language: "en",
+ llm_api_key: null,
+ llm_api_key_set: false,
+ search_api_key_set: false,
+ confirmation_mode: false,
+ security_analyzer: null,
+ remote_runtime_resource_factor: null,
+ provider_tokens_set: {},
+ enable_default_condenser: false,
+ condenser_max_size: null,
+ enable_sound_notifications: false,
+ enable_proactive_conversation_starters: false,
+ enable_solvability_analysis: false,
+ user_consents_to_analytics: null,
+ max_budget_per_task: null,
+ });
+
+ // Act
+ renderWithProviders( );
+
+ // Assert
+ await screen.findByText("V0 Test Agent");
+ expect(getMicroagentsSpy).toHaveBeenCalledWith(conversationId);
+ expect(getMicroagentsSpy).toHaveBeenCalledTimes(1);
+ });
+
+ it("should display v0 skills correctly", async () => {
+ // Arrange
+ vi.spyOn(ConversationService, "getMicroagents").mockResolvedValue({
+ microagents: mockMicroagents,
+ });
+
+ vi.spyOn(SettingsService, "getSettings").mockResolvedValue({
+ v1_enabled: false,
+ llm_model: "test-model",
+ llm_base_url: "",
+ agent: "test-agent",
+ language: "en",
+ llm_api_key: null,
+ llm_api_key_set: false,
+ search_api_key_set: false,
+ confirmation_mode: false,
+ security_analyzer: null,
+ remote_runtime_resource_factor: null,
+ provider_tokens_set: {},
+ enable_default_condenser: false,
+ condenser_max_size: null,
+ enable_sound_notifications: false,
+ enable_proactive_conversation_starters: false,
+ enable_solvability_analysis: false,
+ user_consents_to_analytics: null,
+ max_budget_per_task: null,
+ });
+
+ // Act
+ renderWithProviders( );
+
+ // Assert
+ const agentName = await screen.findByText("V0 Test Agent");
+ expect(agentName).toBeInTheDocument();
+ });
+ });
+
+ describe("V1 API Usage (v1_enabled: true)", () => {
+ it("should call v1 V1ConversationService.getSkills when v1_enabled is true", async () => {
+ // Arrange
+ const getSkillsSpy = vi
+ .spyOn(V1ConversationService, "getSkills")
+ .mockResolvedValue({ skills: mockSkills });
+
+ vi.spyOn(SettingsService, "getSettings").mockResolvedValue({
+ v1_enabled: true,
+ llm_model: "test-model",
+ llm_base_url: "",
+ agent: "test-agent",
+ language: "en",
+ llm_api_key: null,
+ llm_api_key_set: false,
+ search_api_key_set: false,
+ confirmation_mode: false,
+ security_analyzer: null,
+ remote_runtime_resource_factor: null,
+ provider_tokens_set: {},
+ enable_default_condenser: false,
+ condenser_max_size: null,
+ enable_sound_notifications: false,
+ enable_proactive_conversation_starters: false,
+ enable_solvability_analysis: false,
+ user_consents_to_analytics: null,
+ max_budget_per_task: null,
+ });
+
+ // Act
+ renderWithProviders( );
+
+ // Assert
+ await screen.findByText("V1 Test Skill");
+ expect(getSkillsSpy).toHaveBeenCalledWith(conversationId);
+ expect(getSkillsSpy).toHaveBeenCalledTimes(1);
+ });
+
+ it("should display v1 skills correctly", async () => {
+ // Arrange
+ vi.spyOn(V1ConversationService, "getSkills").mockResolvedValue({
+ skills: mockSkills,
+ });
+
+ vi.spyOn(SettingsService, "getSettings").mockResolvedValue({
+ v1_enabled: true,
+ llm_model: "test-model",
+ llm_base_url: "",
+ agent: "test-agent",
+ language: "en",
+ llm_api_key: null,
+ llm_api_key_set: false,
+ search_api_key_set: false,
+ confirmation_mode: false,
+ security_analyzer: null,
+ remote_runtime_resource_factor: null,
+ provider_tokens_set: {},
+ enable_default_condenser: false,
+ condenser_max_size: null,
+ enable_sound_notifications: false,
+ enable_proactive_conversation_starters: false,
+ enable_solvability_analysis: false,
+ user_consents_to_analytics: null,
+ max_budget_per_task: null,
+ });
+
+ // Act
+ renderWithProviders( );
+
+ // Assert
+ const skillName = await screen.findByText("V1 Test Skill");
+ expect(skillName).toBeInTheDocument();
+ });
+
+ it("should use v1 API when v1_enabled is true", async () => {
+ // Arrange
+ vi.spyOn(SettingsService, "getSettings").mockResolvedValue({
+ v1_enabled: true,
+ llm_model: "test-model",
+ llm_base_url: "",
+ agent: "test-agent",
+ language: "en",
+ llm_api_key: null,
+ llm_api_key_set: false,
+ search_api_key_set: false,
+ confirmation_mode: false,
+ security_analyzer: null,
+ remote_runtime_resource_factor: null,
+ provider_tokens_set: {},
+ enable_default_condenser: false,
+ condenser_max_size: null,
+ enable_sound_notifications: false,
+ enable_proactive_conversation_starters: false,
+ enable_solvability_analysis: false,
+ user_consents_to_analytics: null,
+ max_budget_per_task: null,
+ });
+
+ const getSkillsSpy = vi
+ .spyOn(V1ConversationService, "getSkills")
+ .mockResolvedValue({
+ skills: mockSkills,
+ });
+
+ // Act
+ renderWithProviders( );
+
+ // Assert
+ await screen.findByText("V1 Test Skill");
+ // Verify v1 API was called
+ expect(getSkillsSpy).toHaveBeenCalledWith(conversationId);
+ });
+ });
+
+ describe("API Switching on Settings Change", () => {
+ it("should refetch using different API when v1_enabled setting changes", async () => {
+ // Arrange
+ const getMicroagentsSpy = vi
+ .spyOn(ConversationService, "getMicroagents")
+ .mockResolvedValue({ microagents: mockMicroagents });
+ const getSkillsSpy = vi
+ .spyOn(V1ConversationService, "getSkills")
+ .mockResolvedValue({ skills: mockSkills });
+
+ const settingsSpy = vi
+ .spyOn(SettingsService, "getSettings")
+ .mockResolvedValue({
+ v1_enabled: false,
+ llm_model: "test-model",
+ llm_base_url: "",
+ agent: "test-agent",
+ language: "en",
+ llm_api_key: null,
+ llm_api_key_set: false,
+ search_api_key_set: false,
+ confirmation_mode: false,
+ security_analyzer: null,
+ remote_runtime_resource_factor: null,
+ provider_tokens_set: {},
+ enable_default_condenser: false,
+ condenser_max_size: null,
+ enable_sound_notifications: false,
+ enable_proactive_conversation_starters: false,
+ enable_solvability_analysis: false,
+ user_consents_to_analytics: null,
+ max_budget_per_task: null,
+ });
+
+ // Act - Initial render with v1_enabled: false
+ const { rerender } = renderWithProviders(
+ ,
+ );
+
+ // Assert - v0 API called initially
+ await screen.findByText("V0 Test Agent");
+ expect(getMicroagentsSpy).toHaveBeenCalledWith(conversationId);
+
+ // Arrange - Change settings to v1_enabled: true
+ settingsSpy.mockResolvedValue({
+ v1_enabled: true,
+ llm_model: "test-model",
+ llm_base_url: "",
+ agent: "test-agent",
+ language: "en",
+ llm_api_key: null,
+ llm_api_key_set: false,
+ search_api_key_set: false,
+ confirmation_mode: false,
+ security_analyzer: null,
+ remote_runtime_resource_factor: null,
+ provider_tokens_set: {},
+ enable_default_condenser: false,
+ condenser_max_size: null,
+ enable_sound_notifications: false,
+ enable_proactive_conversation_starters: false,
+ enable_solvability_analysis: false,
+ user_consents_to_analytics: null,
+ max_budget_per_task: null,
+ });
+
+ // Act - Force re-render
+ rerender( );
+
+ // Assert - v1 API should be called after settings change
+ await screen.findByText("V1 Test Skill");
+ expect(getSkillsSpy).toHaveBeenCalledWith(conversationId);
+ });
+ });
+});
diff --git a/frontend/src/components/v1/chat/event-content-helpers/__tests__/get-observation-content.test.ts b/frontend/__tests__/components/v1/chat/event-content-helpers/get-observation-content.test.ts
similarity index 96%
rename from frontend/src/components/v1/chat/event-content-helpers/__tests__/get-observation-content.test.ts
rename to frontend/__tests__/components/v1/chat/event-content-helpers/get-observation-content.test.ts
index d35dc97925..9e2da14a26 100644
--- a/frontend/src/components/v1/chat/event-content-helpers/__tests__/get-observation-content.test.ts
+++ b/frontend/__tests__/components/v1/chat/event-content-helpers/get-observation-content.test.ts
@@ -1,5 +1,5 @@
import { describe, it, expect } from "vitest";
-import { getObservationContent } from "../get-observation-content";
+import { getObservationContent } from "#/components/v1/chat/event-content-helpers/get-observation-content";
import { ObservationEvent } from "#/types/v1/core";
import { BrowserObservation } from "#/types/v1/core/base/observation";
diff --git a/frontend/src/services/__tests__/actions.test.ts b/frontend/__tests__/services/actions.test.ts
similarity index 98%
rename from frontend/src/services/__tests__/actions.test.ts
rename to frontend/__tests__/services/actions.test.ts
index a0df1915a8..44700aef2c 100644
--- a/frontend/src/services/__tests__/actions.test.ts
+++ b/frontend/__tests__/services/actions.test.ts
@@ -1,5 +1,5 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
-import { handleStatusMessage } from "../actions";
+import { handleStatusMessage } from "#/services/actions";
import { StatusMessage } from "#/types/message";
import { queryClient } from "#/query-client-config";
import { useStatusStore } from "#/state/status-store";
diff --git a/frontend/src/utils/__tests__/custom-toast-handlers.test.ts b/frontend/__tests__/utils/custom-toast-handlers.test.ts
similarity index 98%
rename from frontend/src/utils/__tests__/custom-toast-handlers.test.ts
rename to frontend/__tests__/utils/custom-toast-handlers.test.ts
index 09023b517a..404bc1d4dd 100644
--- a/frontend/src/utils/__tests__/custom-toast-handlers.test.ts
+++ b/frontend/__tests__/utils/custom-toast-handlers.test.ts
@@ -3,7 +3,7 @@ import toast from "react-hot-toast";
import {
displaySuccessToast,
displayErrorToast,
-} from "../custom-toast-handlers";
+} from "#/utils/custom-toast-handlers";
// Mock react-hot-toast
vi.mock("react-hot-toast", () => ({
diff --git a/frontend/src/utils/__tests__/settings-utils.test.ts b/frontend/__tests__/utils/settings-utils.test.ts
similarity index 97%
rename from frontend/src/utils/__tests__/settings-utils.test.ts
rename to frontend/__tests__/utils/settings-utils.test.ts
index bf2ae794f2..9eb9a038a5 100644
--- a/frontend/src/utils/__tests__/settings-utils.test.ts
+++ b/frontend/__tests__/utils/settings-utils.test.ts
@@ -1,5 +1,5 @@
import { describe, it, expect } from "vitest";
-import { parseMaxBudgetPerTask, extractSettings } from "../settings-utils";
+import { parseMaxBudgetPerTask, extractSettings } from "#/utils/settings-utils";
describe("parseMaxBudgetPerTask", () => {
it("should return null for empty string", () => {
diff --git a/frontend/src/utils/__tests__/toast-duration.test.ts b/frontend/__tests__/utils/toast-duration.test.ts
similarity index 97%
rename from frontend/src/utils/__tests__/toast-duration.test.ts
rename to frontend/__tests__/utils/toast-duration.test.ts
index 3b5ffa8b69..3ef6c803d9 100644
--- a/frontend/src/utils/__tests__/toast-duration.test.ts
+++ b/frontend/__tests__/utils/toast-duration.test.ts
@@ -1,5 +1,5 @@
import { describe, it, expect } from "vitest";
-import { calculateToastDuration } from "../toast-duration";
+import { calculateToastDuration } from "#/utils/toast-duration";
describe("calculateToastDuration", () => {
it("should return minimum duration for short messages", () => {
diff --git a/frontend/src/utils/__tests__/vscode-url-helper.test.ts b/frontend/__tests__/utils/vscode-url-helper.test.ts
similarity index 96%
rename from frontend/src/utils/__tests__/vscode-url-helper.test.ts
rename to frontend/__tests__/utils/vscode-url-helper.test.ts
index c85804089b..a55b03bbbf 100644
--- a/frontend/src/utils/__tests__/vscode-url-helper.test.ts
+++ b/frontend/__tests__/utils/vscode-url-helper.test.ts
@@ -1,5 +1,5 @@
import { describe, it, expect, beforeEach, afterEach } from "vitest";
-import { transformVSCodeUrl } from "../vscode-url-helper";
+import { transformVSCodeUrl } from "#/utils/vscode-url-helper";
describe("transformVSCodeUrl", () => {
const originalWindowLocation = window.location;
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index b4425a3c58..e130cad40f 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -1,14 +1,14 @@
{
"name": "openhands-frontend",
- "version": "0.62.0",
+ "version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "openhands-frontend",
- "version": "0.62.0",
+ "version": "1.0.0",
"dependencies": {
- "@heroui/react": "2.8.5",
+ "@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",
@@ -24,13 +24,13 @@
"downshift": "^9.0.13",
"eslint-config-airbnb-typescript": "^18.0.0",
"framer-motion": "^12.23.25",
- "i18next": "^25.7.2",
+ "i18next": "^25.7.3",
"i18next-browser-languagedetector": "^8.2.0",
"i18next-http-backend": "^3.0.2",
"isbot": "^5.1.32",
"lucide-react": "^0.561.0",
"monaco-editor": "^0.55.1",
- "posthog-js": "^1.306.0",
+ "posthog-js": "^1.309.0",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"react-hot-toast": "^2.6.0",
@@ -45,7 +45,7 @@
"socket.io-client": "^4.8.1",
"tailwind-merge": "^3.4.0",
"tailwind-scrollbar": "^4.0.2",
- "vite": "^7.2.7",
+ "vite": "^7.3.0",
"zustand": "^5.0.9"
},
"devDependencies": {
@@ -56,15 +56,15 @@
"@tanstack/eslint-plugin-query": "^5.91.0",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.9.1",
- "@testing-library/react": "^16.3.0",
+ "@testing-library/react": "^16.3.1",
"@testing-library/user-event": "^14.6.1",
- "@types/node": "^25.0.1",
+ "@types/node": "^25.0.3",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"@types/react-syntax-highlighter": "^15.5.13",
"@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0",
- "@vitest/coverage-v8": "^4.0.14",
+ "@vitest/coverage-v8": "^4.0.16",
"cross-env": "^10.1.0",
"eslint": "^8.57.0",
"eslint-config-airbnb": "^19.0.4",
@@ -85,7 +85,7 @@
"tailwindcss": "^4.1.8",
"typescript": "^5.9.3",
"vite-plugin-svgr": "^4.5.0",
- "vite-tsconfig-paths": "^5.1.4",
+ "vite-tsconfig-paths": "^6.0.2",
"vitest": "^4.0.14"
},
"engines": {
@@ -789,13 +789,12 @@
"license": "MIT"
},
"node_modules/@esbuild/aix-ppc64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
- "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
+ "version": "0.27.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.1.tgz",
+ "integrity": "sha512-HHB50pdsBX6k47S4u5g/CaLjqS3qwaOVE5ILsq64jyzgMhLuCuZ8rGzM9yhsAjfjkbgUPMzZEPa7DAp7yz6vuA==",
"cpu": [
"ppc64"
],
- "license": "MIT",
"optional": true,
"os": [
"aix"
@@ -805,13 +804,12 @@
}
},
"node_modules/@esbuild/android-arm": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
- "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
+ "version": "0.27.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.1.tgz",
+ "integrity": "sha512-kFqa6/UcaTbGm/NncN9kzVOODjhZW8e+FRdSeypWe6j33gzclHtwlANs26JrupOntlcWmB0u8+8HZo8s7thHvg==",
"cpu": [
"arm"
],
- "license": "MIT",
"optional": true,
"os": [
"android"
@@ -821,13 +819,12 @@
}
},
"node_modules/@esbuild/android-arm64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
- "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
+ "version": "0.27.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.1.tgz",
+ "integrity": "sha512-45fuKmAJpxnQWixOGCrS+ro4Uvb4Re9+UTieUY2f8AEc+t7d4AaZ6eUJ3Hva7dtrxAAWHtlEFsXFMAgNnGU9uQ==",
"cpu": [
"arm64"
],
- "license": "MIT",
"optional": true,
"os": [
"android"
@@ -837,13 +834,12 @@
}
},
"node_modules/@esbuild/android-x64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
- "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
+ "version": "0.27.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.1.tgz",
+ "integrity": "sha512-LBEpOz0BsgMEeHgenf5aqmn/lLNTFXVfoWMUox8CtWWYK9X4jmQzWjoGoNb8lmAYml/tQ/Ysvm8q7szu7BoxRQ==",
"cpu": [
"x64"
],
- "license": "MIT",
"optional": true,
"os": [
"android"
@@ -853,13 +849,12 @@
}
},
"node_modules/@esbuild/darwin-arm64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
- "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
+ "version": "0.27.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.1.tgz",
+ "integrity": "sha512-veg7fL8eMSCVKL7IW4pxb54QERtedFDfY/ASrumK/SbFsXnRazxY4YykN/THYqFnFwJ0aVjiUrVG2PwcdAEqQQ==",
"cpu": [
"arm64"
],
- "license": "MIT",
"optional": true,
"os": [
"darwin"
@@ -869,13 +864,12 @@
}
},
"node_modules/@esbuild/darwin-x64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
- "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
+ "version": "0.27.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.1.tgz",
+ "integrity": "sha512-+3ELd+nTzhfWb07Vol7EZ+5PTbJ/u74nC6iv4/lwIU99Ip5uuY6QoIf0Hn4m2HoV0qcnRivN3KSqc+FyCHjoVQ==",
"cpu": [
"x64"
],
- "license": "MIT",
"optional": true,
"os": [
"darwin"
@@ -885,13 +879,12 @@
}
},
"node_modules/@esbuild/freebsd-arm64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
- "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
+ "version": "0.27.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.1.tgz",
+ "integrity": "sha512-/8Rfgns4XD9XOSXlzUDepG8PX+AVWHliYlUkFI3K3GB6tqbdjYqdhcb4BKRd7C0BhZSoaCxhv8kTcBrcZWP+xg==",
"cpu": [
"arm64"
],
- "license": "MIT",
"optional": true,
"os": [
"freebsd"
@@ -901,13 +894,12 @@
}
},
"node_modules/@esbuild/freebsd-x64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
- "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
+ "version": "0.27.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.1.tgz",
+ "integrity": "sha512-GITpD8dK9C+r+5yRT/UKVT36h/DQLOHdwGVwwoHidlnA168oD3uxA878XloXebK4Ul3gDBBIvEdL7go9gCUFzQ==",
"cpu": [
"x64"
],
- "license": "MIT",
"optional": true,
"os": [
"freebsd"
@@ -917,13 +909,12 @@
}
},
"node_modules/@esbuild/linux-arm": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
- "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
+ "version": "0.27.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.1.tgz",
+ "integrity": "sha512-ieMID0JRZY/ZeCrsFQ3Y3NlHNCqIhTprJfDgSB3/lv5jJZ8FX3hqPyXWhe+gvS5ARMBJ242PM+VNz/ctNj//eA==",
"cpu": [
"arm"
],
- "license": "MIT",
"optional": true,
"os": [
"linux"
@@ -933,13 +924,12 @@
}
},
"node_modules/@esbuild/linux-arm64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
- "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
+ "version": "0.27.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.1.tgz",
+ "integrity": "sha512-W9//kCrh/6in9rWIBdKaMtuTTzNj6jSeG/haWBADqLLa9P8O5YSRDzgD5y9QBok4AYlzS6ARHifAb75V6G670Q==",
"cpu": [
"arm64"
],
- "license": "MIT",
"optional": true,
"os": [
"linux"
@@ -949,13 +939,12 @@
}
},
"node_modules/@esbuild/linux-ia32": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
- "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
+ "version": "0.27.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.1.tgz",
+ "integrity": "sha512-VIUV4z8GD8rtSVMfAj1aXFahsi/+tcoXXNYmXgzISL+KB381vbSTNdeZHHHIYqFyXcoEhu9n5cT+05tRv13rlw==",
"cpu": [
"ia32"
],
- "license": "MIT",
"optional": true,
"os": [
"linux"
@@ -965,13 +954,12 @@
}
},
"node_modules/@esbuild/linux-loong64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
- "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
+ "version": "0.27.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.1.tgz",
+ "integrity": "sha512-l4rfiiJRN7sTNI//ff65zJ9z8U+k6zcCg0LALU5iEWzY+a1mVZ8iWC1k5EsNKThZ7XCQ6YWtsZ8EWYm7r1UEsg==",
"cpu": [
"loong64"
],
- "license": "MIT",
"optional": true,
"os": [
"linux"
@@ -981,13 +969,12 @@
}
},
"node_modules/@esbuild/linux-mips64el": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
- "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
+ "version": "0.27.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.1.tgz",
+ "integrity": "sha512-U0bEuAOLvO/DWFdygTHWY8C067FXz+UbzKgxYhXC0fDieFa0kDIra1FAhsAARRJbvEyso8aAqvPdNxzWuStBnA==",
"cpu": [
"mips64el"
],
- "license": "MIT",
"optional": true,
"os": [
"linux"
@@ -997,13 +984,12 @@
}
},
"node_modules/@esbuild/linux-ppc64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
- "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
+ "version": "0.27.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.1.tgz",
+ "integrity": "sha512-NzdQ/Xwu6vPSf/GkdmRNsOfIeSGnh7muundsWItmBsVpMoNPVpM61qNzAVY3pZ1glzzAxLR40UyYM23eaDDbYQ==",
"cpu": [
"ppc64"
],
- "license": "MIT",
"optional": true,
"os": [
"linux"
@@ -1013,13 +999,12 @@
}
},
"node_modules/@esbuild/linux-riscv64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
- "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
+ "version": "0.27.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.1.tgz",
+ "integrity": "sha512-7zlw8p3IApcsN7mFw0O1Z1PyEk6PlKMu18roImfl3iQHTnr/yAfYv6s4hXPidbDoI2Q0pW+5xeoM4eTCC0UdrQ==",
"cpu": [
"riscv64"
],
- "license": "MIT",
"optional": true,
"os": [
"linux"
@@ -1029,13 +1014,12 @@
}
},
"node_modules/@esbuild/linux-s390x": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
- "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
+ "version": "0.27.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.1.tgz",
+ "integrity": "sha512-cGj5wli+G+nkVQdZo3+7FDKC25Uh4ZVwOAK6A06Hsvgr8WqBBuOy/1s+PUEd/6Je+vjfm6stX0kmib5b/O2Ykw==",
"cpu": [
"s390x"
],
- "license": "MIT",
"optional": true,
"os": [
"linux"
@@ -1045,13 +1029,12 @@
}
},
"node_modules/@esbuild/linux-x64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
- "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
+ "version": "0.27.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.1.tgz",
+ "integrity": "sha512-z3H/HYI9MM0HTv3hQZ81f+AKb+yEoCRlUby1F80vbQ5XdzEMyY/9iNlAmhqiBKw4MJXwfgsh7ERGEOhrM1niMA==",
"cpu": [
"x64"
],
- "license": "MIT",
"optional": true,
"os": [
"linux"
@@ -1061,13 +1044,12 @@
}
},
"node_modules/@esbuild/netbsd-arm64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
- "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
+ "version": "0.27.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.1.tgz",
+ "integrity": "sha512-wzC24DxAvk8Em01YmVXyjl96Mr+ecTPyOuADAvjGg+fyBpGmxmcr2E5ttf7Im8D0sXZihpxzO1isus8MdjMCXQ==",
"cpu": [
"arm64"
],
- "license": "MIT",
"optional": true,
"os": [
"netbsd"
@@ -1077,13 +1059,12 @@
}
},
"node_modules/@esbuild/netbsd-x64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
- "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
+ "version": "0.27.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.1.tgz",
+ "integrity": "sha512-1YQ8ybGi2yIXswu6eNzJsrYIGFpnlzEWRl6iR5gMgmsrR0FcNoV1m9k9sc3PuP5rUBLshOZylc9nqSgymI+TYg==",
"cpu": [
"x64"
],
- "license": "MIT",
"optional": true,
"os": [
"netbsd"
@@ -1093,13 +1074,12 @@
}
},
"node_modules/@esbuild/openbsd-arm64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
- "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
+ "version": "0.27.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.1.tgz",
+ "integrity": "sha512-5Z+DzLCrq5wmU7RDaMDe2DVXMRm2tTDvX2KU14JJVBN2CT/qov7XVix85QoJqHltpvAOZUAc3ndU56HSMWrv8g==",
"cpu": [
"arm64"
],
- "license": "MIT",
"optional": true,
"os": [
"openbsd"
@@ -1109,13 +1089,12 @@
}
},
"node_modules/@esbuild/openbsd-x64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
- "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
+ "version": "0.27.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.1.tgz",
+ "integrity": "sha512-Q73ENzIdPF5jap4wqLtsfh8YbYSZ8Q0wnxplOlZUOyZy7B4ZKW8DXGWgTCZmF8VWD7Tciwv5F4NsRf6vYlZtqg==",
"cpu": [
"x64"
],
- "license": "MIT",
"optional": true,
"os": [
"openbsd"
@@ -1125,13 +1104,12 @@
}
},
"node_modules/@esbuild/openharmony-arm64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
- "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
+ "version": "0.27.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.1.tgz",
+ "integrity": "sha512-ajbHrGM/XiK+sXM0JzEbJAen+0E+JMQZ2l4RR4VFwvV9JEERx+oxtgkpoKv1SevhjavK2z2ReHk32pjzktWbGg==",
"cpu": [
"arm64"
],
- "license": "MIT",
"optional": true,
"os": [
"openharmony"
@@ -1141,13 +1119,12 @@
}
},
"node_modules/@esbuild/sunos-x64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
- "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
+ "version": "0.27.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.1.tgz",
+ "integrity": "sha512-IPUW+y4VIjuDVn+OMzHc5FV4GubIwPnsz6ubkvN8cuhEqH81NovB53IUlrlBkPMEPxvNnf79MGBoz8rZ2iW8HA==",
"cpu": [
"x64"
],
- "license": "MIT",
"optional": true,
"os": [
"sunos"
@@ -1157,13 +1134,12 @@
}
},
"node_modules/@esbuild/win32-arm64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
- "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
+ "version": "0.27.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.1.tgz",
+ "integrity": "sha512-RIVRWiljWA6CdVu8zkWcRmGP7iRRIIwvhDKem8UMBjPql2TXM5PkDVvvrzMtj1V+WFPB4K7zkIGM7VzRtFkjdg==",
"cpu": [
"arm64"
],
- "license": "MIT",
"optional": true,
"os": [
"win32"
@@ -1173,13 +1149,12 @@
}
},
"node_modules/@esbuild/win32-ia32": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
- "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
+ "version": "0.27.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.1.tgz",
+ "integrity": "sha512-2BR5M8CPbptC1AK5JbJT1fWrHLvejwZidKx3UMSF0ecHMa+smhi16drIrCEggkgviBwLYd5nwrFLSl5Kho96RQ==",
"cpu": [
"ia32"
],
- "license": "MIT",
"optional": true,
"os": [
"win32"
@@ -1189,13 +1164,12 @@
}
},
"node_modules/@esbuild/win32-x64": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
- "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
+ "version": "0.27.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.1.tgz",
+ "integrity": "sha512-d5X6RMYv6taIymSk8JBP+nxv8DQAMY6A51GPgusqLdK9wBz5wWIXy1KjTck6HnjE9hqJzJRdk+1p/t5soSbCtw==",
"cpu": [
"x64"
],
- "license": "MIT",
"optional": true,
"os": [
"win32"
@@ -1295,7 +1269,6 @@
"version": "2.3.6",
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.6.tgz",
"integrity": "sha512-HJnTFeRM2kVFVr5gr5kH1XP6K0JcJtE7Lzvtr3FS/so5f1kpsqqqxy5JF+FRaO6H2qmcMfAUIox7AJteieRtVw==",
- "license": "MIT",
"dependencies": {
"@formatjs/fast-memoize": "2.2.7",
"@formatjs/intl-localematcher": "0.6.2",
@@ -1307,7 +1280,6 @@
"version": "2.2.7",
"resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.7.tgz",
"integrity": "sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==",
- "license": "MIT",
"dependencies": {
"tslib": "^2.8.0"
}
@@ -1316,7 +1288,6 @@
"version": "2.11.4",
"resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.11.4.tgz",
"integrity": "sha512-7kR78cRrPNB4fjGFZg3Rmj5aah8rQj9KPzuLsmcSn4ipLXQvC04keycTI1F7kJYDwIXtT2+7IDEto842CfZBtw==",
- "license": "MIT",
"dependencies": {
"@formatjs/ecma402-abstract": "2.3.6",
"@formatjs/icu-skeleton-parser": "1.8.16",
@@ -1327,7 +1298,6 @@
"version": "1.8.16",
"resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.16.tgz",
"integrity": "sha512-H13E9Xl+PxBd8D5/6TVUluSpxGNvFSlN/b3coUp0e0JpuWXXnQDiavIpY3NnvSp4xhEMoXyyBvVfdFX8jglOHQ==",
- "license": "MIT",
"dependencies": {
"@formatjs/ecma402-abstract": "2.3.6",
"tslib": "^2.8.0"
@@ -1337,21 +1307,19 @@
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.6.2.tgz",
"integrity": "sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA==",
- "license": "MIT",
"dependencies": {
"tslib": "^2.8.0"
}
},
"node_modules/@heroui/accordion": {
- "version": "2.2.24",
- "resolved": "https://registry.npmjs.org/@heroui/accordion/-/accordion-2.2.24.tgz",
- "integrity": "sha512-iVJVKKsGN4t3hn4Exwic6n5SOQOmmmsodSsCt0VUcs5VTHu9876sAC44xlEMpc9CP8pC1wQS3DzWl3mN6Z120g==",
- "license": "MIT",
+ "version": "2.2.25",
+ "resolved": "https://registry.npmjs.org/@heroui/accordion/-/accordion-2.2.25.tgz",
+ "integrity": "sha512-cukvjTXfSLxjCZJ2PwLYUdkJuzKgKfbYkA+l2yvtYfrAQ8G0uz8a+tAGKGcciVLtYke1KsZ/pKjbpInWgGUV7A==",
"dependencies": {
- "@heroui/aria-utils": "2.2.24",
- "@heroui/divider": "2.2.20",
+ "@heroui/aria-utils": "2.2.25",
+ "@heroui/divider": "2.2.21",
"@heroui/dom-animation": "2.1.10",
- "@heroui/framer-utils": "2.1.23",
+ "@heroui/framer-utils": "2.1.24",
"@heroui/react-utils": "2.1.14",
"@heroui/shared-icons": "2.1.10",
"@heroui/shared-utils": "2.1.12",
@@ -1364,19 +1332,18 @@
},
"peerDependencies": {
"@heroui/system": ">=2.4.18",
- "@heroui/theme": ">=2.4.17",
+ "@heroui/theme": ">=2.4.23",
"framer-motion": ">=11.5.6 || >=12.0.0-alpha.1",
"react": ">=18 || >=19.0.0-rc.0",
"react-dom": ">=18 || >=19.0.0-rc.0"
}
},
"node_modules/@heroui/alert": {
- "version": "2.2.27",
- "resolved": "https://registry.npmjs.org/@heroui/alert/-/alert-2.2.27.tgz",
- "integrity": "sha512-Y6oX9SV//tdhxhpgkSZvnjwdx7d8S7RAhgVlxCs2Hla//nCFC3yiMHIv8UotTryAGdOwZIsffmcna9vqbNL5vw==",
- "license": "MIT",
+ "version": "2.2.28",
+ "resolved": "https://registry.npmjs.org/@heroui/alert/-/alert-2.2.28.tgz",
+ "integrity": "sha512-1FgaRWCSj2/s8L1DyQR0ao8cfdC60grC1EInNoqAyvcSJt6j9gK/zWKZTQn+NXDjV2N14dG+b7EjMUc8cJnUjA==",
"dependencies": {
- "@heroui/button": "2.2.27",
+ "@heroui/button": "2.2.28",
"@heroui/react-utils": "2.1.14",
"@heroui/shared-icons": "2.1.10",
"@heroui/shared-utils": "2.1.12",
@@ -1384,18 +1351,17 @@
},
"peerDependencies": {
"@heroui/system": ">=2.4.18",
- "@heroui/theme": ">=2.4.19",
+ "@heroui/theme": ">=2.4.23",
"react": ">=18 || >=19.0.0-rc.0",
"react-dom": ">=18 || >=19.0.0-rc.0"
}
},
"node_modules/@heroui/aria-utils": {
- "version": "2.2.24",
- "resolved": "https://registry.npmjs.org/@heroui/aria-utils/-/aria-utils-2.2.24.tgz",
- "integrity": "sha512-Y7FfQl2jvJr8JjpH+iuJElDwbn3eSWohuxHg6e5+xk5GcPYrEecgr0F/9qD6VU8IvVrRzJ00JzmT87lgA5iE3Q==",
- "license": "MIT",
+ "version": "2.2.25",
+ "resolved": "https://registry.npmjs.org/@heroui/aria-utils/-/aria-utils-2.2.25.tgz",
+ "integrity": "sha512-7ofC3q6qVksIIJMJu3X07oQKrVijw+eaE4LV8AHY/wRl1FFxuTwhxQmjW5JGsGQ0iwlzxf4D5rogYa4YCUcFag==",
"dependencies": {
- "@heroui/system": "2.4.23",
+ "@heroui/system": "2.4.24",
"@react-aria/utils": "3.31.0",
"@react-stately/collections": "3.12.8",
"@react-types/overlays": "3.9.2",
@@ -1407,19 +1373,18 @@
}
},
"node_modules/@heroui/autocomplete": {
- "version": "2.3.29",
- "resolved": "https://registry.npmjs.org/@heroui/autocomplete/-/autocomplete-2.3.29.tgz",
- "integrity": "sha512-BQkiWrrhPbNMFF1Hd60QDyG4iwD+sdsjWh0h7sw2XhcT6Bjw/6Hqpf4eHsTvPElW/554vPZVtChjugRY1N2zsw==",
- "license": "MIT",
+ "version": "2.3.30",
+ "resolved": "https://registry.npmjs.org/@heroui/autocomplete/-/autocomplete-2.3.30.tgz",
+ "integrity": "sha512-TT5p/EybRdxRs9g3DZGHYVpp4Sgs1X0kLZvc7qO4hzNyKEqmBOx8VESVZs43ZVmLxVWf7fOd3kbGVt9Sbm2U8A==",
"dependencies": {
- "@heroui/aria-utils": "2.2.24",
- "@heroui/button": "2.2.27",
- "@heroui/form": "2.1.27",
- "@heroui/input": "2.4.28",
- "@heroui/listbox": "2.3.26",
- "@heroui/popover": "2.3.27",
+ "@heroui/aria-utils": "2.2.25",
+ "@heroui/button": "2.2.28",
+ "@heroui/form": "2.1.28",
+ "@heroui/input": "2.4.29",
+ "@heroui/listbox": "2.3.27",
+ "@heroui/popover": "2.3.28",
"@heroui/react-utils": "2.1.14",
- "@heroui/scroll-shadow": "2.3.18",
+ "@heroui/scroll-shadow": "2.3.19",
"@heroui/shared-icons": "2.1.10",
"@heroui/shared-utils": "2.1.12",
"@heroui/use-safe-layout-effect": "2.1.8",
@@ -1431,17 +1396,16 @@
},
"peerDependencies": {
"@heroui/system": ">=2.4.18",
- "@heroui/theme": ">=2.4.17",
+ "@heroui/theme": ">=2.4.23",
"framer-motion": ">=11.5.6 || >=12.0.0-alpha.1",
"react": ">=18 || >=19.0.0-rc.0",
"react-dom": ">=18 || >=19.0.0-rc.0"
}
},
"node_modules/@heroui/avatar": {
- "version": "2.2.22",
- "resolved": "https://registry.npmjs.org/@heroui/avatar/-/avatar-2.2.22.tgz",
- "integrity": "sha512-znmKdsrVj91Fg8+wm/HA/b8zi3iAg5g3MezliBfS2PmwgZcpBR6VtwgeeP6uN49+TR+faGIrck0Zxceuw4U0FQ==",
- "license": "MIT",
+ "version": "2.2.23",
+ "resolved": "https://registry.npmjs.org/@heroui/avatar/-/avatar-2.2.23.tgz",
+ "integrity": "sha512-YBnb4v1cc/1kZTBx0AH0QNbEno+BhN/zdhxVRJDDI32aVvZhMpR90m7zTG4ma9oetOpCZ0pDeGKenlR9Ack4xg==",
"dependencies": {
"@heroui/react-utils": "2.1.14",
"@heroui/shared-utils": "2.1.12",
@@ -1451,32 +1415,30 @@
},
"peerDependencies": {
"@heroui/system": ">=2.4.18",
- "@heroui/theme": ">=2.4.17",
+ "@heroui/theme": ">=2.4.23",
"react": ">=18 || >=19.0.0-rc.0",
"react-dom": ">=18 || >=19.0.0-rc.0"
}
},
"node_modules/@heroui/badge": {
- "version": "2.2.17",
- "resolved": "https://registry.npmjs.org/@heroui/badge/-/badge-2.2.17.tgz",
- "integrity": "sha512-UNILRsAIJn+B6aWml+Rv2QCyYB7sadNqRPDPzNeVKJd8j3MNgZyyEHDwvqM2FWrgGccQIuWFaUgGdnPxRJpwwg==",
- "license": "MIT",
+ "version": "2.2.18",
+ "resolved": "https://registry.npmjs.org/@heroui/badge/-/badge-2.2.18.tgz",
+ "integrity": "sha512-OfGove8YJ9oDrdugzq05FC15ZKD5nzqe+thPZ+1SY1LZorJQjZvqSD9QnoEH1nG7fu2IdH6pYJy3sZ/b6Vj5Kg==",
"dependencies": {
"@heroui/react-utils": "2.1.14",
"@heroui/shared-utils": "2.1.12"
},
"peerDependencies": {
"@heroui/system": ">=2.4.18",
- "@heroui/theme": ">=2.4.17",
+ "@heroui/theme": ">=2.4.23",
"react": ">=18 || >=19.0.0-rc.0",
"react-dom": ">=18 || >=19.0.0-rc.0"
}
},
"node_modules/@heroui/breadcrumbs": {
- "version": "2.2.22",
- "resolved": "https://registry.npmjs.org/@heroui/breadcrumbs/-/breadcrumbs-2.2.22.tgz",
- "integrity": "sha512-2fWfpbwhRPeC99Kuzu+DnzOYL4TOkDm9sznvSj0kIAbw/Rvl+D2/6fmBOaTRIUXfswWpHVRUCcNYczIAp0PkoA==",
- "license": "MIT",
+ "version": "2.2.23",
+ "resolved": "https://registry.npmjs.org/@heroui/breadcrumbs/-/breadcrumbs-2.2.23.tgz",
+ "integrity": "sha512-trWtN/Ci2NTNRGvIxT8hdOml6med9F3HaCszqyVg3zroh6ZqV3iMPL3u4xRnAe0GLPsGwWFUnao7jbouU+avHw==",
"dependencies": {
"@heroui/react-utils": "2.1.14",
"@heroui/shared-icons": "2.1.10",
@@ -1487,21 +1449,20 @@
},
"peerDependencies": {
"@heroui/system": ">=2.4.18",
- "@heroui/theme": ">=2.4.17",
+ "@heroui/theme": ">=2.4.23",
"react": ">=18 || >=19.0.0-rc.0",
"react-dom": ">=18 || >=19.0.0-rc.0"
}
},
"node_modules/@heroui/button": {
- "version": "2.2.27",
- "resolved": "https://registry.npmjs.org/@heroui/button/-/button-2.2.27.tgz",
- "integrity": "sha512-Fxb8rtjPQm9T4GAtB1oW2QMUiQCtn7EtvO5AN41ANxAgmsNMM5wnLTkxQ05vNueCrp47kTDtSuyMhKU2llATHQ==",
- "license": "MIT",
+ "version": "2.2.28",
+ "resolved": "https://registry.npmjs.org/@heroui/button/-/button-2.2.28.tgz",
+ "integrity": "sha512-B4SSMeKXrbENs4VQ3U/MF+RTncPCU3DPYLYhhrDVVo/LXUIcN/KU/mJwF89eYQjvFXVyaZphC+i/5yLiN3uDcw==",
"dependencies": {
"@heroui/react-utils": "2.1.14",
- "@heroui/ripple": "2.2.20",
+ "@heroui/ripple": "2.2.21",
"@heroui/shared-utils": "2.1.12",
- "@heroui/spinner": "2.2.24",
+ "@heroui/spinner": "2.2.25",
"@heroui/use-aria-button": "2.2.20",
"@react-aria/focus": "3.21.2",
"@react-aria/interactions": "3.25.6",
@@ -1509,21 +1470,20 @@
},
"peerDependencies": {
"@heroui/system": ">=2.4.18",
- "@heroui/theme": ">=2.4.17",
+ "@heroui/theme": ">=2.4.23",
"framer-motion": ">=11.5.6 || >=12.0.0-alpha.1",
"react": ">=18 || >=19.0.0-rc.0",
"react-dom": ">=18 || >=19.0.0-rc.0"
}
},
"node_modules/@heroui/calendar": {
- "version": "2.2.27",
- "resolved": "https://registry.npmjs.org/@heroui/calendar/-/calendar-2.2.27.tgz",
- "integrity": "sha512-VtyXQSoT9u9tC4HjBkJIaSSmhau1LwPUwvof0LjYDpBfTsJKqn+308wI3nAp75BTbAkK+vFM8LI0VfbALCwR4Q==",
- "license": "MIT",
+ "version": "2.2.28",
+ "resolved": "https://registry.npmjs.org/@heroui/calendar/-/calendar-2.2.28.tgz",
+ "integrity": "sha512-iJ1jOljJQCgowGLesl27LPh44JjwYLyxuqwIIJqBspiARdtbCWyVRTXb5RaphnbNcZFDuYhyadkVtzZOYVUn8g==",
"dependencies": {
- "@heroui/button": "2.2.27",
+ "@heroui/button": "2.2.28",
"@heroui/dom-animation": "2.1.10",
- "@heroui/framer-utils": "2.1.23",
+ "@heroui/framer-utils": "2.1.24",
"@heroui/react-utils": "2.1.14",
"@heroui/shared-icons": "2.1.10",
"@heroui/shared-utils": "2.1.12",
@@ -1543,20 +1503,19 @@
},
"peerDependencies": {
"@heroui/system": ">=2.4.18",
- "@heroui/theme": ">=2.4.17",
+ "@heroui/theme": ">=2.4.23",
"framer-motion": ">=11.5.6 || >=12.0.0-alpha.1",
"react": ">=18 || >=19.0.0-rc.0",
"react-dom": ">=18 || >=19.0.0-rc.0"
}
},
"node_modules/@heroui/card": {
- "version": "2.2.25",
- "resolved": "https://registry.npmjs.org/@heroui/card/-/card-2.2.25.tgz",
- "integrity": "sha512-dtd/G24zePIHPutRIxWC69IO3IGJs8X+zh9rBYM9cY5Q972D8Eet5WdWTfDBhw//fFIoagDAs5YcI9emGczGaQ==",
- "license": "MIT",
+ "version": "2.2.26",
+ "resolved": "https://registry.npmjs.org/@heroui/card/-/card-2.2.26.tgz",
+ "integrity": "sha512-L+q1VLhEqA/s8o3DchojwtA66IE4MZzAhhPqivBD+mYCVtrCaueDMlU1q0o73SO2iloemRz33T5s4Uyf+1b8Bg==",
"dependencies": {
"@heroui/react-utils": "2.1.14",
- "@heroui/ripple": "2.2.20",
+ "@heroui/ripple": "2.2.21",
"@heroui/shared-utils": "2.1.12",
"@heroui/use-aria-button": "2.2.20",
"@react-aria/focus": "3.21.2",
@@ -1565,19 +1524,18 @@
},
"peerDependencies": {
"@heroui/system": ">=2.4.18",
- "@heroui/theme": ">=2.4.17",
+ "@heroui/theme": ">=2.4.23",
"framer-motion": ">=11.5.6 || >=12.0.0-alpha.1",
"react": ">=18 || >=19.0.0-rc.0",
"react-dom": ">=18 || >=19.0.0-rc.0"
}
},
"node_modules/@heroui/checkbox": {
- "version": "2.3.27",
- "resolved": "https://registry.npmjs.org/@heroui/checkbox/-/checkbox-2.3.27.tgz",
- "integrity": "sha512-YC0deiB7EOzcpJtk9SdySugD1Z2TNtfyYee2voDBHrng7ZBRB+cmAvizXINHnaQGFi0yuVPrZ5ixR/wsvTNW+Q==",
- "license": "MIT",
+ "version": "2.3.28",
+ "resolved": "https://registry.npmjs.org/@heroui/checkbox/-/checkbox-2.3.28.tgz",
+ "integrity": "sha512-lbnPihxNJXVxvpJeta6o17k7vu6fSvR6w+JsT/s5iurKk5qrkCrNBXmIZYdKJ43MmG3C/A0FWh3uNhZOM5Q04Q==",
"dependencies": {
- "@heroui/form": "2.1.27",
+ "@heroui/form": "2.1.28",
"@heroui/react-utils": "2.1.14",
"@heroui/shared-utils": "2.1.12",
"@heroui/use-callback-ref": "2.1.8",
@@ -1592,16 +1550,15 @@
},
"peerDependencies": {
"@heroui/system": ">=2.4.18",
- "@heroui/theme": ">=2.4.17",
+ "@heroui/theme": ">=2.4.23",
"react": ">=18 || >=19.0.0-rc.0",
"react-dom": ">=18 || >=19.0.0-rc.0"
}
},
"node_modules/@heroui/chip": {
- "version": "2.2.22",
- "resolved": "https://registry.npmjs.org/@heroui/chip/-/chip-2.2.22.tgz",
- "integrity": "sha512-6O4Sv1chP+xxftp7E5gHUJIzo04ML9BW9N9jjxWCqT0Qtl+a/ZxnDalCyup6oraMiVLLHp+zEVX93C+3LONgkg==",
- "license": "MIT",
+ "version": "2.2.23",
+ "resolved": "https://registry.npmjs.org/@heroui/chip/-/chip-2.2.23.tgz",
+ "integrity": "sha512-25HTWX5j9o0suoCYBiEo87ZoTt9VQfca+DSqphNMXHpbCQ0u26fL+8/jjehoYPtySJiLigwQeZn8BEjWWO3pGg==",
"dependencies": {
"@heroui/react-utils": "2.1.14",
"@heroui/shared-icons": "2.1.10",
@@ -1611,34 +1568,32 @@
},
"peerDependencies": {
"@heroui/system": ">=2.4.18",
- "@heroui/theme": ">=2.4.17",
+ "@heroui/theme": ">=2.4.23",
"react": ">=18 || >=19.0.0-rc.0",
"react-dom": ">=18 || >=19.0.0-rc.0"
}
},
"node_modules/@heroui/code": {
- "version": "2.2.21",
- "resolved": "https://registry.npmjs.org/@heroui/code/-/code-2.2.21.tgz",
- "integrity": "sha512-ExHcfTGr9tCbAaBOfMzTla8iHHfwIV5/xRk4WApeVmL4MiIlLMykc9bSi1c88ltaJInQGFAmE6MOFHXuGHxBXw==",
- "license": "MIT",
+ "version": "2.2.22",
+ "resolved": "https://registry.npmjs.org/@heroui/code/-/code-2.2.22.tgz",
+ "integrity": "sha512-i3pDe5Mzzh04jVx0gFwi2NMtCmsYfIRhLvkebXQcmfUDYl0+IGRJLcBsrWoOzes0pE/s7yyv+yJ/VhoU8F5jcg==",
"dependencies": {
"@heroui/react-utils": "2.1.14",
"@heroui/shared-utils": "2.1.12",
- "@heroui/system-rsc": "2.3.20"
+ "@heroui/system-rsc": "2.3.21"
},
"peerDependencies": {
- "@heroui/theme": ">=2.4.17",
+ "@heroui/theme": ">=2.4.23",
"react": ">=18 || >=19.0.0-rc.0",
"react-dom": ">=18 || >=19.0.0-rc.0"
}
},
"node_modules/@heroui/date-input": {
- "version": "2.3.27",
- "resolved": "https://registry.npmjs.org/@heroui/date-input/-/date-input-2.3.27.tgz",
- "integrity": "sha512-IxvZYezbR9jRxTWdsuHH47nsnB6RV1HPY7VwiJd9ZCy6P6oUV0Rx3cdwIRtUnyXbvz1G7+I22NL4C2Ku194l8A==",
- "license": "MIT",
+ "version": "2.3.28",
+ "resolved": "https://registry.npmjs.org/@heroui/date-input/-/date-input-2.3.28.tgz",
+ "integrity": "sha512-fzdfo9QMY9R+XffcuLOXXliM87eEu5Hz2wsUnsEAakXEbzAkFfzdSd72DRAbIiTD7yzSvaoyJHVAJ71+3/tCQg==",
"dependencies": {
- "@heroui/form": "2.1.27",
+ "@heroui/form": "2.1.28",
"@heroui/react-utils": "2.1.14",
"@heroui/shared-utils": "2.1.12",
"@internationalized/date": "3.10.0",
@@ -1650,23 +1605,22 @@
},
"peerDependencies": {
"@heroui/system": ">=2.4.18",
- "@heroui/theme": ">=2.4.17",
+ "@heroui/theme": ">=2.4.23",
"react": ">=18 || >=19.0.0-rc.0",
"react-dom": ">=18 || >=19.0.0-rc.0"
}
},
"node_modules/@heroui/date-picker": {
- "version": "2.3.28",
- "resolved": "https://registry.npmjs.org/@heroui/date-picker/-/date-picker-2.3.28.tgz",
- "integrity": "sha512-duKvXijabpafxU04sItrozf982tXkUDymcT3SoEvW4LDg6bECgPI8bYNN49hlzkI8+zuwJdKzJ4hDmANGVaL8Q==",
- "license": "MIT",
+ "version": "2.3.29",
+ "resolved": "https://registry.npmjs.org/@heroui/date-picker/-/date-picker-2.3.29.tgz",
+ "integrity": "sha512-kSvFjNuST2UhlDjDMvOHlbixyTsb4Dm7QNTXxeQGyKd6D5bUaBRzVSNaLnJ6Od/nEh30xqy3lZEq6nT5VqupMA==",
"dependencies": {
- "@heroui/aria-utils": "2.2.24",
- "@heroui/button": "2.2.27",
- "@heroui/calendar": "2.2.27",
- "@heroui/date-input": "2.3.27",
- "@heroui/form": "2.1.27",
- "@heroui/popover": "2.3.27",
+ "@heroui/aria-utils": "2.2.25",
+ "@heroui/button": "2.2.28",
+ "@heroui/calendar": "2.2.28",
+ "@heroui/date-input": "2.3.28",
+ "@heroui/form": "2.1.28",
+ "@heroui/popover": "2.3.28",
"@heroui/react-utils": "2.1.14",
"@heroui/shared-icons": "2.1.10",
"@heroui/shared-utils": "2.1.12",
@@ -1680,24 +1634,23 @@
},
"peerDependencies": {
"@heroui/system": ">=2.4.18",
- "@heroui/theme": ">=2.4.17",
+ "@heroui/theme": ">=2.4.23",
"framer-motion": ">=11.5.6 || >=12.0.0-alpha.1",
"react": ">=18 || >=19.0.0-rc.0",
"react-dom": ">=18 || >=19.0.0-rc.0"
}
},
"node_modules/@heroui/divider": {
- "version": "2.2.20",
- "resolved": "https://registry.npmjs.org/@heroui/divider/-/divider-2.2.20.tgz",
- "integrity": "sha512-t+NNJ2e5okZraLKQoj+rS2l49IMy5AeXTixjsR+QRZ/WPrETNpMj4lw5cBSxG0i7WhRhlBa+KgqweUUezvCdAg==",
- "license": "MIT",
+ "version": "2.2.21",
+ "resolved": "https://registry.npmjs.org/@heroui/divider/-/divider-2.2.21.tgz",
+ "integrity": "sha512-aVvl8/3fWUc+/fHbg+hD/0wrkoMKmXG0yRgyNrJSeu0pkRwhb0eD4ZjnBK1pCYqnstoltNE33J8ko/sU+WlmPw==",
"dependencies": {
"@heroui/react-rsc-utils": "2.1.9",
- "@heroui/system-rsc": "2.3.20",
+ "@heroui/system-rsc": "2.3.21",
"@react-types/shared": "3.32.1"
},
"peerDependencies": {
- "@heroui/theme": ">=2.4.17",
+ "@heroui/theme": ">=2.4.23",
"react": ">=18 || >=19.0.0-rc.0",
"react-dom": ">=18 || >=19.0.0-rc.0"
}
@@ -1706,38 +1659,35 @@
"version": "2.1.10",
"resolved": "https://registry.npmjs.org/@heroui/dom-animation/-/dom-animation-2.1.10.tgz",
"integrity": "sha512-dt+0xdVPbORwNvFT5pnqV2ULLlSgOJeqlg/DMo97s9RWeD6rD4VedNY90c8C9meqWqGegQYBQ9ztsfX32mGEPA==",
- "license": "MIT",
"peerDependencies": {
"framer-motion": ">=11.5.6 || >=12.0.0-alpha.1"
}
},
"node_modules/@heroui/drawer": {
- "version": "2.2.24",
- "resolved": "https://registry.npmjs.org/@heroui/drawer/-/drawer-2.2.24.tgz",
- "integrity": "sha512-gb51Lj9A8jlL1UvUrQ+MLS9tz+Qw+cdXwIJd39RXDkJwDmxqhzkz+WoOPZZwcOAHtATmwlTuxxlv6Cro59iswg==",
- "license": "MIT",
+ "version": "2.2.25",
+ "resolved": "https://registry.npmjs.org/@heroui/drawer/-/drawer-2.2.25.tgz",
+ "integrity": "sha512-+TFagy61+8dm+EWXLY5NJUGJ4COPL4anRiynw92iSD+arKUGN5b6lJUnjf9NkqwM5jqWKk1vxWdGDZEKZva8Bg==",
"dependencies": {
- "@heroui/framer-utils": "2.1.23",
- "@heroui/modal": "2.2.24",
+ "@heroui/framer-utils": "2.1.24",
+ "@heroui/modal": "2.2.25",
"@heroui/react-utils": "2.1.14",
"@heroui/shared-utils": "2.1.12"
},
"peerDependencies": {
"@heroui/system": ">=2.4.18",
- "@heroui/theme": ">=2.4.17",
+ "@heroui/theme": ">=2.4.23",
"react": ">=18 || >=19.0.0-rc.0",
"react-dom": ">=18 || >=19.0.0-rc.0"
}
},
"node_modules/@heroui/dropdown": {
- "version": "2.3.27",
- "resolved": "https://registry.npmjs.org/@heroui/dropdown/-/dropdown-2.3.27.tgz",
- "integrity": "sha512-6aedMmxC+St5Ixz9o3s0ERkLOR6ZQE2uRccmRchPCEt7ZJU6TAeJo7fSpxIvdEUjFDe+pNhR2ojIocZEXtBZZg==",
- "license": "MIT",
+ "version": "2.3.28",
+ "resolved": "https://registry.npmjs.org/@heroui/dropdown/-/dropdown-2.3.28.tgz",
+ "integrity": "sha512-q+bSLxdsHtauqpQ4529cSkjj8L20UdvbrRGmhRL3YLZyLEzGcCCp6kDRCchkCpTaxK7u869eF9TGSNoFeum92g==",
"dependencies": {
- "@heroui/aria-utils": "2.2.24",
- "@heroui/menu": "2.2.26",
- "@heroui/popover": "2.3.27",
+ "@heroui/aria-utils": "2.2.25",
+ "@heroui/menu": "2.2.27",
+ "@heroui/popover": "2.3.28",
"@heroui/react-utils": "2.1.14",
"@heroui/shared-utils": "2.1.12",
"@react-aria/focus": "3.21.2",
@@ -1747,39 +1697,37 @@
},
"peerDependencies": {
"@heroui/system": ">=2.4.18",
- "@heroui/theme": ">=2.4.17",
+ "@heroui/theme": ">=2.4.23",
"framer-motion": ">=11.5.6 || >=12.0.0-alpha.1",
"react": ">=18 || >=19.0.0-rc.0",
"react-dom": ">=18 || >=19.0.0-rc.0"
}
},
"node_modules/@heroui/form": {
- "version": "2.1.27",
- "resolved": "https://registry.npmjs.org/@heroui/form/-/form-2.1.27.tgz",
- "integrity": "sha512-vtaBqWhxppkJeWgbAZA/A1bRj6XIudBqJWSkoqYlejtLuvaxNwxQ2Z9u7ewxN96R6QqPrQwChlknIn0NgCWlXQ==",
- "license": "MIT",
+ "version": "2.1.28",
+ "resolved": "https://registry.npmjs.org/@heroui/form/-/form-2.1.28.tgz",
+ "integrity": "sha512-skg9GooN1+rgQwM0/7wNqUenq6JBEf3T2tDBItJU/oeNC9oaX00JDpy8rpMz9zS0oUqfbJ0auT11+0FRo2W6CQ==",
"dependencies": {
"@heroui/shared-utils": "2.1.12",
- "@heroui/system": "2.4.23",
- "@heroui/theme": "2.4.23",
+ "@heroui/system": "2.4.24",
+ "@heroui/theme": "2.4.24",
"@react-stately/form": "3.2.2",
"@react-types/form": "3.7.16",
"@react-types/shared": "3.32.1"
},
"peerDependencies": {
"@heroui/system": ">=2.4.18",
- "@heroui/theme": ">=2.4.17",
+ "@heroui/theme": ">=2.4.23",
"react": ">=18",
"react-dom": ">=18"
}
},
"node_modules/@heroui/framer-utils": {
- "version": "2.1.23",
- "resolved": "https://registry.npmjs.org/@heroui/framer-utils/-/framer-utils-2.1.23.tgz",
- "integrity": "sha512-crLLMjRmxs8/fysFv5gwghSGcDmYYkhNfAWh1rFzDy+FRPZN4f/bPH2rt85hdApmuHbWt0QCocqsrjHxLEzrAw==",
- "license": "MIT",
+ "version": "2.1.24",
+ "resolved": "https://registry.npmjs.org/@heroui/framer-utils/-/framer-utils-2.1.24.tgz",
+ "integrity": "sha512-PiHEV8YS3Q0ve3ZnTASVvTeBK0fTFdLtLiPtCuLucC2WGeDFjUerE7++Y+HhWB85Jj/USknEpl0aGsatl3cbgg==",
"dependencies": {
- "@heroui/system": "2.4.23",
+ "@heroui/system": "2.4.24",
"@heroui/use-measure": "2.1.8"
},
"peerDependencies": {
@@ -1789,10 +1737,9 @@
}
},
"node_modules/@heroui/image": {
- "version": "2.2.17",
- "resolved": "https://registry.npmjs.org/@heroui/image/-/image-2.2.17.tgz",
- "integrity": "sha512-B/MrWafTsiCBFnRc0hPTLDBh7APjb/lRuQf18umuh20/1n6KiQXJ7XGSjnrHaA6HQcrtMGh6mDFZDaXq9rHuoA==",
- "license": "MIT",
+ "version": "2.2.18",
+ "resolved": "https://registry.npmjs.org/@heroui/image/-/image-2.2.18.tgz",
+ "integrity": "sha512-hrvj/hDM0+Khb9EqstZOPeO0vIGZvhrJWPMxk7a6i2PqhWWQI+ws+nrwsG5XqAkwE4mqqf9Uw8EMfIG1XE5YYg==",
"dependencies": {
"@heroui/react-utils": "2.1.14",
"@heroui/shared-utils": "2.1.12",
@@ -1800,18 +1747,17 @@
},
"peerDependencies": {
"@heroui/system": ">=2.4.18",
- "@heroui/theme": ">=2.4.17",
+ "@heroui/theme": ">=2.4.23",
"react": ">=18 || >=19.0.0-rc.0",
"react-dom": ">=18 || >=19.0.0-rc.0"
}
},
"node_modules/@heroui/input": {
- "version": "2.4.28",
- "resolved": "https://registry.npmjs.org/@heroui/input/-/input-2.4.28.tgz",
- "integrity": "sha512-uaBubg814YOlVvX13yCAMqsR9HC4jg+asQdukbOvOnFtHY/d53her1BDdXhR9tMcrRTdYWQ3FoHqWbpvd5X4OQ==",
- "license": "MIT",
+ "version": "2.4.29",
+ "resolved": "https://registry.npmjs.org/@heroui/input/-/input-2.4.29.tgz",
+ "integrity": "sha512-PIjFmN6BTLvnlI0I9f7PjxvnviauOczRJGaTnlHKDniknoh7mi8j0voXwL/f6BAkVKrgpT5JiFvdjq6og+cfSA==",
"dependencies": {
- "@heroui/form": "2.1.27",
+ "@heroui/form": "2.1.28",
"@heroui/react-utils": "2.1.14",
"@heroui/shared-icons": "2.1.10",
"@heroui/shared-utils": "2.1.12",
@@ -1826,18 +1772,17 @@
},
"peerDependencies": {
"@heroui/system": ">=2.4.18",
- "@heroui/theme": ">=2.4.19",
+ "@heroui/theme": ">=2.4.23",
"react": ">=18 || >=19.0.0-rc.0",
"react-dom": ">=18 || >=19.0.0-rc.0"
}
},
"node_modules/@heroui/input-otp": {
- "version": "2.1.27",
- "resolved": "https://registry.npmjs.org/@heroui/input-otp/-/input-otp-2.1.27.tgz",
- "integrity": "sha512-VUzQ1u6/0okE0eqDx/2I/8zpGItSsn7Zml01IVwGM4wY2iJeQA+uRjfP+B1ff9jO/y8n582YU4uv/ZSOmmEQ7A==",
- "license": "MIT",
+ "version": "2.1.28",
+ "resolved": "https://registry.npmjs.org/@heroui/input-otp/-/input-otp-2.1.28.tgz",
+ "integrity": "sha512-IHr35WqOHb8SBoMXYt6wxzKQg8iFMdc7iqFa8jqdshfVIS3bvxvJj6PGND3LoZxrRFplCv12lfmp2fWymQLleA==",
"dependencies": {
- "@heroui/form": "2.1.27",
+ "@heroui/form": "2.1.28",
"@heroui/react-utils": "2.1.14",
"@heroui/shared-utils": "2.1.12",
"@heroui/use-form-reset": "2.0.1",
@@ -1850,32 +1795,30 @@
},
"peerDependencies": {
"@heroui/system": ">=2.4.18",
- "@heroui/theme": ">=2.4.17",
+ "@heroui/theme": ">=2.4.23",
"react": ">=18",
"react-dom": ">=18"
}
},
"node_modules/@heroui/kbd": {
- "version": "2.2.22",
- "resolved": "https://registry.npmjs.org/@heroui/kbd/-/kbd-2.2.22.tgz",
- "integrity": "sha512-PKhgwGB7i53kBuqB1YdFZsg7H9fJ8YESMRRPwRRyPSz5feMdwGidyXs+/ix7lrlYp4mlC3wtPp7L79SEyPCpBA==",
- "license": "MIT",
+ "version": "2.2.23",
+ "resolved": "https://registry.npmjs.org/@heroui/kbd/-/kbd-2.2.23.tgz",
+ "integrity": "sha512-nKL1Kl044l1Xsk4U8Nib3wFD2NlZCZo6kdqiqUv+DchOo4s3BJcxWSWqHn6fDVmHNyj3DFMYDvA2f/geMasaHQ==",
"dependencies": {
"@heroui/react-utils": "2.1.14",
"@heroui/shared-utils": "2.1.12",
- "@heroui/system-rsc": "2.3.20"
+ "@heroui/system-rsc": "2.3.21"
},
"peerDependencies": {
- "@heroui/theme": ">=2.4.17",
+ "@heroui/theme": ">=2.4.23",
"react": ">=18 || >=19.0.0-rc.0",
"react-dom": ">=18 || >=19.0.0-rc.0"
}
},
"node_modules/@heroui/link": {
- "version": "2.2.23",
- "resolved": "https://registry.npmjs.org/@heroui/link/-/link-2.2.23.tgz",
- "integrity": "sha512-lObtPRLy8ModlTvJiKhczuAV/CIt31hde6xPGFYRpPsaQN1b7RgQMmai5/Iv/M8WrzFmFZRpgW75RKYIB6hHVQ==",
- "license": "MIT",
+ "version": "2.2.24",
+ "resolved": "https://registry.npmjs.org/@heroui/link/-/link-2.2.24.tgz",
+ "integrity": "sha512-rxtSC/8++wCtZs2GqBCukQHtDAbqB5bXT24v03q86oz7VOlbn8pox38LwFKrb/H+A3o+BjSKuTJsYidJcQ5clg==",
"dependencies": {
"@heroui/react-utils": "2.1.14",
"@heroui/shared-icons": "2.1.10",
@@ -1886,19 +1829,18 @@
},
"peerDependencies": {
"@heroui/system": ">=2.4.18",
- "@heroui/theme": ">=2.4.17",
+ "@heroui/theme": ">=2.4.23",
"react": ">=18 || >=19.0.0-rc.0",
"react-dom": ">=18 || >=19.0.0-rc.0"
}
},
"node_modules/@heroui/listbox": {
- "version": "2.3.26",
- "resolved": "https://registry.npmjs.org/@heroui/listbox/-/listbox-2.3.26.tgz",
- "integrity": "sha512-/k3k+xyl2d+aFfT02h+/0njhsDX8vJDEkPK+dl9ETYI9Oz3L+xbHN9yIzuWjBXYkNGlQCjQ46N+0jWjhP5B4pA==",
- "license": "MIT",
+ "version": "2.3.27",
+ "resolved": "https://registry.npmjs.org/@heroui/listbox/-/listbox-2.3.27.tgz",
+ "integrity": "sha512-NUBDwP9Xzx3A/0iX/09hhs4/y8Loo+bCTm/vqFqYyufR8AOGLw1Xn0poTybPfE4L5U+6Y1P7GM0VjgZVw9dFQQ==",
"dependencies": {
- "@heroui/aria-utils": "2.2.24",
- "@heroui/divider": "2.2.20",
+ "@heroui/aria-utils": "2.2.25",
+ "@heroui/divider": "2.2.21",
"@heroui/react-utils": "2.1.14",
"@heroui/shared-utils": "2.1.12",
"@heroui/use-is-mobile": "2.2.12",
@@ -1911,19 +1853,18 @@
},
"peerDependencies": {
"@heroui/system": ">=2.4.18",
- "@heroui/theme": ">=2.4.17",
+ "@heroui/theme": ">=2.4.23",
"react": ">=18 || >=19.0.0-rc.0",
"react-dom": ">=18 || >=19.0.0-rc.0"
}
},
"node_modules/@heroui/menu": {
- "version": "2.2.26",
- "resolved": "https://registry.npmjs.org/@heroui/menu/-/menu-2.2.26.tgz",
- "integrity": "sha512-raR5pXgEqizKD9GsWS1yKqTm4RPWMrSQlqXLE2zNMQk0TkDqmPVw1z5griMqu2Zt9Vf2Ectf55vh4c0DNOUGlg==",
- "license": "MIT",
+ "version": "2.2.27",
+ "resolved": "https://registry.npmjs.org/@heroui/menu/-/menu-2.2.27.tgz",
+ "integrity": "sha512-Ifsb9QBVpAFFcIEEcp3nU28DBtIU0iI7B5HHpblHDJoDtjIbkyNOnyxoEj8eX63QTWQcKrmNnFYdtsrtS9K1RA==",
"dependencies": {
- "@heroui/aria-utils": "2.2.24",
- "@heroui/divider": "2.2.20",
+ "@heroui/aria-utils": "2.2.25",
+ "@heroui/divider": "2.2.21",
"@heroui/react-utils": "2.1.14",
"@heroui/shared-utils": "2.1.12",
"@heroui/use-is-mobile": "2.2.12",
@@ -1936,19 +1877,18 @@
},
"peerDependencies": {
"@heroui/system": ">=2.4.18",
- "@heroui/theme": ">=2.4.17",
+ "@heroui/theme": ">=2.4.23",
"react": ">=18 || >=19.0.0-rc.0",
"react-dom": ">=18 || >=19.0.0-rc.0"
}
},
"node_modules/@heroui/modal": {
- "version": "2.2.24",
- "resolved": "https://registry.npmjs.org/@heroui/modal/-/modal-2.2.24.tgz",
- "integrity": "sha512-ISbgorNqgps9iUvQdgANxprdN+6H3Sx9TrGKpuW798qjc2f0T4rTbjrEfFPT8tFx6XYF4P5j7T7m3zoKcortHQ==",
- "license": "MIT",
+ "version": "2.2.25",
+ "resolved": "https://registry.npmjs.org/@heroui/modal/-/modal-2.2.25.tgz",
+ "integrity": "sha512-qoUk0fe/GMbKHUWcW8XThp+TifEG6GgmpBKZ4x8hhM5o/t1cKAD4+F2pKahtih0ba5qjM+tFtwnUV7z7Mt8+xg==",
"dependencies": {
"@heroui/dom-animation": "2.1.10",
- "@heroui/framer-utils": "2.1.23",
+ "@heroui/framer-utils": "2.1.24",
"@heroui/react-utils": "2.1.14",
"@heroui/shared-icons": "2.1.10",
"@heroui/shared-utils": "2.1.12",
@@ -1964,20 +1904,19 @@
},
"peerDependencies": {
"@heroui/system": ">=2.4.18",
- "@heroui/theme": ">=2.4.17",
+ "@heroui/theme": ">=2.4.23",
"framer-motion": ">=11.5.6 || >=12.0.0-alpha.1",
"react": ">=18 || >=19.0.0-rc.0",
"react-dom": ">=18 || >=19.0.0-rc.0"
}
},
"node_modules/@heroui/navbar": {
- "version": "2.2.25",
- "resolved": "https://registry.npmjs.org/@heroui/navbar/-/navbar-2.2.25.tgz",
- "integrity": "sha512-5fNIMDpX2htDTMb/Xgv81qw/FuNWb+0Wpfc6rkFtNYd968I7G6Kjm782QB8WQjZ8DsMugcLEYUN4lpbJHRSdwg==",
- "license": "MIT",
+ "version": "2.2.26",
+ "resolved": "https://registry.npmjs.org/@heroui/navbar/-/navbar-2.2.26.tgz",
+ "integrity": "sha512-uQhISgbQgea1ki0et3hDJ8+IXc35zMNowRQTKgWeEF8T3yS5X2fKuLzJc7/cf0vUGnxH0FPB3Z5Cb7o1nwjr9A==",
"dependencies": {
"@heroui/dom-animation": "2.1.10",
- "@heroui/framer-utils": "2.1.23",
+ "@heroui/framer-utils": "2.1.24",
"@heroui/react-utils": "2.1.14",
"@heroui/shared-utils": "2.1.12",
"@heroui/use-resize": "2.1.8",
@@ -1991,20 +1930,19 @@
},
"peerDependencies": {
"@heroui/system": ">=2.4.18",
- "@heroui/theme": ">=2.4.17",
+ "@heroui/theme": ">=2.4.23",
"framer-motion": ">=11.5.6 || >=12.0.0-alpha.1",
"react": ">=18 || >=19.0.0-rc.0",
"react-dom": ">=18 || >=19.0.0-rc.0"
}
},
"node_modules/@heroui/number-input": {
- "version": "2.0.18",
- "resolved": "https://registry.npmjs.org/@heroui/number-input/-/number-input-2.0.18.tgz",
- "integrity": "sha512-28v0/0FABs+yy3CcJimcr5uNlhaJSyKt1ENMSXfzPxdN2WgIs14+6NLMT+KV7ibcJl7kmqG0uc8vuIDLVrM5bQ==",
- "license": "MIT",
+ "version": "2.0.19",
+ "resolved": "https://registry.npmjs.org/@heroui/number-input/-/number-input-2.0.19.tgz",
+ "integrity": "sha512-5UHdznU9XIqjRH17dG277YQrTnUeifWmHdU76Jzf78+SVsJgQdLqcRINHPVj382q0kd6vLMzc4Hyb2fQ0g2WXg==",
"dependencies": {
- "@heroui/button": "2.2.27",
- "@heroui/form": "2.1.27",
+ "@heroui/button": "2.2.28",
+ "@heroui/form": "2.1.28",
"@heroui/react-utils": "2.1.14",
"@heroui/shared-icons": "2.1.10",
"@heroui/shared-utils": "2.1.12",
@@ -2020,16 +1958,15 @@
},
"peerDependencies": {
"@heroui/system": ">=2.4.18",
- "@heroui/theme": ">=2.4.19",
+ "@heroui/theme": ">=2.4.23",
"react": ">=18 || >=19.0.0-rc.0",
"react-dom": ">=18 || >=19.0.0-rc.0"
}
},
"node_modules/@heroui/pagination": {
- "version": "2.2.24",
- "resolved": "https://registry.npmjs.org/@heroui/pagination/-/pagination-2.2.24.tgz",
- "integrity": "sha512-5ObSJ1PzB9D1CjHV0MfDNzLR69vSYpx/rNQLBo/D4g5puaAR7kkGgw5ncf5eirhdKuy9y8VGAhjwhBxO4NUdpQ==",
- "license": "MIT",
+ "version": "2.2.25",
+ "resolved": "https://registry.npmjs.org/@heroui/pagination/-/pagination-2.2.25.tgz",
+ "integrity": "sha512-PQZMNQ7wiv++cLEpEXDAdID3IQE2FlG1UkcuYhVYLPJgGSxoKKcM81wmE/HYMgmIMXySiZ+9E/UM8HATrpvTzA==",
"dependencies": {
"@heroui/react-utils": "2.1.14",
"@heroui/shared-icons": "2.1.10",
@@ -2044,21 +1981,20 @@
},
"peerDependencies": {
"@heroui/system": ">=2.4.18",
- "@heroui/theme": ">=2.4.17",
+ "@heroui/theme": ">=2.4.23",
"react": ">=18 || >=19.0.0-rc.0",
"react-dom": ">=18 || >=19.0.0-rc.0"
}
},
"node_modules/@heroui/popover": {
- "version": "2.3.27",
- "resolved": "https://registry.npmjs.org/@heroui/popover/-/popover-2.3.27.tgz",
- "integrity": "sha512-PmSCKQcAvKIegK59Flr9cglbsEu7OAegQMtwNIjqWHsPT18NNphimmUSJrtuD78rcfKekrZ+Uo9qJEUf0zGZDw==",
- "license": "MIT",
+ "version": "2.3.28",
+ "resolved": "https://registry.npmjs.org/@heroui/popover/-/popover-2.3.28.tgz",
+ "integrity": "sha512-0KHClVQVhLTCqUOtsKEZQ3dqPpNjd7qTISD2Ud3vACdLXprSLWmOzo2ItT6PAh881oIZnPS8l/0/jZ1ON/izdA==",
"dependencies": {
- "@heroui/aria-utils": "2.2.24",
- "@heroui/button": "2.2.27",
+ "@heroui/aria-utils": "2.2.25",
+ "@heroui/button": "2.2.28",
"@heroui/dom-animation": "2.1.10",
- "@heroui/framer-utils": "2.1.23",
+ "@heroui/framer-utils": "2.1.24",
"@heroui/react-utils": "2.1.14",
"@heroui/shared-utils": "2.1.12",
"@heroui/use-aria-button": "2.2.20",
@@ -2072,17 +2008,16 @@
},
"peerDependencies": {
"@heroui/system": ">=2.4.18",
- "@heroui/theme": ">=2.4.17",
+ "@heroui/theme": ">=2.4.23",
"framer-motion": ">=11.5.6 || >=12.0.0-alpha.1",
"react": ">=18 || >=19.0.0-rc.0",
"react-dom": ">=18 || >=19.0.0-rc.0"
}
},
"node_modules/@heroui/progress": {
- "version": "2.2.22",
- "resolved": "https://registry.npmjs.org/@heroui/progress/-/progress-2.2.22.tgz",
- "integrity": "sha512-ch+iWEDo8d+Owz81vu4+Kj6CLfxi0nUlivQBhXeOzgU3VZbRmxJyW8S6l7wk6GyKJZxsCbYbjV1wPSjZhKJXCg==",
- "license": "MIT",
+ "version": "2.2.23",
+ "resolved": "https://registry.npmjs.org/@heroui/progress/-/progress-2.2.23.tgz",
+ "integrity": "sha512-5mfFPv5oW69yD5m/Y1cz0R+s4W8cwvLCZXzVtevoqyzkInNks8w2FKeGptkXcDeXVxqfhwDmNU4DXUmc4nRx3w==",
"dependencies": {
"@heroui/react-utils": "2.1.14",
"@heroui/shared-utils": "2.1.12",
@@ -2092,18 +2027,17 @@
},
"peerDependencies": {
"@heroui/system": ">=2.4.18",
- "@heroui/theme": ">=2.4.17",
+ "@heroui/theme": ">=2.4.23",
"react": ">=18 || >=19.0.0-rc.0",
"react-dom": ">=18 || >=19.0.0-rc.0"
}
},
"node_modules/@heroui/radio": {
- "version": "2.3.27",
- "resolved": "https://registry.npmjs.org/@heroui/radio/-/radio-2.3.27.tgz",
- "integrity": "sha512-kfDxzPR0u4++lZX2Gf6wbEe/hGbFnoXI4XLbe4e+ZDjGdBSakNuJlcDvWHVoDFZH1xXyOO9w/dHfZuE6O2VGLA==",
- "license": "MIT",
+ "version": "2.3.28",
+ "resolved": "https://registry.npmjs.org/@heroui/radio/-/radio-2.3.28.tgz",
+ "integrity": "sha512-qrzZpEXRl4EH3zKeCujyKeK2yvcvaOaosxdZnMrT2O7wxX9LeOp6ZPMwIdMFmJYj7iyPym2nUwFfQBne7JNuvA==",
"dependencies": {
- "@heroui/form": "2.1.27",
+ "@heroui/form": "2.1.28",
"@heroui/react-utils": "2.1.14",
"@heroui/shared-utils": "2.1.12",
"@react-aria/focus": "3.21.2",
@@ -2116,66 +2050,65 @@
},
"peerDependencies": {
"@heroui/system": ">=2.4.18",
- "@heroui/theme": ">=2.4.17",
+ "@heroui/theme": ">=2.4.23",
"react": ">=18 || >=19.0.0-rc.0",
"react-dom": ">=18 || >=19.0.0-rc.0"
}
},
"node_modules/@heroui/react": {
- "version": "2.8.5",
- "resolved": "https://registry.npmjs.org/@heroui/react/-/react-2.8.5.tgz",
- "integrity": "sha512-cGiG0/DCPsYopa+zACFDmtx9LQDfY5KU58Tt82ELANhmKRyYAesAq9tSa01dG+MjOXUTUR6cxp5i5RmRn8rPYg==",
- "license": "MIT",
+ "version": "2.8.6",
+ "resolved": "https://registry.npmjs.org/@heroui/react/-/react-2.8.6.tgz",
+ "integrity": "sha512-iDmmkqZZtBqVqsSSZiV6PIWN3AEOZLQFXwt9Lob2Oy7gQQuFDP+iljg/ARc3fZ9JBNbJTfgGFdNjrnaFpPtRyw==",
"dependencies": {
- "@heroui/accordion": "2.2.24",
- "@heroui/alert": "2.2.27",
- "@heroui/autocomplete": "2.3.29",
- "@heroui/avatar": "2.2.22",
- "@heroui/badge": "2.2.17",
- "@heroui/breadcrumbs": "2.2.22",
- "@heroui/button": "2.2.27",
- "@heroui/calendar": "2.2.27",
- "@heroui/card": "2.2.25",
- "@heroui/checkbox": "2.3.27",
- "@heroui/chip": "2.2.22",
- "@heroui/code": "2.2.21",
- "@heroui/date-input": "2.3.27",
- "@heroui/date-picker": "2.3.28",
- "@heroui/divider": "2.2.20",
- "@heroui/drawer": "2.2.24",
- "@heroui/dropdown": "2.3.27",
- "@heroui/form": "2.1.27",
- "@heroui/framer-utils": "2.1.23",
- "@heroui/image": "2.2.17",
- "@heroui/input": "2.4.28",
- "@heroui/input-otp": "2.1.27",
- "@heroui/kbd": "2.2.22",
- "@heroui/link": "2.2.23",
- "@heroui/listbox": "2.3.26",
- "@heroui/menu": "2.2.26",
- "@heroui/modal": "2.2.24",
- "@heroui/navbar": "2.2.25",
- "@heroui/number-input": "2.0.18",
- "@heroui/pagination": "2.2.24",
- "@heroui/popover": "2.3.27",
- "@heroui/progress": "2.2.22",
- "@heroui/radio": "2.3.27",
- "@heroui/ripple": "2.2.20",
- "@heroui/scroll-shadow": "2.3.18",
- "@heroui/select": "2.4.28",
- "@heroui/skeleton": "2.2.17",
- "@heroui/slider": "2.4.24",
- "@heroui/snippet": "2.2.28",
- "@heroui/spacer": "2.2.21",
- "@heroui/spinner": "2.2.24",
- "@heroui/switch": "2.2.24",
- "@heroui/system": "2.4.23",
- "@heroui/table": "2.2.27",
- "@heroui/tabs": "2.2.24",
- "@heroui/theme": "2.4.23",
- "@heroui/toast": "2.0.17",
- "@heroui/tooltip": "2.2.24",
- "@heroui/user": "2.2.22",
+ "@heroui/accordion": "2.2.25",
+ "@heroui/alert": "2.2.28",
+ "@heroui/autocomplete": "2.3.30",
+ "@heroui/avatar": "2.2.23",
+ "@heroui/badge": "2.2.18",
+ "@heroui/breadcrumbs": "2.2.23",
+ "@heroui/button": "2.2.28",
+ "@heroui/calendar": "2.2.28",
+ "@heroui/card": "2.2.26",
+ "@heroui/checkbox": "2.3.28",
+ "@heroui/chip": "2.2.23",
+ "@heroui/code": "2.2.22",
+ "@heroui/date-input": "2.3.28",
+ "@heroui/date-picker": "2.3.29",
+ "@heroui/divider": "2.2.21",
+ "@heroui/drawer": "2.2.25",
+ "@heroui/dropdown": "2.3.28",
+ "@heroui/form": "2.1.28",
+ "@heroui/framer-utils": "2.1.24",
+ "@heroui/image": "2.2.18",
+ "@heroui/input": "2.4.29",
+ "@heroui/input-otp": "2.1.28",
+ "@heroui/kbd": "2.2.23",
+ "@heroui/link": "2.2.24",
+ "@heroui/listbox": "2.3.27",
+ "@heroui/menu": "2.2.27",
+ "@heroui/modal": "2.2.25",
+ "@heroui/navbar": "2.2.26",
+ "@heroui/number-input": "2.0.19",
+ "@heroui/pagination": "2.2.25",
+ "@heroui/popover": "2.3.28",
+ "@heroui/progress": "2.2.23",
+ "@heroui/radio": "2.3.28",
+ "@heroui/ripple": "2.2.21",
+ "@heroui/scroll-shadow": "2.3.19",
+ "@heroui/select": "2.4.29",
+ "@heroui/skeleton": "2.2.18",
+ "@heroui/slider": "2.4.25",
+ "@heroui/snippet": "2.2.29",
+ "@heroui/spacer": "2.2.22",
+ "@heroui/spinner": "2.2.25",
+ "@heroui/switch": "2.2.25",
+ "@heroui/system": "2.4.24",
+ "@heroui/table": "2.2.28",
+ "@heroui/tabs": "2.2.25",
+ "@heroui/theme": "2.4.24",
+ "@heroui/toast": "2.0.18",
+ "@heroui/tooltip": "2.2.25",
+ "@heroui/user": "2.2.23",
"@react-aria/visually-hidden": "3.8.28"
},
"peerDependencies": {
@@ -2188,7 +2121,6 @@
"version": "2.1.9",
"resolved": "https://registry.npmjs.org/@heroui/react-rsc-utils/-/react-rsc-utils-2.1.9.tgz",
"integrity": "sha512-e77OEjNCmQxE9/pnLDDb93qWkX58/CcgIqdNAczT/zUP+a48NxGq2A2WRimvc1uviwaNL2StriE2DmyZPyYW7Q==",
- "license": "MIT",
"peerDependencies": {
"react": ">=18 || >=19.0.0-rc.0"
}
@@ -2197,7 +2129,6 @@
"version": "2.1.14",
"resolved": "https://registry.npmjs.org/@heroui/react-utils/-/react-utils-2.1.14.tgz",
"integrity": "sha512-hhKklYKy9sRH52C9A8P0jWQ79W4MkIvOnKBIuxEMHhigjfracy0o0lMnAUdEsJni4oZKVJYqNGdQl+UVgcmeDA==",
- "license": "MIT",
"dependencies": {
"@heroui/react-rsc-utils": "2.1.9",
"@heroui/shared-utils": "2.1.12"
@@ -2207,27 +2138,25 @@
}
},
"node_modules/@heroui/ripple": {
- "version": "2.2.20",
- "resolved": "https://registry.npmjs.org/@heroui/ripple/-/ripple-2.2.20.tgz",
- "integrity": "sha512-3+fBx5jO7l8SE84ZG0vB5BOxKKr23Ay180AeIWcf8m8lhXXd4iShVz2S+keW9PewqVHv52YBaxLoSVQ93Ddcxw==",
- "license": "MIT",
+ "version": "2.2.21",
+ "resolved": "https://registry.npmjs.org/@heroui/ripple/-/ripple-2.2.21.tgz",
+ "integrity": "sha512-wairSq9LnhbIqTCJmUlJAQURQ1wcRK/L8pjg2s3R/XnvZlPXHy4ZzfphiwIlTI21z/f6tH3arxv/g1uXd1RY0g==",
"dependencies": {
"@heroui/dom-animation": "2.1.10",
"@heroui/shared-utils": "2.1.12"
},
"peerDependencies": {
"@heroui/system": ">=2.4.18",
- "@heroui/theme": ">=2.4.17",
+ "@heroui/theme": ">=2.4.23",
"framer-motion": ">=11.5.6 || >=12.0.0-alpha.1",
"react": ">=18 || >=19.0.0-rc.0",
"react-dom": ">=18 || >=19.0.0-rc.0"
}
},
"node_modules/@heroui/scroll-shadow": {
- "version": "2.3.18",
- "resolved": "https://registry.npmjs.org/@heroui/scroll-shadow/-/scroll-shadow-2.3.18.tgz",
- "integrity": "sha512-P/nLQbFPOlbTLRjO2tKoZCljJtU7iq81wsp7C8wZ1rZI1RmkTx3UgLLeoFWgmAp3ZlUIYgaewTnejt6eRx+28w==",
- "license": "MIT",
+ "version": "2.3.19",
+ "resolved": "https://registry.npmjs.org/@heroui/scroll-shadow/-/scroll-shadow-2.3.19.tgz",
+ "integrity": "sha512-y5mdBlhiITVrFnQTDqEphYj7p5pHqoFSFtVuRRvl9wUec2lMxEpD85uMGsfL8OgQTKIAqGh2s6M360+VJm7ajQ==",
"dependencies": {
"@heroui/react-utils": "2.1.14",
"@heroui/shared-utils": "2.1.12",
@@ -2235,26 +2164,25 @@
},
"peerDependencies": {
"@heroui/system": ">=2.4.18",
- "@heroui/theme": ">=2.4.17",
+ "@heroui/theme": ">=2.4.23",
"react": ">=18 || >=19.0.0-rc.0",
"react-dom": ">=18 || >=19.0.0-rc.0"
}
},
"node_modules/@heroui/select": {
- "version": "2.4.28",
- "resolved": "https://registry.npmjs.org/@heroui/select/-/select-2.4.28.tgz",
- "integrity": "sha512-Dg3jv248Tu+g2WJMWseDjWA0FAG356elZIcE0OufVAIzQoWjLhgbkTqY9ths0HkcHy0nDwQWvyrrwkbif1kNqA==",
- "license": "MIT",
+ "version": "2.4.29",
+ "resolved": "https://registry.npmjs.org/@heroui/select/-/select-2.4.29.tgz",
+ "integrity": "sha512-rFsI+UNUtK6WTm6oDM8A45tu8rDqt1zHoSoBQ8RJDkRITDcKRBTaTnvJI/Ez+kMRNH4fQ45LgoSPxw/JOOMg4w==",
"dependencies": {
- "@heroui/aria-utils": "2.2.24",
- "@heroui/form": "2.1.27",
- "@heroui/listbox": "2.3.26",
- "@heroui/popover": "2.3.27",
+ "@heroui/aria-utils": "2.2.25",
+ "@heroui/form": "2.1.28",
+ "@heroui/listbox": "2.3.27",
+ "@heroui/popover": "2.3.28",
"@heroui/react-utils": "2.1.14",
- "@heroui/scroll-shadow": "2.3.18",
+ "@heroui/scroll-shadow": "2.3.19",
"@heroui/shared-icons": "2.1.10",
"@heroui/shared-utils": "2.1.12",
- "@heroui/spinner": "2.2.24",
+ "@heroui/spinner": "2.2.25",
"@heroui/use-aria-button": "2.2.20",
"@heroui/use-aria-multiselect": "2.4.19",
"@heroui/use-form-reset": "2.0.1",
@@ -2268,7 +2196,7 @@
},
"peerDependencies": {
"@heroui/system": ">=2.4.18",
- "@heroui/theme": ">=2.4.17",
+ "@heroui/theme": ">=2.4.23",
"framer-motion": ">=11.5.6 || >=12.0.0-alpha.1",
"react": ">=18 || >=19.0.0-rc.0",
"react-dom": ">=18 || >=19.0.0-rc.0"
@@ -2278,7 +2206,6 @@
"version": "2.1.10",
"resolved": "https://registry.npmjs.org/@heroui/shared-icons/-/shared-icons-2.1.10.tgz",
"integrity": "sha512-ePo60GjEpM0SEyZBGOeySsLueNDCqLsVL79Fq+5BphzlrBAcaKY7kUp74964ImtkXvknTxAWzuuTr3kCRqj6jg==",
- "license": "MIT",
"peerDependencies": {
"react": ">=18 || >=19.0.0-rc.0"
}
@@ -2287,33 +2214,30 @@
"version": "2.1.12",
"resolved": "https://registry.npmjs.org/@heroui/shared-utils/-/shared-utils-2.1.12.tgz",
"integrity": "sha512-0iCnxVAkIPtrHQo26Qa5g0UTqMTpugTbClNOrEPsrQuyRAq7Syux998cPwGlneTfB5E5xcU3LiEdA9GUyeK2cQ==",
- "hasInstallScript": true,
- "license": "MIT"
+ "hasInstallScript": true
},
"node_modules/@heroui/skeleton": {
- "version": "2.2.17",
- "resolved": "https://registry.npmjs.org/@heroui/skeleton/-/skeleton-2.2.17.tgz",
- "integrity": "sha512-WDzwODs+jW+GgMr3oOdLtXXfv8ScXuuWgxN2iPWWyDBcQYXX2XCKGVjCpM5lSKf1UG4Yp3iXuqKzH1m+E+m7kg==",
- "license": "MIT",
+ "version": "2.2.18",
+ "resolved": "https://registry.npmjs.org/@heroui/skeleton/-/skeleton-2.2.18.tgz",
+ "integrity": "sha512-7AjU5kjk9rqrKP9mWQiAVj0dow4/vbK5/ejh4jqdb3DZm7bM2+DGzfnQPiS0c2eWR606CgOuuoImpwDS82HJtA==",
"dependencies": {
"@heroui/shared-utils": "2.1.12"
},
"peerDependencies": {
"@heroui/system": ">=2.4.18",
- "@heroui/theme": ">=2.4.17",
+ "@heroui/theme": ">=2.4.23",
"react": ">=18 || >=19.0.0-rc.0",
"react-dom": ">=18 || >=19.0.0-rc.0"
}
},
"node_modules/@heroui/slider": {
- "version": "2.4.24",
- "resolved": "https://registry.npmjs.org/@heroui/slider/-/slider-2.4.24.tgz",
- "integrity": "sha512-GKdqFTCe9O8tT3HEZ/W4TEWkz7ADtUBzuOBXw779Oqqf02HNg9vSnISlNvI6G0ymYjY42EanwA+dChHbPBIVJw==",
- "license": "MIT",
+ "version": "2.4.25",
+ "resolved": "https://registry.npmjs.org/@heroui/slider/-/slider-2.4.25.tgz",
+ "integrity": "sha512-1ULgaqsu1Vzyyx6S7TGs+13PX5BGArZhLiApQfKwiA3TFvT0MNzTVoWVgyFZ8XLqh4esSUnqddhivqQhbRzrHw==",
"dependencies": {
"@heroui/react-utils": "2.1.14",
"@heroui/shared-utils": "2.1.12",
- "@heroui/tooltip": "2.2.24",
+ "@heroui/tooltip": "2.2.25",
"@react-aria/focus": "3.21.2",
"@react-aria/i18n": "3.12.13",
"@react-aria/interactions": "3.25.6",
@@ -2323,70 +2247,66 @@
},
"peerDependencies": {
"@heroui/system": ">=2.4.18",
- "@heroui/theme": ">=2.4.19",
+ "@heroui/theme": ">=2.4.23",
"react": ">=18 || >=19.0.0-rc.0",
"react-dom": ">=18 || >=19.0.0-rc.0"
}
},
"node_modules/@heroui/snippet": {
- "version": "2.2.28",
- "resolved": "https://registry.npmjs.org/@heroui/snippet/-/snippet-2.2.28.tgz",
- "integrity": "sha512-UfC/ZcYpmOutAcazxkizJWlhvqzr077szDyQ85thyUC5yhuRRLrsOHDIhyLWQrEKIcWw5+CaEGS2VLwAFlgfzw==",
- "license": "MIT",
+ "version": "2.2.29",
+ "resolved": "https://registry.npmjs.org/@heroui/snippet/-/snippet-2.2.29.tgz",
+ "integrity": "sha512-RuyK/DldxvVYb6ToPk5cNNYeDkL+phKZPYHrUxBJK/PzuAkqi3AzQV7zHd+3IfTNxQbevRjzCXENE5F3GKP/MQ==",
"dependencies": {
- "@heroui/button": "2.2.27",
+ "@heroui/button": "2.2.28",
"@heroui/react-utils": "2.1.14",
"@heroui/shared-icons": "2.1.10",
"@heroui/shared-utils": "2.1.12",
- "@heroui/tooltip": "2.2.24",
+ "@heroui/tooltip": "2.2.25",
"@heroui/use-clipboard": "2.1.9",
"@react-aria/focus": "3.21.2"
},
"peerDependencies": {
"@heroui/system": ">=2.4.18",
- "@heroui/theme": ">=2.4.17",
+ "@heroui/theme": ">=2.4.23",
"framer-motion": ">=11.5.6 || >=12.0.0-alpha.1",
"react": ">=18 || >=19.0.0-rc.0",
"react-dom": ">=18 || >=19.0.0-rc.0"
}
},
"node_modules/@heroui/spacer": {
- "version": "2.2.21",
- "resolved": "https://registry.npmjs.org/@heroui/spacer/-/spacer-2.2.21.tgz",
- "integrity": "sha512-WKD+BlgHfqJ8lrkkg/6cvzSWNsbRjzr24HpZnv6cDeWX95wVLTOco9HVR8ohwStMqwu5zYeUd1bw6yCDVTo53w==",
- "license": "MIT",
+ "version": "2.2.22",
+ "resolved": "https://registry.npmjs.org/@heroui/spacer/-/spacer-2.2.22.tgz",
+ "integrity": "sha512-BJ7RauvSY3gx10ntqZkCcyTy9K2FS4AeeryQUE9RgkMKQxP4t5TbeYLPEyomjWK+cCL/ERQCCruW16D3vKyWmw==",
"dependencies": {
"@heroui/react-utils": "2.1.14",
"@heroui/shared-utils": "2.1.12",
- "@heroui/system-rsc": "2.3.20"
+ "@heroui/system-rsc": "2.3.21"
},
"peerDependencies": {
- "@heroui/theme": ">=2.4.17",
+ "@heroui/theme": ">=2.4.23",
"react": ">=18 || >=19.0.0-rc.0",
"react-dom": ">=18 || >=19.0.0-rc.0"
}
},
"node_modules/@heroui/spinner": {
- "version": "2.2.24",
- "resolved": "https://registry.npmjs.org/@heroui/spinner/-/spinner-2.2.24.tgz",
- "integrity": "sha512-HfKkFffrIN9UdJY2UaenlB8xEwIzolCCFCwU0j3wVnLMX+Dw+ixwaELdAxX14Z6gPQYec6AROKetkWWit14rlw==",
- "license": "MIT",
+ "version": "2.2.25",
+ "resolved": "https://registry.npmjs.org/@heroui/spinner/-/spinner-2.2.25.tgz",
+ "integrity": "sha512-zDuLJicUL51vGLEBbHWy/t6DlOvs9YILM4YLmzS/o84ExTgfrCycXNs6JkoteFiNu570qqZMeAA2aYneGfl/PQ==",
"dependencies": {
"@heroui/shared-utils": "2.1.12",
- "@heroui/system": "2.4.23",
- "@heroui/system-rsc": "2.3.20"
+ "@heroui/system": "2.4.24",
+ "@heroui/system-rsc": "2.3.21"
},
"peerDependencies": {
- "@heroui/theme": ">=2.4.17",
+ "@heroui/theme": ">=2.4.23",
"react": ">=18 || >=19.0.0-rc.0",
"react-dom": ">=18 || >=19.0.0-rc.0"
}
},
"node_modules/@heroui/switch": {
- "version": "2.2.24",
- "resolved": "https://registry.npmjs.org/@heroui/switch/-/switch-2.2.24.tgz",
- "integrity": "sha512-RbV+MECncBKsthX3D8r+CGoQRu8Q3AAYUEdm/7ody6+bMZFmBilm695yLiqziMI33Ct/WQ0WkpvrTClIcmxU/A==",
- "license": "MIT",
+ "version": "2.2.25",
+ "resolved": "https://registry.npmjs.org/@heroui/switch/-/switch-2.2.25.tgz",
+ "integrity": "sha512-F0Yj+kgVfD2bdy6REFvNySeGuYg1OT2phwMPwSZGUl7ZFeGSvvWSnbYS4/wS3JIM5PyEibSaB8QIPc8r00xq1A==",
"dependencies": {
"@heroui/react-utils": "2.1.14",
"@heroui/shared-utils": "2.1.12",
@@ -2399,19 +2319,18 @@
},
"peerDependencies": {
"@heroui/system": ">=2.4.18",
- "@heroui/theme": ">=2.4.17",
+ "@heroui/theme": ">=2.4.23",
"react": ">=18 || >=19.0.0-rc.0",
"react-dom": ">=18 || >=19.0.0-rc.0"
}
},
"node_modules/@heroui/system": {
- "version": "2.4.23",
- "resolved": "https://registry.npmjs.org/@heroui/system/-/system-2.4.23.tgz",
- "integrity": "sha512-kgYvfkIOQKM6CCBIlNSE2tXMtNrS1mvEUbvwnaU3pEYbMlceBtwA5v7SlpaJy/5dqKcTbfmVMUCmXnY/Kw4vaQ==",
- "license": "MIT",
+ "version": "2.4.24",
+ "resolved": "https://registry.npmjs.org/@heroui/system/-/system-2.4.24.tgz",
+ "integrity": "sha512-9GKQgUc91otQfwmq6TLE72QKxtB341aK5NpBHS3gRoWYEuNN714Zl3OXwIZNvdXPJpsTaUo1ID1ibJU9tfgwdg==",
"dependencies": {
"@heroui/react-utils": "2.1.14",
- "@heroui/system-rsc": "2.3.20",
+ "@heroui/system-rsc": "2.3.21",
"@react-aria/i18n": "3.12.13",
"@react-aria/overlays": "3.30.0",
"@react-aria/utils": "3.31.0"
@@ -2423,39 +2342,26 @@
}
},
"node_modules/@heroui/system-rsc": {
- "version": "2.3.20",
- "resolved": "https://registry.npmjs.org/@heroui/system-rsc/-/system-rsc-2.3.20.tgz",
- "integrity": "sha512-uZwQErEud/lAX7KRXEdsDcGLyygBffHcgnbCDrLvmTf3cyBE84YziG7AjM7Ts8ZcrF+wBXX4+a1IqnKGlsGEdQ==",
- "license": "MIT",
+ "version": "2.3.21",
+ "resolved": "https://registry.npmjs.org/@heroui/system-rsc/-/system-rsc-2.3.21.tgz",
+ "integrity": "sha512-icB7njbNgkI3dcfZhY5LP7VFspaVgWL1lcg9Q7uJMAaj6gGFqqSSnHkSMwpR9AGLxVRKTHey0TUx8CeZDe8XDw==",
"dependencies": {
- "@react-types/shared": "3.32.1",
- "clsx": "^1.2.1"
+ "@react-types/shared": "3.32.1"
},
"peerDependencies": {
- "@heroui/theme": ">=2.4.17",
+ "@heroui/theme": ">=2.4.23",
"react": ">=18 || >=19.0.0-rc.0"
}
},
- "node_modules/@heroui/system-rsc/node_modules/clsx": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
- "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==",
- "license": "MIT",
- "engines": {
- "node": ">=6"
- }
- },
"node_modules/@heroui/table": {
- "version": "2.2.27",
- "resolved": "https://registry.npmjs.org/@heroui/table/-/table-2.2.27.tgz",
- "integrity": "sha512-XFmbEgBzf89WH1VzmnwENxVzK4JrHV5jdlzyM3snNhk8uDSjfecnUY33qR62cpdZsKiCFFcYf7kQPkCnJGnD0Q==",
- "license": "MIT",
+ "version": "2.2.28",
+ "resolved": "https://registry.npmjs.org/@heroui/table/-/table-2.2.28.tgz",
+ "integrity": "sha512-0z3xs0kxDXvvd9gy/uHgvK0/bmpJF0m9t3omNMnB0I0EUx+gJ/CnaaPiF9M5veg/128rc45J7X2FgY3fPAKcmA==",
"dependencies": {
- "@heroui/checkbox": "2.3.27",
+ "@heroui/checkbox": "2.3.28",
"@heroui/react-utils": "2.1.14",
"@heroui/shared-icons": "2.1.10",
"@heroui/shared-utils": "2.1.12",
- "@heroui/spacer": "2.2.21",
"@react-aria/focus": "3.21.2",
"@react-aria/interactions": "3.25.6",
"@react-aria/table": "3.17.8",
@@ -2468,18 +2374,17 @@
},
"peerDependencies": {
"@heroui/system": ">=2.4.18",
- "@heroui/theme": ">=2.4.17",
+ "@heroui/theme": ">=2.4.23",
"react": ">=18 || >=19.0.0-rc.0",
"react-dom": ">=18 || >=19.0.0-rc.0"
}
},
"node_modules/@heroui/tabs": {
- "version": "2.2.24",
- "resolved": "https://registry.npmjs.org/@heroui/tabs/-/tabs-2.2.24.tgz",
- "integrity": "sha512-2SfxzAXe1t2Zz0v16kqkb7DR2wW86XoDwRUpLex6zhEN4/uT5ILeynxIVSUyAvVN3z95cnaQt0XPQBfUjAIQhQ==",
- "license": "MIT",
+ "version": "2.2.25",
+ "resolved": "https://registry.npmjs.org/@heroui/tabs/-/tabs-2.2.25.tgz",
+ "integrity": "sha512-bIpz/8TTNMabmzObN2zs+3WhQXbKyr9tZUPkk3rMQxIshpg9oyyEWOS8XiMBxrEzSByLfPNypl5sX1au6Dw2Ew==",
"dependencies": {
- "@heroui/aria-utils": "2.2.24",
+ "@heroui/aria-utils": "2.2.25",
"@heroui/react-utils": "2.1.14",
"@heroui/shared-utils": "2.1.12",
"@heroui/use-is-mounted": "2.1.8",
@@ -2492,60 +2397,38 @@
},
"peerDependencies": {
"@heroui/system": ">=2.4.18",
- "@heroui/theme": ">=2.4.22",
+ "@heroui/theme": ">=2.4.23",
"framer-motion": ">=11.5.6 || >=12.0.0-alpha.1",
"react": ">=18 || >=19.0.0-rc.0",
"react-dom": ">=18 || >=19.0.0-rc.0"
}
},
"node_modules/@heroui/theme": {
- "version": "2.4.23",
- "resolved": "https://registry.npmjs.org/@heroui/theme/-/theme-2.4.23.tgz",
- "integrity": "sha512-5hoaRWG+/d/t06p7Pfhz70DUP0Uggjids7/z2Ytgup4A8KAOvDIXxvHUDlk6rRHKiN1wDMNA5H+EWsSXB/m03Q==",
- "license": "MIT",
+ "version": "2.4.24",
+ "resolved": "https://registry.npmjs.org/@heroui/theme/-/theme-2.4.24.tgz",
+ "integrity": "sha512-lL+anmY4GGWwKyTbJ2PEBZE4talIZ3hu4yGpku9TktCVG2nC2YTwiWQFJ+Jcbf8Cf9vuLzI1sla5bz2jUqiBRA==",
"dependencies": {
"@heroui/shared-utils": "2.1.12",
- "clsx": "^1.2.1",
"color": "^4.2.3",
"color2k": "^2.0.3",
"deepmerge": "4.3.1",
"flat": "^5.0.2",
- "tailwind-merge": "3.3.1",
- "tailwind-variants": "3.1.1"
+ "tailwind-merge": "3.4.0",
+ "tailwind-variants": "3.2.2"
},
"peerDependencies": {
"tailwindcss": ">=4.0.0"
}
},
- "node_modules/@heroui/theme/node_modules/clsx": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
- "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==",
- "license": "MIT",
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/@heroui/theme/node_modules/tailwind-merge": {
- "version": "3.3.1",
- "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.1.tgz",
- "integrity": "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==",
- "license": "MIT",
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/dcastil"
- }
- },
"node_modules/@heroui/toast": {
- "version": "2.0.17",
- "resolved": "https://registry.npmjs.org/@heroui/toast/-/toast-2.0.17.tgz",
- "integrity": "sha512-w3TaA1DYLcwdDjpwf9xw5YSr+odo9GGHsObsrMmLEQDS0JQhmKyK5sQqXUzb9d27EC6KVwGjeVg0hUHYQBK2JA==",
- "license": "MIT",
+ "version": "2.0.18",
+ "resolved": "https://registry.npmjs.org/@heroui/toast/-/toast-2.0.18.tgz",
+ "integrity": "sha512-5IoqEq10W/AaUgKWKIR7bbTB6U+rHMkikzGwW+IndsvFLR3meyb5l4K5cmVCmDsMHubUaRa9UFDeAokyNXvpWA==",
"dependencies": {
"@heroui/react-utils": "2.1.14",
"@heroui/shared-icons": "2.1.10",
"@heroui/shared-utils": "2.1.12",
- "@heroui/spinner": "2.2.24",
+ "@heroui/spinner": "2.2.25",
"@heroui/use-is-mobile": "2.2.12",
"@react-aria/interactions": "3.25.6",
"@react-aria/toast": "3.0.8",
@@ -2553,21 +2436,20 @@
},
"peerDependencies": {
"@heroui/system": ">=2.4.18",
- "@heroui/theme": ">=2.4.17",
+ "@heroui/theme": ">=2.4.23",
"framer-motion": ">=11.5.6 || >=12.0.0-alpha.1",
"react": ">=18 || >=19.0.0-rc.0",
"react-dom": ">=18 || >=19.0.0-rc.0"
}
},
"node_modules/@heroui/tooltip": {
- "version": "2.2.24",
- "resolved": "https://registry.npmjs.org/@heroui/tooltip/-/tooltip-2.2.24.tgz",
- "integrity": "sha512-H+0STFea2/Z4obDdk+ZPoDzJxJQHIWGSjnW/jieThJbJ5zow/qBfcg5DqzIdiC+FCJ4dDD5jEDZ4W4H/fQUKQA==",
- "license": "MIT",
+ "version": "2.2.25",
+ "resolved": "https://registry.npmjs.org/@heroui/tooltip/-/tooltip-2.2.25.tgz",
+ "integrity": "sha512-f+WxkQy0YBzzE6VhzVgA/CeD7nvo0hhOapx0UScU8zsQ1J+n5Kr5YY/7CgMHmFLyC/Amrqlf7WSgljRl4iWivQ==",
"dependencies": {
- "@heroui/aria-utils": "2.2.24",
+ "@heroui/aria-utils": "2.2.25",
"@heroui/dom-animation": "2.1.10",
- "@heroui/framer-utils": "2.1.23",
+ "@heroui/framer-utils": "2.1.24",
"@heroui/react-utils": "2.1.14",
"@heroui/shared-utils": "2.1.12",
"@heroui/use-aria-overlay": "2.0.4",
@@ -2580,7 +2462,7 @@
},
"peerDependencies": {
"@heroui/system": ">=2.4.18",
- "@heroui/theme": ">=2.4.17",
+ "@heroui/theme": ">=2.4.23",
"framer-motion": ">=11.5.6 || >=12.0.0-alpha.1",
"react": ">=18 || >=19.0.0-rc.0",
"react-dom": ">=18 || >=19.0.0-rc.0"
@@ -2590,7 +2472,6 @@
"version": "2.2.18",
"resolved": "https://registry.npmjs.org/@heroui/use-aria-accordion/-/use-aria-accordion-2.2.18.tgz",
"integrity": "sha512-qjRkae2p4MFDrNqO6v6YCor0BtVi3idMd1dsI82XM16bxLQ2stqG4Ajrg60xV0AN+WKZUq10oetqkJuY6MYg0w==",
- "license": "MIT",
"dependencies": {
"@react-aria/button": "3.14.2",
"@react-aria/focus": "3.21.2",
@@ -2607,7 +2488,6 @@
"version": "2.2.20",
"resolved": "https://registry.npmjs.org/@heroui/use-aria-button/-/use-aria-button-2.2.20.tgz",
"integrity": "sha512-Y0Bmze/pxEACKsHMbA1sYA3ghMJ+9fSnWvZBwlUxqiVXDEy2YrrK2JmXEgsuHGQdKD9RqU2Od3V4VqIIiaHiMA==",
- "license": "MIT",
"dependencies": {
"@react-aria/focus": "3.21.2",
"@react-aria/interactions": "3.25.6",
@@ -2623,7 +2503,6 @@
"version": "2.2.21",
"resolved": "https://registry.npmjs.org/@heroui/use-aria-link/-/use-aria-link-2.2.21.tgz",
"integrity": "sha512-sG2rUutT/E/FYguzZmg715cXcM6+ue9wRfs2Gi6epWJwIVpS51uEagJKY0wIutJDfuCPfQ9AuxXfJek4CnxjKw==",
- "license": "MIT",
"dependencies": {
"@react-aria/focus": "3.21.2",
"@react-aria/interactions": "3.25.6",
@@ -2639,7 +2518,6 @@
"version": "2.2.19",
"resolved": "https://registry.npmjs.org/@heroui/use-aria-modal-overlay/-/use-aria-modal-overlay-2.2.19.tgz",
"integrity": "sha512-MPvszNrt+1DauiSyOAwb0pKbYahpEVi9hrmidnO8cd1SA7B2ES0fNRBeNMAwcaeR/Nzsv+Cw1hRXt3egwqi0lg==",
- "license": "MIT",
"dependencies": {
"@heroui/use-aria-overlay": "2.0.4",
"@react-aria/overlays": "3.30.0",
@@ -2655,7 +2533,6 @@
"version": "2.4.19",
"resolved": "https://registry.npmjs.org/@heroui/use-aria-multiselect/-/use-aria-multiselect-2.4.19.tgz",
"integrity": "sha512-RLDSpOLJqNESn6OK/zKuyTriK6sqMby76si/4kTMCs+4lmMPOyFKP3fREywu+zyJjRUCuZPa6xYuN2OHKQRDow==",
- "license": "MIT",
"dependencies": {
"@react-aria/i18n": "3.12.13",
"@react-aria/interactions": "3.25.6",
@@ -2680,7 +2557,6 @@
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@heroui/use-aria-overlay/-/use-aria-overlay-2.0.4.tgz",
"integrity": "sha512-iv+y0+OvQd1eWiZftPI07JE3c5AdK85W5k3rDlhk5MFEI3dllkIpu8z8zLh3ge/BQGFiGkySVC5iXl8w84gMUQ==",
- "license": "MIT",
"dependencies": {
"@react-aria/focus": "3.21.2",
"@react-aria/interactions": "3.25.6",
@@ -2696,7 +2572,6 @@
"version": "2.1.8",
"resolved": "https://registry.npmjs.org/@heroui/use-callback-ref/-/use-callback-ref-2.1.8.tgz",
"integrity": "sha512-D1JDo9YyFAprYpLID97xxQvf86NvyWLay30BeVVZT9kWmar6O9MbCRc7ACi7Ngko60beonj6+amTWkTm7QuY/Q==",
- "license": "MIT",
"dependencies": {
"@heroui/use-safe-layout-effect": "2.1.8"
},
@@ -2708,7 +2583,6 @@
"version": "2.1.9",
"resolved": "https://registry.npmjs.org/@heroui/use-clipboard/-/use-clipboard-2.1.9.tgz",
"integrity": "sha512-lkBq5RpXHiPvk1BXKJG8gMM0f7jRMIGnxAXDjAUzZyXKBuWLoM+XlaUWmZHtmkkjVFMX1L4vzA+vxi9rZbenEQ==",
- "license": "MIT",
"peerDependencies": {
"react": ">=18 || >=19.0.0-rc.0"
}
@@ -2717,7 +2591,6 @@
"version": "2.2.13",
"resolved": "https://registry.npmjs.org/@heroui/use-data-scroll-overflow/-/use-data-scroll-overflow-2.2.13.tgz",
"integrity": "sha512-zboLXO1pgYdzMUahDcVt5jf+l1jAQ/D9dFqr7AxWLfn6tn7/EgY0f6xIrgWDgJnM0U3hKxVeY13pAeB4AFTqTw==",
- "license": "MIT",
"dependencies": {
"@heroui/shared-utils": "2.1.12"
},
@@ -2729,7 +2602,6 @@
"version": "2.2.17",
"resolved": "https://registry.npmjs.org/@heroui/use-disclosure/-/use-disclosure-2.2.17.tgz",
"integrity": "sha512-S3pN0WmpcTTZuQHcXw4RcTVsxLaCZ95H5qi/JPN83ahhWTCC+pN8lwE37vSahbMTM1YriiHyTM6AWpv/E3Jq7w==",
- "license": "MIT",
"dependencies": {
"@heroui/use-callback-ref": "2.1.8",
"@react-aria/utils": "3.31.0",
@@ -2743,7 +2615,6 @@
"version": "2.1.18",
"resolved": "https://registry.npmjs.org/@heroui/use-draggable/-/use-draggable-2.1.18.tgz",
"integrity": "sha512-ihQdmLGYJ6aTEaJ0/yCXYn6VRdrRV2eO03XD2A3KANZPb1Bj/n4r298xNMql5VnGq5ZNDJB9nTv8NNCu9pmPdg==",
- "license": "MIT",
"dependencies": {
"@react-aria/interactions": "3.25.6"
},
@@ -2755,7 +2626,6 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@heroui/use-form-reset/-/use-form-reset-2.0.1.tgz",
"integrity": "sha512-6slKWiLtVfgZnVeHVkM9eXgjwI07u0CUaLt2kQpfKPqTSTGfbHgCYJFduijtThhTdKBhdH6HCmzTcnbVlAxBXw==",
- "license": "MIT",
"peerDependencies": {
"react": ">=18 || >=19.0.0-rc.0"
}
@@ -2764,7 +2634,6 @@
"version": "2.1.13",
"resolved": "https://registry.npmjs.org/@heroui/use-image/-/use-image-2.1.13.tgz",
"integrity": "sha512-NLApz+xin2bKHEXr+eSrtB0lN8geKP5VOea5QGbOCiHq4DBXu4QctpRkSfCHGIQzWdBVaLPoV+5wd0lR2S2Egg==",
- "license": "MIT",
"dependencies": {
"@heroui/react-utils": "2.1.14",
"@heroui/use-safe-layout-effect": "2.1.8"
@@ -2777,7 +2646,6 @@
"version": "2.2.14",
"resolved": "https://registry.npmjs.org/@heroui/use-intersection-observer/-/use-intersection-observer-2.2.14.tgz",
"integrity": "sha512-qYJeMk4cTsF+xIckRctazCgWQ4BVOpJu+bhhkB1NrN+MItx19Lcb7ksOqMdN5AiSf85HzDcAEPIQ9w9RBlt5sg==",
- "license": "MIT",
"peerDependencies": {
"react": ">=18 || >=19.0.0-rc.0"
}
@@ -2786,7 +2654,6 @@
"version": "2.2.12",
"resolved": "https://registry.npmjs.org/@heroui/use-is-mobile/-/use-is-mobile-2.2.12.tgz",
"integrity": "sha512-2UKa4v1xbvFwerWKoMTrg4q9ZfP9MVIVfCl1a7JuKQlXq3jcyV6z1as5bZ41pCsTOT+wUVOFnlr6rzzQwT9ZOA==",
- "license": "MIT",
"dependencies": {
"@react-aria/ssr": "3.9.10"
},
@@ -2798,7 +2665,6 @@
"version": "2.1.8",
"resolved": "https://registry.npmjs.org/@heroui/use-is-mounted/-/use-is-mounted-2.1.8.tgz",
"integrity": "sha512-DO/Th1vD4Uy8KGhd17oGlNA4wtdg91dzga+VMpmt94gSZe1WjsangFwoUBxF2uhlzwensCX9voye3kerP/lskg==",
- "license": "MIT",
"peerDependencies": {
"react": ">=18 || >=19.0.0-rc.0"
}
@@ -2807,7 +2673,6 @@
"version": "2.1.8",
"resolved": "https://registry.npmjs.org/@heroui/use-measure/-/use-measure-2.1.8.tgz",
"integrity": "sha512-GjT9tIgluqYMZWfAX6+FFdRQBqyHeuqUMGzAXMTH9kBXHU0U5C5XU2c8WFORkNDoZIg1h13h1QdV+Vy4LE1dEA==",
- "license": "MIT",
"peerDependencies": {
"react": ">=18 || >=19.0.0-rc.0"
}
@@ -2816,7 +2681,6 @@
"version": "2.2.18",
"resolved": "https://registry.npmjs.org/@heroui/use-pagination/-/use-pagination-2.2.18.tgz",
"integrity": "sha512-qm1mUe5UgV0kPZItcs/jiX/BxzdDagmcxaJkYR6DkhfMRoCuOdoJhcoh8ncbCAgHpzPESPn1VxsOcG4/Y+Jkdw==",
- "license": "MIT",
"dependencies": {
"@heroui/shared-utils": "2.1.12",
"@react-aria/i18n": "3.12.13"
@@ -2829,7 +2693,6 @@
"version": "2.1.8",
"resolved": "https://registry.npmjs.org/@heroui/use-resize/-/use-resize-2.1.8.tgz",
"integrity": "sha512-htF3DND5GmrSiMGnzRbISeKcH+BqhQ/NcsP9sBTIl7ewvFaWiDhEDiUHdJxflmJGd/c5qZq2nYQM/uluaqIkKA==",
- "license": "MIT",
"peerDependencies": {
"react": ">=18 || >=19.0.0-rc.0"
}
@@ -2838,7 +2701,6 @@
"version": "2.1.8",
"resolved": "https://registry.npmjs.org/@heroui/use-safe-layout-effect/-/use-safe-layout-effect-2.1.8.tgz",
"integrity": "sha512-wbnZxVWCYqk10XRMu0veSOiVsEnLcmGUmJiapqgaz0fF8XcpSScmqjTSoWjHIEWaHjQZ6xr+oscD761D6QJN+Q==",
- "license": "MIT",
"peerDependencies": {
"react": ">=18 || >=19.0.0-rc.0"
}
@@ -2847,7 +2709,6 @@
"version": "2.1.8",
"resolved": "https://registry.npmjs.org/@heroui/use-scroll-position/-/use-scroll-position-2.1.8.tgz",
"integrity": "sha512-NxanHKObxVfWaPpNRyBR8v7RfokxrzcHyTyQfbgQgAGYGHTMaOGkJGqF8kBzInc3zJi+F0zbX7Nb0QjUgsLNUQ==",
- "license": "MIT",
"peerDependencies": {
"react": ">=18 || >=19.0.0-rc.0"
}
@@ -2856,25 +2717,23 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@heroui/use-viewport-size/-/use-viewport-size-2.0.1.tgz",
"integrity": "sha512-blv8BEB/QdLePLWODPRzRS2eELJ2eyHbdOIADbL0KcfLzOUEg9EiuVk90hcSUDAFqYiJ3YZ5Z0up8sdPcR8Y7g==",
- "license": "MIT",
"peerDependencies": {
"react": ">=18 || >=19.0.0-rc.0"
}
},
"node_modules/@heroui/user": {
- "version": "2.2.22",
- "resolved": "https://registry.npmjs.org/@heroui/user/-/user-2.2.22.tgz",
- "integrity": "sha512-kOLxh9Bjgl/ya/f+W7/eKVO/n1GPsU5TPzwocC9+FU/+MbCOrmkevhAGGUrb259KCnp9WCv7WGRIcf8rrsreDw==",
- "license": "MIT",
+ "version": "2.2.23",
+ "resolved": "https://registry.npmjs.org/@heroui/user/-/user-2.2.23.tgz",
+ "integrity": "sha512-o/ngJ4yTD4svjYKSP3hJNwhyWLhHk5g/wjqGvH81INfpeV7wPlzpM/C6LIezGB3rZjGM9d4ozSofv6spbCKCiA==",
"dependencies": {
- "@heroui/avatar": "2.2.22",
+ "@heroui/avatar": "2.2.23",
"@heroui/react-utils": "2.1.14",
"@heroui/shared-utils": "2.1.12",
"@react-aria/focus": "3.21.2"
},
"peerDependencies": {
"@heroui/system": ">=2.4.18",
- "@heroui/theme": ">=2.4.17",
+ "@heroui/theme": ">=2.4.23",
"react": ">=18 || >=19.0.0-rc.0",
"react-dom": ">=18 || >=19.0.0-rc.0"
}
@@ -3080,7 +2939,6 @@
"version": "3.10.0",
"resolved": "https://registry.npmjs.org/@internationalized/date/-/date-3.10.0.tgz",
"integrity": "sha512-oxDR/NTEJ1k+UFVQElaNIk65E/Z83HK1z1WI3lQyhTtnNg4R5oVXaPzK3jcpKG8UHKDVuDQHzn+wsxSz8RP3aw==",
- "license": "Apache-2.0",
"dependencies": {
"@swc/helpers": "^0.5.0"
}
@@ -3089,7 +2947,6 @@
"version": "3.1.8",
"resolved": "https://registry.npmjs.org/@internationalized/message/-/message-3.1.8.tgz",
"integrity": "sha512-Rwk3j/TlYZhn3HQ6PyXUV0XP9Uv42jqZGNegt0BXlxjE6G3+LwHjbQZAGHhCnCPdaA6Tvd3ma/7QzLlLkJxAWA==",
- "license": "Apache-2.0",
"dependencies": {
"@swc/helpers": "^0.5.0",
"intl-messageformat": "^10.1.0"
@@ -3099,7 +2956,6 @@
"version": "3.6.5",
"resolved": "https://registry.npmjs.org/@internationalized/number/-/number-3.6.5.tgz",
"integrity": "sha512-6hY4Kl4HPBvtfS62asS/R22JzNNy8vi/Ssev7x6EobfCp+9QIB2hKvI2EtbdJ0VSQacxVNtqhE/NmF/NZ0gm6g==",
- "license": "Apache-2.0",
"dependencies": {
"@swc/helpers": "^0.5.0"
}
@@ -3108,7 +2964,6 @@
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/@internationalized/string/-/string-3.2.7.tgz",
"integrity": "sha512-D4OHBjrinH+PFZPvfCXvG28n2LSykWcJ7GIioQL+ok0LON15SdfoUssoHzzOUmVZLbRoREsQXVzA6r8JKsbP6A==",
- "license": "Apache-2.0",
"dependencies": {
"@swc/helpers": "^0.5.0"
}
@@ -3337,10 +3192,9 @@
"license": "MIT"
},
"node_modules/@posthog/core": {
- "version": "1.7.1",
- "resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.7.1.tgz",
- "integrity": "sha512-kjK0eFMIpKo9GXIbts8VtAknsoZ18oZorANdtuTj1CbgS28t4ZVq//HAWhnxEuXRTrtkd+SUJ6Ux3j2Af8NCuA==",
- "license": "MIT",
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.8.0.tgz",
+ "integrity": "sha512-SfmG1EdbR+2zpQccgBUxM/snCROB9WGkY7VH1r9iaoTNqoaN9IkmIEA/07cZLY4DxVP8jt6Vdfe3s84xksac1g==",
"dependencies": {
"cross-spawn": "^7.0.6"
}
@@ -3349,7 +3203,6 @@
"version": "3.5.29",
"resolved": "https://registry.npmjs.org/@react-aria/breadcrumbs/-/breadcrumbs-3.5.29.tgz",
"integrity": "sha512-rKS0dryllaZJqrr3f/EAf2liz8CBEfmL5XACj+Z1TAig6GIYe1QuA3BtkX0cV9OkMugXdX8e3cbA7nD10ORRqg==",
- "license": "Apache-2.0",
"dependencies": {
"@react-aria/i18n": "^3.12.13",
"@react-aria/link": "^3.8.6",
@@ -3367,7 +3220,6 @@
"version": "3.14.2",
"resolved": "https://registry.npmjs.org/@react-aria/button/-/button-3.14.2.tgz",
"integrity": "sha512-VbLIA+Kd6f/MDjd+TJBUg2+vNDw66pnvsj2E4RLomjI9dfBuN7d+Yo2UnsqKVyhePjCUZ6xxa2yDuD63IOSIYA==",
- "license": "Apache-2.0",
"dependencies": {
"@react-aria/interactions": "^3.25.6",
"@react-aria/toolbar": "3.0.0-beta.21",
@@ -3386,7 +3238,6 @@
"version": "3.9.2",
"resolved": "https://registry.npmjs.org/@react-aria/calendar/-/calendar-3.9.2.tgz",
"integrity": "sha512-uSLxLgOPRnEU4Jg59lAhUVA+uDx/55NBg4lpfsP2ynazyiJ5LCXmYceJi+VuOqMml7d9W0dB87OldOeLdIxYVA==",
- "license": "Apache-2.0",
"dependencies": {
"@internationalized/date": "^3.10.0",
"@react-aria/i18n": "^3.12.13",
@@ -3408,7 +3259,6 @@
"version": "3.16.2",
"resolved": "https://registry.npmjs.org/@react-aria/checkbox/-/checkbox-3.16.2.tgz",
"integrity": "sha512-29Mj9ZqXioJ0bcMnNGooHztnTau5pikZqX3qCRj5bYR3by/ZFFavYoMroh9F7s/MbFm/tsKX+Sf02lYFEdXRjA==",
- "license": "Apache-2.0",
"dependencies": {
"@react-aria/form": "^3.1.2",
"@react-aria/interactions": "^3.25.6",
@@ -3431,7 +3281,6 @@
"version": "3.14.0",
"resolved": "https://registry.npmjs.org/@react-aria/combobox/-/combobox-3.14.0.tgz",
"integrity": "sha512-z4ro0Hma//p4nL2IJx5iUa7NwxeXbzSoZ0se5uTYjG1rUUMszg+wqQh/AQoL+eiULn7rs18JY9wwNbVIkRNKWA==",
- "license": "Apache-2.0",
"dependencies": {
"@react-aria/focus": "^3.21.2",
"@react-aria/i18n": "^3.12.13",
@@ -3459,7 +3308,6 @@
"version": "3.15.2",
"resolved": "https://registry.npmjs.org/@react-aria/datepicker/-/datepicker-3.15.2.tgz",
"integrity": "sha512-th078hyNqPf4P2K10su/y32zPDjs3lOYVdHvsL9/+5K1dnTvLHCK5vgUyLuyn8FchhF7cmHV49D+LZVv65PEpQ==",
- "license": "Apache-2.0",
"dependencies": {
"@internationalized/date": "^3.10.0",
"@internationalized/number": "^3.6.5",
@@ -3489,7 +3337,6 @@
"version": "3.5.31",
"resolved": "https://registry.npmjs.org/@react-aria/dialog/-/dialog-3.5.31.tgz",
"integrity": "sha512-inxQMyrzX0UBW9Mhraq0nZ4HjHdygQvllzloT1E/RlDd61lr3RbmJR6pLsrbKOTtSvDIBJpCso1xEdHCFNmA0Q==",
- "license": "Apache-2.0",
"dependencies": {
"@react-aria/interactions": "^3.25.6",
"@react-aria/overlays": "^3.30.0",
@@ -3507,7 +3354,6 @@
"version": "3.21.2",
"resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.21.2.tgz",
"integrity": "sha512-JWaCR7wJVggj+ldmM/cb/DXFg47CXR55lznJhZBh4XVqJjMKwaOOqpT5vNN7kpC1wUpXicGNuDnJDN1S/+6dhQ==",
- "license": "Apache-2.0",
"dependencies": {
"@react-aria/interactions": "^3.25.6",
"@react-aria/utils": "^3.31.0",
@@ -3524,7 +3370,6 @@
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@react-aria/form/-/form-3.1.2.tgz",
"integrity": "sha512-R3i7L7Ci61PqZQvOrnL9xJeWEbh28UkTVgkj72EvBBn39y4h7ReH++0stv7rRs8p5ozETSKezBbGfu4UsBewWw==",
- "license": "Apache-2.0",
"dependencies": {
"@react-aria/interactions": "^3.25.6",
"@react-aria/utils": "^3.31.0",
@@ -3541,7 +3386,6 @@
"version": "3.14.5",
"resolved": "https://registry.npmjs.org/@react-aria/grid/-/grid-3.14.5.tgz",
"integrity": "sha512-XHw6rgjlTqc85e3zjsWo3U0EVwjN5MOYtrolCKc/lc2ItNdcY3OlMhpsU9+6jHwg/U3VCSWkGvwAz9hg7krd8Q==",
- "license": "Apache-2.0",
"dependencies": {
"@react-aria/focus": "^3.21.2",
"@react-aria/i18n": "^3.12.13",
@@ -3566,7 +3410,6 @@
"version": "3.12.13",
"resolved": "https://registry.npmjs.org/@react-aria/i18n/-/i18n-3.12.13.tgz",
"integrity": "sha512-YTM2BPg0v1RvmP8keHenJBmlx8FXUKsdYIEX7x6QWRd1hKlcDwphfjzvt0InX9wiLiPHsT5EoBTpuUk8SXc0Mg==",
- "license": "Apache-2.0",
"dependencies": {
"@internationalized/date": "^3.10.0",
"@internationalized/message": "^3.1.8",
@@ -3586,7 +3429,6 @@
"version": "3.25.6",
"resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.25.6.tgz",
"integrity": "sha512-5UgwZmohpixwNMVkMvn9K1ceJe6TzlRlAfuYoQDUuOkk62/JVJNDLAPKIf5YMRc7d2B0rmfgaZLMtbREb0Zvkw==",
- "license": "Apache-2.0",
"dependencies": {
"@react-aria/ssr": "^3.9.10",
"@react-aria/utils": "^3.31.0",
@@ -3603,7 +3445,6 @@
"version": "3.7.22",
"resolved": "https://registry.npmjs.org/@react-aria/label/-/label-3.7.22.tgz",
"integrity": "sha512-jLquJeA5ZNqDT64UpTc9XJ7kQYltUlNcgxZ37/v4mHe0UZ7QohCKdKQhXHONb0h2jjNUpp2HOZI8J9++jOpzxA==",
- "license": "Apache-2.0",
"dependencies": {
"@react-aria/utils": "^3.31.0",
"@react-types/shared": "^3.32.1",
@@ -3618,7 +3459,6 @@
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/@react-aria/landmark/-/landmark-3.0.7.tgz",
"integrity": "sha512-t8c610b8hPLS6Vwv+rbuSyljZosI1s5+Tosfa0Fk4q7d+Ex6Yj7hLfUFy59GxZAufhUYfGX396fT0gPqAbU1tg==",
- "license": "Apache-2.0",
"dependencies": {
"@react-aria/utils": "^3.31.0",
"@react-types/shared": "^3.32.1",
@@ -3634,7 +3474,6 @@
"version": "3.8.6",
"resolved": "https://registry.npmjs.org/@react-aria/link/-/link-3.8.6.tgz",
"integrity": "sha512-7F7UDJnwbU9IjfoAdl6f3Hho5/WB7rwcydUOjUux0p7YVWh/fTjIFjfAGyIir7MJhPapun1D0t97QQ3+8jXVcg==",
- "license": "Apache-2.0",
"dependencies": {
"@react-aria/interactions": "^3.25.6",
"@react-aria/utils": "^3.31.0",
@@ -3651,7 +3490,6 @@
"version": "3.15.0",
"resolved": "https://registry.npmjs.org/@react-aria/listbox/-/listbox-3.15.0.tgz",
"integrity": "sha512-Ub1Wu79R9sgxM7h4HeEdjOgOKDHwduvYcnDqsSddGXgpkL8ADjsy2YUQ0hHY5VnzA4BxK36bLp4mzSna8Qvj1w==",
- "license": "Apache-2.0",
"dependencies": {
"@react-aria/interactions": "^3.25.6",
"@react-aria/label": "^3.7.22",
@@ -3672,7 +3510,6 @@
"version": "3.4.4",
"resolved": "https://registry.npmjs.org/@react-aria/live-announcer/-/live-announcer-3.4.4.tgz",
"integrity": "sha512-PTTBIjNRnrdJOIRTDGNifY2d//kA7GUAwRFJNOEwSNG4FW+Bq9awqLiflw0JkpyB0VNIwou6lqKPHZVLsGWOXA==",
- "license": "Apache-2.0",
"dependencies": {
"@swc/helpers": "^0.5.0"
}
@@ -3681,7 +3518,6 @@
"version": "3.19.3",
"resolved": "https://registry.npmjs.org/@react-aria/menu/-/menu-3.19.3.tgz",
"integrity": "sha512-52fh8y8b2776R2VrfZPpUBJYC9oTP7XDy+zZuZTxPEd7Ywk0JNUl5F92y6ru22yPkS13sdhrNM/Op+V/KulmAg==",
- "license": "Apache-2.0",
"dependencies": {
"@react-aria/focus": "^3.21.2",
"@react-aria/i18n": "^3.12.13",
@@ -3707,7 +3543,6 @@
"version": "3.12.2",
"resolved": "https://registry.npmjs.org/@react-aria/numberfield/-/numberfield-3.12.2.tgz",
"integrity": "sha512-M2b+z0HIXiXpGAWOQkO2kpIjaLNUXJ5Q3/GMa3Fkr+B1piFX0VuOynYrtddKVrmXCe+r5t+XcGb0KS29uqv7nQ==",
- "license": "Apache-2.0",
"dependencies": {
"@react-aria/i18n": "^3.12.13",
"@react-aria/interactions": "^3.25.6",
@@ -3730,7 +3565,6 @@
"version": "3.30.0",
"resolved": "https://registry.npmjs.org/@react-aria/overlays/-/overlays-3.30.0.tgz",
"integrity": "sha512-UpjqSjYZx5FAhceWCRVsW6fX1sEwya1fQ/TKkL53FAlLFR8QKuoKqFlmiL43YUFTcGK3UdEOy3cWTleLQwdSmQ==",
- "license": "Apache-2.0",
"dependencies": {
"@react-aria/focus": "^3.21.2",
"@react-aria/i18n": "^3.12.13",
@@ -3753,7 +3587,6 @@
"version": "3.4.27",
"resolved": "https://registry.npmjs.org/@react-aria/progress/-/progress-3.4.27.tgz",
"integrity": "sha512-0OA1shs1575g1zmO8+rWozdbTnxThFFhOfuoL1m7UV5Dley6FHpueoKB1ECv7B+Qm4dQt6DoEqLg7wsbbQDhmg==",
- "license": "Apache-2.0",
"dependencies": {
"@react-aria/i18n": "^3.12.13",
"@react-aria/label": "^3.7.22",
@@ -3771,7 +3604,6 @@
"version": "3.12.2",
"resolved": "https://registry.npmjs.org/@react-aria/radio/-/radio-3.12.2.tgz",
"integrity": "sha512-I11f6I90neCh56rT/6ieAs3XyDKvEfbj/QmbU5cX3p+SJpRRPN0vxQi5D1hkh0uxDpeClxygSr31NmZsd4sqfg==",
- "license": "Apache-2.0",
"dependencies": {
"@react-aria/focus": "^3.21.2",
"@react-aria/form": "^3.1.2",
@@ -3793,7 +3625,6 @@
"version": "3.26.0",
"resolved": "https://registry.npmjs.org/@react-aria/selection/-/selection-3.26.0.tgz",
"integrity": "sha512-ZBH3EfWZ+RfhTj01dH8L17uT7iNbXWS8u77/fUpHgtrm0pwNVhx0TYVnLU1YpazQ/3WVpvWhmBB8sWwD1FlD/g==",
- "license": "Apache-2.0",
"dependencies": {
"@react-aria/focus": "^3.21.2",
"@react-aria/i18n": "^3.12.13",
@@ -3812,7 +3643,6 @@
"version": "3.8.2",
"resolved": "https://registry.npmjs.org/@react-aria/slider/-/slider-3.8.2.tgz",
"integrity": "sha512-6KyUGaVzRE4xAz1LKHbNh1q5wzxe58pdTHFSnxNe6nk1SCoHw7NfI4h2s2m6LgJ0megFxsT0Ir8aHaFyyxmbgg==",
- "license": "Apache-2.0",
"dependencies": {
"@react-aria/i18n": "^3.12.13",
"@react-aria/interactions": "^3.25.6",
@@ -3832,7 +3662,6 @@
"version": "3.6.19",
"resolved": "https://registry.npmjs.org/@react-aria/spinbutton/-/spinbutton-3.6.19.tgz",
"integrity": "sha512-xOIXegDpts9t3RSHdIN0iYQpdts0FZ3LbpYJIYVvdEHo9OpDS+ElnDzCGtwZLguvZlwc5s1LAKuKopDUsAEMkw==",
- "license": "Apache-2.0",
"dependencies": {
"@react-aria/i18n": "^3.12.13",
"@react-aria/live-announcer": "^3.4.4",
@@ -3850,7 +3679,6 @@
"version": "3.9.10",
"resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.10.tgz",
"integrity": "sha512-hvTm77Pf+pMBhuBm760Li0BVIO38jv1IBws1xFm1NoL26PU+fe+FMW5+VZWyANR6nYL65joaJKZqOdTQMkO9IQ==",
- "license": "Apache-2.0",
"dependencies": {
"@swc/helpers": "^0.5.0"
},
@@ -3865,7 +3693,6 @@
"version": "3.7.8",
"resolved": "https://registry.npmjs.org/@react-aria/switch/-/switch-3.7.8.tgz",
"integrity": "sha512-AfsUq1/YiuoprhcBUD9vDPyWaigAwctQNW1fMb8dROL+i/12B+Zekj8Ml+jbU69/kIVtfL0Jl7/0Bo9KK3X0xQ==",
- "license": "Apache-2.0",
"dependencies": {
"@react-aria/toggle": "^3.12.2",
"@react-stately/toggle": "^3.9.2",
@@ -3882,7 +3709,6 @@
"version": "3.17.8",
"resolved": "https://registry.npmjs.org/@react-aria/table/-/table-3.17.8.tgz",
"integrity": "sha512-bXiZoxTMbsqUJsYDhHPzKc3jw0HFJ/xMsJ49a0f7mp5r9zACxNLeIU0wJ4Uvx37dnYOHKzGliG+rj5l4sph7MA==",
- "license": "Apache-2.0",
"dependencies": {
"@react-aria/focus": "^3.21.2",
"@react-aria/grid": "^3.14.5",
@@ -3909,7 +3735,6 @@
"version": "3.10.8",
"resolved": "https://registry.npmjs.org/@react-aria/tabs/-/tabs-3.10.8.tgz",
"integrity": "sha512-sPPJyTyoAqsBh76JinBAxStOcbjZvyWFYKpJ9Uqw+XT0ObshAPPFSGeh8DiQemPs02RwJdrfARPMhyqiX8t59A==",
- "license": "Apache-2.0",
"dependencies": {
"@react-aria/focus": "^3.21.2",
"@react-aria/i18n": "^3.12.13",
@@ -3929,7 +3754,6 @@
"version": "3.18.2",
"resolved": "https://registry.npmjs.org/@react-aria/textfield/-/textfield-3.18.2.tgz",
"integrity": "sha512-G+lM8VYSor6g9Yptc6hLZ6BF+0cq0pYol1z6wdQUQgJN8tg4HPtzq75lsZtlCSIznL3amgRAxJtd0dUrsAnvaQ==",
- "license": "Apache-2.0",
"dependencies": {
"@react-aria/form": "^3.1.2",
"@react-aria/interactions": "^3.25.6",
@@ -3950,7 +3774,6 @@
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/@react-aria/toast/-/toast-3.0.8.tgz",
"integrity": "sha512-rfJIms6AkMyQ7ZgKrMZgGfPwGcB/t1JoEwbc1PAmXcAvFI/hzF6YF7ZFDXiq38ucFsP9PnHmbXIzM9w4ccl18A==",
- "license": "Apache-2.0",
"dependencies": {
"@react-aria/i18n": "^3.12.13",
"@react-aria/interactions": "^3.25.6",
@@ -3970,7 +3793,6 @@
"version": "3.12.2",
"resolved": "https://registry.npmjs.org/@react-aria/toggle/-/toggle-3.12.2.tgz",
"integrity": "sha512-g25XLYqJuJpt0/YoYz2Rab8ax+hBfbssllcEFh0v0jiwfk2gwTWfRU9KAZUvxIqbV8Nm8EBmrYychDpDcvW1kw==",
- "license": "Apache-2.0",
"dependencies": {
"@react-aria/interactions": "^3.25.6",
"@react-aria/utils": "^3.31.0",
@@ -3988,7 +3810,6 @@
"version": "3.0.0-beta.21",
"resolved": "https://registry.npmjs.org/@react-aria/toolbar/-/toolbar-3.0.0-beta.21.tgz",
"integrity": "sha512-yRCk/GD8g+BhdDgxd3I0a0c8Ni4Wyo6ERzfSoBkPkwQ4X2E2nkopmraM9D0fXw4UcIr4bnmvADzkHXtBN0XrBg==",
- "license": "Apache-2.0",
"dependencies": {
"@react-aria/focus": "^3.21.2",
"@react-aria/i18n": "^3.12.13",
@@ -4005,7 +3826,6 @@
"version": "3.8.8",
"resolved": "https://registry.npmjs.org/@react-aria/tooltip/-/tooltip-3.8.8.tgz",
"integrity": "sha512-CmHUqtXtFWmG4AHMEr9hIVex+oscK6xcM2V47gq9ijNInxe3M6UBu/dBdkgGP/jYv9N7tzCAjTR8nNIHQXwvWw==",
- "license": "Apache-2.0",
"dependencies": {
"@react-aria/interactions": "^3.25.6",
"@react-aria/utils": "^3.31.0",
@@ -4023,7 +3843,6 @@
"version": "3.31.0",
"resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.31.0.tgz",
"integrity": "sha512-ABOzCsZrWzf78ysswmguJbx3McQUja7yeGj6/vZo4JVsZNlxAN+E9rs381ExBRI0KzVo6iBTeX5De8eMZPJXig==",
- "license": "Apache-2.0",
"dependencies": {
"@react-aria/ssr": "^3.9.10",
"@react-stately/flags": "^3.1.2",
@@ -4041,7 +3860,6 @@
"version": "3.8.28",
"resolved": "https://registry.npmjs.org/@react-aria/visually-hidden/-/visually-hidden-3.8.28.tgz",
"integrity": "sha512-KRRjbVVob2CeBidF24dzufMxBveEUtUu7IM+hpdZKB+gxVROoh4XRLPv9SFmaH89Z7D9To3QoykVZoWD0lan6Q==",
- "license": "Apache-2.0",
"dependencies": {
"@react-aria/interactions": "^3.25.6",
"@react-aria/utils": "^3.31.0",
@@ -4190,7 +4008,6 @@
"version": "3.9.0",
"resolved": "https://registry.npmjs.org/@react-stately/calendar/-/calendar-3.9.0.tgz",
"integrity": "sha512-U5Nf2kx9gDhJRxdDUm5gjfyUlt/uUfOvM1vDW2UA62cA6+2k2cavMLc2wNlXOb/twFtl6p0joYKHG7T4xnEFkg==",
- "license": "Apache-2.0",
"dependencies": {
"@internationalized/date": "^3.10.0",
"@react-stately/utils": "^3.10.8",
@@ -4206,7 +4023,6 @@
"version": "3.7.2",
"resolved": "https://registry.npmjs.org/@react-stately/checkbox/-/checkbox-3.7.2.tgz",
"integrity": "sha512-j1ycUVz5JmqhaL6mDZgDNZqBilOB8PBW096sDPFaTtuYreDx2HOd1igxiIvwlvPESZwsJP7FVM3mYnaoXtpKPA==",
- "license": "Apache-2.0",
"dependencies": {
"@react-stately/form": "^3.2.2",
"@react-stately/utils": "^3.10.8",
@@ -4222,7 +4038,6 @@
"version": "3.12.8",
"resolved": "https://registry.npmjs.org/@react-stately/collections/-/collections-3.12.8.tgz",
"integrity": "sha512-AceJYLLXt1Y2XIcOPi6LEJSs4G/ubeYW3LqOCQbhfIgMaNqKfQMIfagDnPeJX9FVmPFSlgoCBxb1pTJW2vjCAQ==",
- "license": "Apache-2.0",
"dependencies": {
"@react-types/shared": "^3.32.1",
"@swc/helpers": "^0.5.0"
@@ -4235,7 +4050,6 @@
"version": "3.12.0",
"resolved": "https://registry.npmjs.org/@react-stately/combobox/-/combobox-3.12.0.tgz",
"integrity": "sha512-A6q9R/7cEa/qoQsBkdslXWvD7ztNLLQ9AhBhVN9QvzrmrH5B4ymUwcTU8lWl22ykH7RRwfonLeLXJL4C+/L2oQ==",
- "license": "Apache-2.0",
"dependencies": {
"@react-stately/collections": "^3.12.8",
"@react-stately/form": "^3.2.2",
@@ -4254,7 +4068,6 @@
"version": "3.15.2",
"resolved": "https://registry.npmjs.org/@react-stately/datepicker/-/datepicker-3.15.2.tgz",
"integrity": "sha512-S5GL+W37chvV8knv9v0JRv0L6hKo732qqabCCHXzOpYxkLIkV4f/y3cHdEzFWzpZ0O0Gkg7WgeYo160xOdBKYg==",
- "license": "Apache-2.0",
"dependencies": {
"@internationalized/date": "^3.10.0",
"@internationalized/string": "^3.2.7",
@@ -4273,7 +4086,6 @@
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@react-stately/flags/-/flags-3.1.2.tgz",
"integrity": "sha512-2HjFcZx1MyQXoPqcBGALwWWmgFVUk2TuKVIQxCbRq7fPyWXIl6VHcakCLurdtYC2Iks7zizvz0Idv48MQ38DWg==",
- "license": "Apache-2.0",
"dependencies": {
"@swc/helpers": "^0.5.0"
}
@@ -4282,7 +4094,6 @@
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/@react-stately/form/-/form-3.2.2.tgz",
"integrity": "sha512-soAheOd7oaTO6eNs6LXnfn0tTqvOoe3zN9FvtIhhrErKz9XPc5sUmh3QWwR45+zKbitOi1HOjfA/gifKhZcfWw==",
- "license": "Apache-2.0",
"dependencies": {
"@react-types/shared": "^3.32.1",
"@swc/helpers": "^0.5.0"
@@ -4295,7 +4106,6 @@
"version": "3.11.6",
"resolved": "https://registry.npmjs.org/@react-stately/grid/-/grid-3.11.6.tgz",
"integrity": "sha512-vWPAkzpeTIsrurHfMubzMuqEw7vKzFhIJeEK5sEcLunyr1rlADwTzeWrHNbPMl66NAIAi70Dr1yNq+kahQyvMA==",
- "license": "Apache-2.0",
"dependencies": {
"@react-stately/collections": "^3.12.8",
"@react-stately/selection": "^3.20.6",
@@ -4311,7 +4121,6 @@
"version": "3.13.1",
"resolved": "https://registry.npmjs.org/@react-stately/list/-/list-3.13.1.tgz",
"integrity": "sha512-eHaoauh21twbcl0kkwULhVJ+CzYcy1jUjMikNVMHOQdhr4WIBdExf7PmSgKHKqsSPhpGg6IpTCY2dUX3RycjDg==",
- "license": "Apache-2.0",
"dependencies": {
"@react-stately/collections": "^3.12.8",
"@react-stately/selection": "^3.20.6",
@@ -4327,7 +4136,6 @@
"version": "3.9.8",
"resolved": "https://registry.npmjs.org/@react-stately/menu/-/menu-3.9.8.tgz",
"integrity": "sha512-bo0NOhofnTHLESiYfsSSw6gyXiPVJJ0UlN2igUXtJk5PmyhWjFzUzTzcnd7B028OB0si9w3LIWM3stqz5271Eg==",
- "license": "Apache-2.0",
"dependencies": {
"@react-stately/overlays": "^3.6.20",
"@react-types/menu": "^3.10.5",
@@ -4342,7 +4150,6 @@
"version": "3.10.2",
"resolved": "https://registry.npmjs.org/@react-stately/numberfield/-/numberfield-3.10.2.tgz",
"integrity": "sha512-jlKVFYaH3RX5KvQ7a+SAMQuPccZCzxLkeYkBE64u1Zvi7YhJ8hkTMHG/fmZMbk1rHlseE2wfBdk0Rlya3MvoNQ==",
- "license": "Apache-2.0",
"dependencies": {
"@internationalized/number": "^3.6.5",
"@react-stately/form": "^3.2.2",
@@ -4358,7 +4165,6 @@
"version": "3.6.20",
"resolved": "https://registry.npmjs.org/@react-stately/overlays/-/overlays-3.6.20.tgz",
"integrity": "sha512-YAIe+uI8GUXX8F/0Pzr53YeC5c/bjqbzDFlV8NKfdlCPa6+Jp4B/IlYVjIooBj9+94QvbQdjylegvYWK/iPwlg==",
- "license": "Apache-2.0",
"dependencies": {
"@react-stately/utils": "^3.10.8",
"@react-types/overlays": "^3.9.2",
@@ -4372,7 +4178,6 @@
"version": "3.11.2",
"resolved": "https://registry.npmjs.org/@react-stately/radio/-/radio-3.11.2.tgz",
"integrity": "sha512-UM7L6AW+k8edhSBUEPZAqiWNRNadfOKK7BrCXyBiG79zTz0zPcXRR+N+gzkDn7EMSawDeyK1SHYUuoSltTactg==",
- "license": "Apache-2.0",
"dependencies": {
"@react-stately/form": "^3.2.2",
"@react-stately/utils": "^3.10.8",
@@ -4388,7 +4193,6 @@
"version": "3.20.6",
"resolved": "https://registry.npmjs.org/@react-stately/selection/-/selection-3.20.6.tgz",
"integrity": "sha512-a0bjuP2pJYPKEiedz2Us1W1aSz0iHRuyeQEdBOyL6Z6VUa6hIMq9H60kvseir2T85cOa4QggizuRV7mcO6bU5w==",
- "license": "Apache-2.0",
"dependencies": {
"@react-stately/collections": "^3.12.8",
"@react-stately/utils": "^3.10.8",
@@ -4403,7 +4207,6 @@
"version": "3.7.2",
"resolved": "https://registry.npmjs.org/@react-stately/slider/-/slider-3.7.2.tgz",
"integrity": "sha512-EVBHUdUYwj++XqAEiQg2fGi8Reccznba0uyQ3gPejF0pAc390Q/J5aqiTEDfiCM7uJ6WHxTM6lcCqHQBISk2dQ==",
- "license": "Apache-2.0",
"dependencies": {
"@react-stately/utils": "^3.10.8",
"@react-types/shared": "^3.32.1",
@@ -4418,7 +4221,6 @@
"version": "3.15.1",
"resolved": "https://registry.npmjs.org/@react-stately/table/-/table-3.15.1.tgz",
"integrity": "sha512-MhMAgE/LgAzHcAn1P3p/nQErzJ6DiixSJ1AOt2JlnAKEb5YJg4ATKWCb2IjBLwywt9ZCzfm3KMUzkctZqAoxwA==",
- "license": "Apache-2.0",
"dependencies": {
"@react-stately/collections": "^3.12.8",
"@react-stately/flags": "^3.1.2",
@@ -4438,7 +4240,6 @@
"version": "3.8.6",
"resolved": "https://registry.npmjs.org/@react-stately/tabs/-/tabs-3.8.6.tgz",
"integrity": "sha512-9RYxmgjVIxUpIsGKPIF7uRoHWOEz8muwaYiStCVeyiYBPmarvZoIYtTXcwSMN/vEs7heVN5uGCL6/bfdY4+WiA==",
- "license": "Apache-2.0",
"dependencies": {
"@react-stately/list": "^3.13.1",
"@react-types/shared": "^3.32.1",
@@ -4453,7 +4254,6 @@
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@react-stately/toast/-/toast-3.1.2.tgz",
"integrity": "sha512-HiInm7bck32khFBHZThTQaAF6e6/qm57F4mYRWdTq8IVeGDzpkbUYibnLxRhk0UZ5ybc6me+nqqPkG/lVmM42Q==",
- "license": "Apache-2.0",
"dependencies": {
"@swc/helpers": "^0.5.0",
"use-sync-external-store": "^1.4.0"
@@ -4466,7 +4266,6 @@
"version": "3.9.2",
"resolved": "https://registry.npmjs.org/@react-stately/toggle/-/toggle-3.9.2.tgz",
"integrity": "sha512-dOxs9wrVXHUmA7lc8l+N9NbTJMAaXcYsnNGsMwfXIXQ3rdq+IjWGNYJ52UmNQyRYFcg0jrzRrU16TyGbNjOdNQ==",
- "license": "Apache-2.0",
"dependencies": {
"@react-stately/utils": "^3.10.8",
"@react-types/checkbox": "^3.10.2",
@@ -4481,7 +4280,6 @@
"version": "3.5.8",
"resolved": "https://registry.npmjs.org/@react-stately/tooltip/-/tooltip-3.5.8.tgz",
"integrity": "sha512-gkcUx2ROhCiGNAYd2BaTejakXUUNLPnnoJ5+V/mN480pN+OrO8/2V9pqb/IQmpqxLsso93zkM3A4wFHHLBBmPQ==",
- "license": "Apache-2.0",
"dependencies": {
"@react-stately/overlays": "^3.6.20",
"@react-types/tooltip": "^3.4.21",
@@ -4495,7 +4293,6 @@
"version": "3.9.3",
"resolved": "https://registry.npmjs.org/@react-stately/tree/-/tree-3.9.3.tgz",
"integrity": "sha512-ZngG79nLFxE/GYmpwX6E/Rma2MMkzdoJPRI3iWk3dgqnGMMzpPnUp/cvjDsU3UHF7xDVusC5BT6pjWN0uxCIFQ==",
- "license": "Apache-2.0",
"dependencies": {
"@react-stately/collections": "^3.12.8",
"@react-stately/selection": "^3.20.6",
@@ -4511,7 +4308,6 @@
"version": "3.10.8",
"resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.10.8.tgz",
"integrity": "sha512-SN3/h7SzRsusVQjQ4v10LaVsDc81jyyR0DD5HnsQitm/I5WDpaSr2nRHtyloPFU48jlql1XX/S04T2DLQM7Y3g==",
- "license": "Apache-2.0",
"dependencies": {
"@swc/helpers": "^0.5.0"
},
@@ -4523,7 +4319,6 @@
"version": "4.4.4",
"resolved": "https://registry.npmjs.org/@react-stately/virtualizer/-/virtualizer-4.4.4.tgz",
"integrity": "sha512-ri8giqXSZOrznZDCCOE4U36wSkOhy+hrFK7yo/YVcpxTqqp3d3eisfKMqbDsgqBW+XTHycTU/xeAf0u9NqrfpQ==",
- "license": "Apache-2.0",
"dependencies": {
"@react-types/shared": "^3.32.1",
"@swc/helpers": "^0.5.0"
@@ -4537,7 +4332,6 @@
"version": "3.0.0-alpha.26",
"resolved": "https://registry.npmjs.org/@react-types/accordion/-/accordion-3.0.0-alpha.26.tgz",
"integrity": "sha512-OXf/kXcD2vFlEnkcZy/GG+a/1xO9BN7Uh3/5/Ceuj9z2E/WwD55YwU3GFM5zzkZ4+DMkdowHnZX37XnmbyD3Mg==",
- "license": "Apache-2.0",
"dependencies": {
"@react-types/shared": "^3.27.0"
},
@@ -4549,7 +4343,6 @@
"version": "3.7.17",
"resolved": "https://registry.npmjs.org/@react-types/breadcrumbs/-/breadcrumbs-3.7.17.tgz",
"integrity": "sha512-IhvVTcfli5o/UDlGACXxjlor2afGlMQA8pNR3faH0bBUay1Fmm3IWktVw9Xwmk+KraV2RTAg9e+E6p8DOQZfiw==",
- "license": "Apache-2.0",
"dependencies": {
"@react-types/link": "^3.6.5",
"@react-types/shared": "^3.32.1"
@@ -4562,7 +4355,6 @@
"version": "3.14.1",
"resolved": "https://registry.npmjs.org/@react-types/button/-/button-3.14.1.tgz",
"integrity": "sha512-D8C4IEwKB7zEtiWYVJ3WE/5HDcWlze9mLWQ5hfsBfpePyWCgO3bT/+wjb/7pJvcAocrkXo90QrMm85LcpBtrpg==",
- "license": "Apache-2.0",
"dependencies": {
"@react-types/shared": "^3.32.1"
},
@@ -4574,7 +4366,6 @@
"version": "3.8.0",
"resolved": "https://registry.npmjs.org/@react-types/calendar/-/calendar-3.8.0.tgz",
"integrity": "sha512-ZDZgfZgbz1ydWOFs1mH7QFfX3ioJrmb3Y/lkoubQE0HWXLZzyYNvhhKyFJRS1QJ40IofLSBHriwbQb/tsUnGlw==",
- "license": "Apache-2.0",
"dependencies": {
"@internationalized/date": "^3.10.0",
"@react-types/shared": "^3.32.1"
@@ -4587,7 +4378,6 @@
"version": "3.10.2",
"resolved": "https://registry.npmjs.org/@react-types/checkbox/-/checkbox-3.10.2.tgz",
"integrity": "sha512-ktPkl6ZfIdGS1tIaGSU/2S5Agf2NvXI9qAgtdMDNva0oLyAZ4RLQb6WecPvofw1J7YKXu0VA5Mu7nlX+FM2weQ==",
- "license": "Apache-2.0",
"dependencies": {
"@react-types/shared": "^3.32.1"
},
@@ -4599,7 +4389,6 @@
"version": "3.13.9",
"resolved": "https://registry.npmjs.org/@react-types/combobox/-/combobox-3.13.9.tgz",
"integrity": "sha512-G6GmLbzVkLW6VScxPAr/RtliEyPhBClfYaIllK1IZv+Z42SVnOpKzhnoe79BpmiFqy1AaC3+LjZX783mrsHCwA==",
- "license": "Apache-2.0",
"dependencies": {
"@react-types/shared": "^3.32.1"
},
@@ -4611,7 +4400,6 @@
"version": "3.13.2",
"resolved": "https://registry.npmjs.org/@react-types/datepicker/-/datepicker-3.13.2.tgz",
"integrity": "sha512-+M6UZxJnejYY8kz0spbY/hP08QJ5rsZ3aNarRQQHc48xV2oelFLX5MhAqizfLEsvyfb0JYrhWoh4z1xZtAmYCg==",
- "license": "Apache-2.0",
"dependencies": {
"@internationalized/date": "^3.10.0",
"@react-types/calendar": "^3.8.0",
@@ -4626,7 +4414,6 @@
"version": "3.5.22",
"resolved": "https://registry.npmjs.org/@react-types/dialog/-/dialog-3.5.22.tgz",
"integrity": "sha512-smSvzOcqKE196rWk0oqJDnz+ox5JM5+OT0PmmJXiUD4q7P5g32O6W5Bg7hMIFUI9clBtngo8kLaX2iMg+GqAzg==",
- "license": "Apache-2.0",
"dependencies": {
"@react-types/overlays": "^3.9.2",
"@react-types/shared": "^3.32.1"
@@ -4639,7 +4426,6 @@
"version": "3.7.16",
"resolved": "https://registry.npmjs.org/@react-types/form/-/form-3.7.16.tgz",
"integrity": "sha512-Sb7KJoWEaQ/e4XIY+xRbjKvbP1luome98ZXevpD+zVSyGjEcfIroebizP6K1yMHCWP/043xH6GUkgEqWPoVGjg==",
- "license": "Apache-2.0",
"dependencies": {
"@react-types/shared": "^3.32.1"
},
@@ -4651,7 +4437,6 @@
"version": "3.3.6",
"resolved": "https://registry.npmjs.org/@react-types/grid/-/grid-3.3.6.tgz",
"integrity": "sha512-vIZJlYTii2n1We9nAugXwM2wpcpsC6JigJFBd6vGhStRdRWRoU4yv1Gc98Usbx0FQ/J7GLVIgeG8+1VMTKBdxw==",
- "license": "Apache-2.0",
"dependencies": {
"@react-types/shared": "^3.32.1"
},
@@ -4663,7 +4448,6 @@
"version": "3.6.5",
"resolved": "https://registry.npmjs.org/@react-types/link/-/link-3.6.5.tgz",
"integrity": "sha512-+I2s3XWBEvLrzts0GnNeA84mUkwo+a7kLUWoaJkW0TOBDG7my95HFYxF9WnqKye7NgpOkCqz4s3oW96xPdIniQ==",
- "license": "Apache-2.0",
"dependencies": {
"@react-types/shared": "^3.32.1"
},
@@ -4675,7 +4459,6 @@
"version": "3.7.4",
"resolved": "https://registry.npmjs.org/@react-types/listbox/-/listbox-3.7.4.tgz",
"integrity": "sha512-p4YEpTl/VQGrqVE8GIfqTS5LkT5jtjDTbVeZgrkPnX/fiPhsfbTPiZ6g0FNap4+aOGJFGEEZUv2q4vx+rCORww==",
- "license": "Apache-2.0",
"dependencies": {
"@react-types/shared": "^3.32.1"
},
@@ -4687,7 +4470,6 @@
"version": "3.10.5",
"resolved": "https://registry.npmjs.org/@react-types/menu/-/menu-3.10.5.tgz",
"integrity": "sha512-HBTrKll2hm0VKJNM4ubIv1L9MNo8JuOnm2G3M+wXvb6EYIyDNxxJkhjsqsGpUXJdAOSkacHBDcNh2HsZABNX4A==",
- "license": "Apache-2.0",
"dependencies": {
"@react-types/overlays": "^3.9.2",
"@react-types/shared": "^3.32.1"
@@ -4700,7 +4482,6 @@
"version": "3.8.15",
"resolved": "https://registry.npmjs.org/@react-types/numberfield/-/numberfield-3.8.15.tgz",
"integrity": "sha512-97r92D23GKCOjGIGMeW9nt+/KlfM3GeWH39Czcmd2/D5y3k6z4j0avbsfx2OttCtJszrnENjw3GraYGYI2KosQ==",
- "license": "Apache-2.0",
"dependencies": {
"@react-types/shared": "^3.32.1"
},
@@ -4712,7 +4493,6 @@
"version": "3.9.2",
"resolved": "https://registry.npmjs.org/@react-types/overlays/-/overlays-3.9.2.tgz",
"integrity": "sha512-Q0cRPcBGzNGmC8dBuHyoPR7N3057KTS5g+vZfQ53k8WwmilXBtemFJPLsogJbspuewQ/QJ3o2HYsp2pne7/iNw==",
- "license": "Apache-2.0",
"dependencies": {
"@react-types/shared": "^3.32.1"
},
@@ -4724,7 +4504,6 @@
"version": "3.5.16",
"resolved": "https://registry.npmjs.org/@react-types/progress/-/progress-3.5.16.tgz",
"integrity": "sha512-I9tSdCFfvQ7gHJtm90VAKgwdTWXQgVNvLRStEc0z9h+bXBxdvZb+QuiRPERChwFQ9VkK4p4rDqaFo69nDqWkpw==",
- "license": "Apache-2.0",
"dependencies": {
"@react-types/shared": "^3.32.1"
},
@@ -4736,7 +4515,6 @@
"version": "3.9.2",
"resolved": "https://registry.npmjs.org/@react-types/radio/-/radio-3.9.2.tgz",
"integrity": "sha512-3UcJXu37JrTkRyP4GJPDBU7NmDTInrEdOe+bVzA1j4EegzdkJmLBkLg5cLDAbpiEHB+xIsvbJdx6dxeMuc+H3g==",
- "license": "Apache-2.0",
"dependencies": {
"@react-types/shared": "^3.32.1"
},
@@ -4748,7 +4526,6 @@
"version": "3.32.1",
"resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.32.1.tgz",
"integrity": "sha512-famxyD5emrGGpFuUlgOP6fVW2h/ZaF405G5KDi3zPHzyjAWys/8W6NAVJtNbkCkhedmvL0xOhvt8feGXyXaw5w==",
- "license": "Apache-2.0",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
}
@@ -4757,7 +4534,6 @@
"version": "3.8.2",
"resolved": "https://registry.npmjs.org/@react-types/slider/-/slider-3.8.2.tgz",
"integrity": "sha512-MQYZP76OEOYe7/yA2To+Dl0LNb0cKKnvh5JtvNvDnAvEprn1RuLiay8Oi/rTtXmc2KmBa4VdTcsXsmkbbkeN2Q==",
- "license": "Apache-2.0",
"dependencies": {
"@react-types/shared": "^3.32.1"
},
@@ -4769,7 +4545,6 @@
"version": "3.5.15",
"resolved": "https://registry.npmjs.org/@react-types/switch/-/switch-3.5.15.tgz",
"integrity": "sha512-r/ouGWQmIeHyYSP1e5luET+oiR7N7cLrAlWsrAfYRWHxqXOSNQloQnZJ3PLHrKFT02fsrQhx2rHaK2LfKeyN3A==",
- "license": "Apache-2.0",
"dependencies": {
"@react-types/shared": "^3.32.1"
},
@@ -4781,7 +4556,6 @@
"version": "3.13.4",
"resolved": "https://registry.npmjs.org/@react-types/table/-/table-3.13.4.tgz",
"integrity": "sha512-I/DYiZQl6aNbMmjk90J9SOhkzVDZvyA3Vn3wMWCiajkMNjvubFhTfda5DDf2SgFP5l0Yh6TGGH5XumRv9LqL5Q==",
- "license": "Apache-2.0",
"dependencies": {
"@react-types/grid": "^3.3.6",
"@react-types/shared": "^3.32.1"
@@ -4794,7 +4568,6 @@
"version": "3.3.19",
"resolved": "https://registry.npmjs.org/@react-types/tabs/-/tabs-3.3.19.tgz",
"integrity": "sha512-fE+qI43yR5pAMpeqPxGqQq9jDHXEPqXskuxNHERMW0PYMdPyem2Cw6goc5F4qeZO3Hf6uPZgHkvJz2OAq7TbBw==",
- "license": "Apache-2.0",
"dependencies": {
"@react-types/shared": "^3.32.1"
},
@@ -4806,7 +4579,6 @@
"version": "3.12.6",
"resolved": "https://registry.npmjs.org/@react-types/textfield/-/textfield-3.12.6.tgz",
"integrity": "sha512-hpEVKE+M3uUkTjw2WrX1NrH/B3rqDJFUa+ViNK2eVranLY4ZwFqbqaYXSzHupOF3ecSjJJv2C103JrwFvx6TPQ==",
- "license": "Apache-2.0",
"dependencies": {
"@react-types/shared": "^3.32.1"
},
@@ -4818,7 +4590,6 @@
"version": "3.4.21",
"resolved": "https://registry.npmjs.org/@react-types/tooltip/-/tooltip-3.4.21.tgz",
"integrity": "sha512-ugGHOZU6WbOdeTdbjnaEc+Ms7/WhsUCg+T3PCOIeOT9FG02Ce189yJ/+hd7oqL/tVwIhEMYJIqSCgSELFox+QA==",
- "license": "Apache-2.0",
"dependencies": {
"@react-types/overlays": "^3.9.2",
"@react-types/shared": "^3.32.1"
@@ -5177,11 +4948,10 @@
"license": "MIT"
},
"node_modules/@standard-schema/spec": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz",
- "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==",
- "dev": true,
- "license": "MIT"
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
+ "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
+ "dev": true
},
"node_modules/@svgr/babel-plugin-add-jsx-attribute": {
"version": "8.0.0",
@@ -5425,7 +5195,6 @@
"version": "0.5.17",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz",
"integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==",
- "license": "Apache-2.0",
"dependencies": {
"tslib": "^2.8.0"
}
@@ -5786,7 +5555,6 @@
"version": "3.11.3",
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.11.3.tgz",
"integrity": "sha512-vCU+OTylXN3hdC8RKg68tPlBPjjxtzon7Ys46MgrSLE+JhSjSTPvoQifV6DQJeJmA8Q3KT6CphJbejupx85vFw==",
- "license": "MIT",
"dependencies": {
"@tanstack/virtual-core": "3.11.3"
},
@@ -5803,7 +5571,6 @@
"version": "3.11.3",
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.11.3.tgz",
"integrity": "sha512-v2mrNSnMwnPJtcVqNvV0c5roGCBqeogN8jDtgtuHCphdwBasOZ17x8UV8qpHUh+u0MLfX43c0uUHKje0s+Zb0w==",
- "license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
@@ -5857,11 +5624,10 @@
"license": "MIT"
},
"node_modules/@testing-library/react": {
- "version": "16.3.0",
- "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.0.tgz",
- "integrity": "sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==",
+ "version": "16.3.1",
+ "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.1.tgz",
+ "integrity": "sha512-gr4KtAWqIOQoucWYD/f6ki+j5chXfcPc74Col/6poTyqTmn7zRmodWahWRCp8tYd+GMqBonw6hstNzqjbs6gjw==",
"dev": true,
- "license": "MIT",
"dependencies": {
"@babel/runtime": "^7.12.5"
},
@@ -5916,7 +5682,6 @@
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
"integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
"dev": true,
- "license": "MIT",
"dependencies": {
"@types/deep-eql": "*",
"assertion-error": "^2.0.1"
@@ -5935,8 +5700,7 @@
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
"integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
- "dev": true,
- "license": "MIT"
+ "dev": true
},
"node_modules/@types/estree": {
"version": "1.0.8",
@@ -5991,9 +5755,9 @@
"license": "MIT"
},
"node_modules/@types/node": {
- "version": "25.0.1",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.1.tgz",
- "integrity": "sha512-czWPzKIAXucn9PtsttxmumiQ9N0ok9FrBwgRWrwmVLlp86BrMExzvXRLFYRJ+Ex3g6yqj+KuaxfX1JTgV2lpfg==",
+ "version": "25.0.3",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz",
+ "integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==",
"devOptional": true,
"dependencies": {
"undici-types": "~7.16.0"
@@ -6471,14 +6235,13 @@
"license": "ISC"
},
"node_modules/@vitest/coverage-v8": {
- "version": "4.0.15",
- "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.0.15.tgz",
- "integrity": "sha512-FUJ+1RkpTFW7rQITdgTi93qOCWJobWhBirEPCeXh2SW2wsTlFxy51apDz5gzG+ZEYt/THvWeNmhdAoS9DTwpCw==",
+ "version": "4.0.16",
+ "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.0.16.tgz",
+ "integrity": "sha512-2rNdjEIsPRzsdu6/9Eq0AYAzYdpP6Bx9cje9tL3FE5XzXRQF1fNU9pe/1yE8fCrS0HD+fBtt6gLPh6LI57tX7A==",
"dev": true,
- "license": "MIT",
"dependencies": {
"@bcoe/v8-coverage": "^1.0.2",
- "@vitest/utils": "4.0.15",
+ "@vitest/utils": "4.0.16",
"ast-v8-to-istanbul": "^0.3.8",
"istanbul-lib-coverage": "^3.2.2",
"istanbul-lib-report": "^3.0.1",
@@ -6493,8 +6256,8 @@
"url": "https://opencollective.com/vitest"
},
"peerDependencies": {
- "@vitest/browser": "4.0.15",
- "vitest": "4.0.15"
+ "@vitest/browser": "4.0.16",
+ "vitest": "4.0.16"
},
"peerDependenciesMeta": {
"@vitest/browser": {
@@ -6503,16 +6266,15 @@
}
},
"node_modules/@vitest/expect": {
- "version": "4.0.15",
- "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.15.tgz",
- "integrity": "sha512-Gfyva9/GxPAWXIWjyGDli9O+waHDC0Q0jaLdFP1qPAUUfo1FEXPXUfUkp3eZA0sSq340vPycSyOlYUeM15Ft1w==",
+ "version": "4.0.16",
+ "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.16.tgz",
+ "integrity": "sha512-eshqULT2It7McaJkQGLkPjPjNph+uevROGuIMJdG3V+0BSR2w9u6J9Lwu+E8cK5TETlfou8GRijhafIMhXsimA==",
"dev": true,
- "license": "MIT",
"dependencies": {
"@standard-schema/spec": "^1.0.0",
"@types/chai": "^5.2.2",
- "@vitest/spy": "4.0.15",
- "@vitest/utils": "4.0.15",
+ "@vitest/spy": "4.0.16",
+ "@vitest/utils": "4.0.16",
"chai": "^6.2.1",
"tinyrainbow": "^3.0.3"
},
@@ -6521,13 +6283,12 @@
}
},
"node_modules/@vitest/mocker": {
- "version": "4.0.15",
- "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.15.tgz",
- "integrity": "sha512-CZ28GLfOEIFkvCFngN8Sfx5h+Se0zN+h4B7yOsPVCcgtiO7t5jt9xQh2E1UkFep+eb9fjyMfuC5gBypwb07fvQ==",
+ "version": "4.0.16",
+ "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.16.tgz",
+ "integrity": "sha512-yb6k4AZxJTB+q9ycAvsoxGn+j/po0UaPgajllBgt1PzoMAAmJGYFdDk0uCcRcxb3BrME34I6u8gHZTQlkqSZpg==",
"dev": true,
- "license": "MIT",
"dependencies": {
- "@vitest/spy": "4.0.15",
+ "@vitest/spy": "4.0.16",
"estree-walker": "^3.0.3",
"magic-string": "^0.30.21"
},
@@ -6548,11 +6309,10 @@
}
},
"node_modules/@vitest/pretty-format": {
- "version": "4.0.15",
- "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.15.tgz",
- "integrity": "sha512-SWdqR8vEv83WtZcrfLNqlqeQXlQLh2iilO1Wk1gv4eiHKjEzvgHb2OVc3mIPyhZE6F+CtfYjNlDJwP5MN6Km7A==",
+ "version": "4.0.16",
+ "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.16.tgz",
+ "integrity": "sha512-eNCYNsSty9xJKi/UdVD8Ou16alu7AYiS2fCPRs0b1OdhJiV89buAXQLpTbe+X8V9L6qrs9CqyvU7OaAopJYPsA==",
"dev": true,
- "license": "MIT",
"dependencies": {
"tinyrainbow": "^3.0.3"
},
@@ -6561,13 +6321,12 @@
}
},
"node_modules/@vitest/runner": {
- "version": "4.0.15",
- "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.15.tgz",
- "integrity": "sha512-+A+yMY8dGixUhHmNdPUxOh0la6uVzun86vAbuMT3hIDxMrAOmn5ILBHm8ajrqHE0t8R9T1dGnde1A5DTnmi3qw==",
+ "version": "4.0.16",
+ "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.16.tgz",
+ "integrity": "sha512-VWEDm5Wv9xEo80ctjORcTQRJ539EGPB3Pb9ApvVRAY1U/WkHXmmYISqU5E79uCwcW7xYUV38gwZD+RV755fu3Q==",
"dev": true,
- "license": "MIT",
"dependencies": {
- "@vitest/utils": "4.0.15",
+ "@vitest/utils": "4.0.16",
"pathe": "^2.0.3"
},
"funding": {
@@ -6578,17 +6337,15 @@
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
- "dev": true,
- "license": "MIT"
+ "dev": true
},
"node_modules/@vitest/snapshot": {
- "version": "4.0.15",
- "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.15.tgz",
- "integrity": "sha512-A7Ob8EdFZJIBjLjeO0DZF4lqR6U7Ydi5/5LIZ0xcI+23lYlsYJAfGn8PrIWTYdZQRNnSRlzhg0zyGu37mVdy5g==",
+ "version": "4.0.16",
+ "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.16.tgz",
+ "integrity": "sha512-sf6NcrYhYBsSYefxnry+DR8n3UV4xWZwWxYbCJUt2YdvtqzSPR7VfGrY0zsv090DAbjFZsi7ZaMi1KnSRyK1XA==",
"dev": true,
- "license": "MIT",
"dependencies": {
- "@vitest/pretty-format": "4.0.15",
+ "@vitest/pretty-format": "4.0.16",
"magic-string": "^0.30.21",
"pathe": "^2.0.3"
},
@@ -6600,27 +6357,24 @@
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
- "dev": true,
- "license": "MIT"
+ "dev": true
},
"node_modules/@vitest/spy": {
- "version": "4.0.15",
- "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.15.tgz",
- "integrity": "sha512-+EIjOJmnY6mIfdXtE/bnozKEvTC4Uczg19yeZ2vtCz5Yyb0QQ31QWVQ8hswJ3Ysx/K2EqaNsVanjr//2+P3FHw==",
+ "version": "4.0.16",
+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.16.tgz",
+ "integrity": "sha512-4jIOWjKP0ZUaEmJm00E0cOBLU+5WE0BpeNr3XN6TEF05ltro6NJqHWxXD0kA8/Zc8Nh23AT8WQxwNG+WeROupw==",
"dev": true,
- "license": "MIT",
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/utils": {
- "version": "4.0.15",
- "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.15.tgz",
- "integrity": "sha512-HXjPW2w5dxhTD0dLwtYHDnelK3j8sR8cWIaLxr22evTyY6q8pRCjZSmhRWVjBaOVXChQd6AwMzi9pucorXCPZA==",
+ "version": "4.0.16",
+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.16.tgz",
+ "integrity": "sha512-h8z9yYhV3e1LEfaQ3zdypIrnAg/9hguReGZoS7Gl0aBG5xgA410zBqECqmaF/+RkTggRsfnzc1XaAHA6bmUufA==",
"dev": true,
- "license": "MIT",
"dependencies": {
- "@vitest/pretty-format": "4.0.15",
+ "@vitest/pretty-format": "4.0.16",
"tinyrainbow": "^3.0.3"
},
"funding": {
@@ -6961,7 +6715,6 @@
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
"integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
"dev": true,
- "license": "MIT",
"engines": {
"node": ">=12"
}
@@ -7357,7 +7110,6 @@
"resolved": "https://registry.npmjs.org/chai/-/chai-6.2.1.tgz",
"integrity": "sha512-p4Z49OGG5W/WBCPSS/dH3jQ73kD6tiMmUM+bckNK6Jr5JHMG3k9bg/BvKR8lKmtVBKmOiuVaV2ws8s9oSbwysg==",
"dev": true,
- "license": "MIT",
"engines": {
"node": ">=18"
}
@@ -7568,7 +7320,6 @@
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
"integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
- "license": "MIT",
"dependencies": {
"color-convert": "^2.0.1",
"color-string": "^1.9.0"
@@ -7608,8 +7359,7 @@
"node_modules/color2k": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/color2k/-/color2k-2.0.3.tgz",
- "integrity": "sha512-zW190nQTIoXcGCaU08DvVNFTmQhUpnJfVuAKfWqUQkflXKpaDdpaYoM0iluLS9lgJNHyBF58KKA2FBEwkD7wog==",
- "license": "MIT"
+ "integrity": "sha512-zW190nQTIoXcGCaU08DvVNFTmQhUpnJfVuAKfWqUQkflXKpaDdpaYoM0iluLS9lgJNHyBF58KKA2FBEwkD7wog=="
},
"node_modules/colorette": {
"version": "2.0.20",
@@ -8045,7 +7795,6 @@
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
- "license": "MIT",
"engines": {
"node": ">=0.10.0"
}
@@ -8553,11 +8302,10 @@
}
},
"node_modules/esbuild": {
- "version": "0.25.12",
- "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
- "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
+ "version": "0.27.1",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.1.tgz",
+ "integrity": "sha512-yY35KZckJJuVVPXpvjgxiCuVEJT67F6zDeVTv4rizyPrfGBUpZQsvmxnN+C371c2esD/hNMjj4tpBhuueLN7aA==",
"hasInstallScript": true,
- "license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
@@ -8565,32 +8313,32 @@
"node": ">=18"
},
"optionalDependencies": {
- "@esbuild/aix-ppc64": "0.25.12",
- "@esbuild/android-arm": "0.25.12",
- "@esbuild/android-arm64": "0.25.12",
- "@esbuild/android-x64": "0.25.12",
- "@esbuild/darwin-arm64": "0.25.12",
- "@esbuild/darwin-x64": "0.25.12",
- "@esbuild/freebsd-arm64": "0.25.12",
- "@esbuild/freebsd-x64": "0.25.12",
- "@esbuild/linux-arm": "0.25.12",
- "@esbuild/linux-arm64": "0.25.12",
- "@esbuild/linux-ia32": "0.25.12",
- "@esbuild/linux-loong64": "0.25.12",
- "@esbuild/linux-mips64el": "0.25.12",
- "@esbuild/linux-ppc64": "0.25.12",
- "@esbuild/linux-riscv64": "0.25.12",
- "@esbuild/linux-s390x": "0.25.12",
- "@esbuild/linux-x64": "0.25.12",
- "@esbuild/netbsd-arm64": "0.25.12",
- "@esbuild/netbsd-x64": "0.25.12",
- "@esbuild/openbsd-arm64": "0.25.12",
- "@esbuild/openbsd-x64": "0.25.12",
- "@esbuild/openharmony-arm64": "0.25.12",
- "@esbuild/sunos-x64": "0.25.12",
- "@esbuild/win32-arm64": "0.25.12",
- "@esbuild/win32-ia32": "0.25.12",
- "@esbuild/win32-x64": "0.25.12"
+ "@esbuild/aix-ppc64": "0.27.1",
+ "@esbuild/android-arm": "0.27.1",
+ "@esbuild/android-arm64": "0.27.1",
+ "@esbuild/android-x64": "0.27.1",
+ "@esbuild/darwin-arm64": "0.27.1",
+ "@esbuild/darwin-x64": "0.27.1",
+ "@esbuild/freebsd-arm64": "0.27.1",
+ "@esbuild/freebsd-x64": "0.27.1",
+ "@esbuild/linux-arm": "0.27.1",
+ "@esbuild/linux-arm64": "0.27.1",
+ "@esbuild/linux-ia32": "0.27.1",
+ "@esbuild/linux-loong64": "0.27.1",
+ "@esbuild/linux-mips64el": "0.27.1",
+ "@esbuild/linux-ppc64": "0.27.1",
+ "@esbuild/linux-riscv64": "0.27.1",
+ "@esbuild/linux-s390x": "0.27.1",
+ "@esbuild/linux-x64": "0.27.1",
+ "@esbuild/netbsd-arm64": "0.27.1",
+ "@esbuild/netbsd-x64": "0.27.1",
+ "@esbuild/openbsd-arm64": "0.27.1",
+ "@esbuild/openbsd-x64": "0.27.1",
+ "@esbuild/openharmony-arm64": "0.27.1",
+ "@esbuild/sunos-x64": "0.27.1",
+ "@esbuild/win32-arm64": "0.27.1",
+ "@esbuild/win32-ia32": "0.27.1",
+ "@esbuild/win32-x64": "0.27.1"
}
},
"node_modules/escalade": {
@@ -9557,7 +9305,6 @@
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz",
"integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==",
- "license": "BSD-3-Clause",
"bin": {
"flat": "cli.js"
}
@@ -10310,9 +10057,9 @@
}
},
"node_modules/i18next": {
- "version": "25.7.2",
- "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.7.2.tgz",
- "integrity": "sha512-58b4kmLpLv1buWUEwegMDUqZVR5J+rT+WTRFaBGL7lxDuJQQ0NrJFrq+eT2N94aYVR1k1Sr13QITNOL88tZCuw==",
+ "version": "25.7.3",
+ "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.7.3.tgz",
+ "integrity": "sha512-2XaT+HpYGuc2uTExq9TVRhLsso+Dxym6PWaKpn36wfBmTI779OQ7iP/XaZHzrnGyzU4SHpFrTYLKfVyBfAhVNA==",
"funding": [
{
"type": "individual",
@@ -10327,7 +10074,6 @@
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
}
],
- "license": "MIT",
"dependencies": {
"@babel/runtime": "^7.28.4"
},
@@ -10445,7 +10191,6 @@
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/input-otp/-/input-otp-1.4.1.tgz",
"integrity": "sha512-+yvpmKYKHi9jIGngxagY9oWiiblPB7+nEO75F2l2o4vs+6vpPZZmUl4tBNYuTCvQjhvEIbdNeJu70bhfYP2nbw==",
- "license": "MIT",
"peerDependencies": {
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc"
@@ -10470,7 +10215,6 @@
"version": "10.7.18",
"resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.7.18.tgz",
"integrity": "sha512-m3Ofv/X/tV8Y3tHXLohcuVuhWKo7BBq62cqY15etqmLxg2DZ34AGGgQDeR+SCta2+zICb1NX83af0GJmbQ1++g==",
- "license": "BSD-3-Clause",
"dependencies": {
"@formatjs/ecma402-abstract": "2.3.6",
"@formatjs/fast-memoize": "2.2.7",
@@ -13659,11 +13403,11 @@
}
},
"node_modules/posthog-js": {
- "version": "1.306.0",
- "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.306.0.tgz",
- "integrity": "sha512-sjsy0El4HL6PgbyWiUF0CaKb2d1Q8okbSeT4eajan3QSvkWus6ygHQuW2l4lfvp6NLRQrIZKH/0sUanhASptUQ==",
+ "version": "1.309.0",
+ "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.309.0.tgz",
+ "integrity": "sha512-SmFF0uKX3tNTgQOW4mR4shGLQ0YYG0FXyKTz13SbIH83/FtAJedppOIL7s0y9e7rjogBh6LsPekphhchs9Kh1Q==",
"dependencies": {
- "@posthog/core": "1.7.1",
+ "@posthog/core": "1.8.0",
"core-js": "^3.38.1",
"fflate": "^0.4.8",
"preact": "^10.19.3",
@@ -14594,7 +14338,6 @@
"version": "3.0.10",
"resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.0.10.tgz",
"integrity": "sha512-t44QCeDKAPf1mtQH3fYpWz8IM/DyvHLjs8wUvvwMYxk5moOqCzrMSxK6HQVD0QVmVjXFavoFIPRVrMuJPKAvtg==",
- "license": "MIT",
"dependencies": {
"compute-scroll-into-view": "^3.0.2"
}
@@ -15538,10 +15281,9 @@
}
},
"node_modules/tailwind-variants": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/tailwind-variants/-/tailwind-variants-3.1.1.tgz",
- "integrity": "sha512-ftLXe3krnqkMHsuBTEmaVUXYovXtPyTK7ckEfDRXS8PBZx0bAUas+A0jYxuKA5b8qg++wvQ3d2MQ7l/xeZxbZQ==",
- "license": "MIT",
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/tailwind-variants/-/tailwind-variants-3.2.2.tgz",
+ "integrity": "sha512-Mi4kHeMTLvKlM98XPnK+7HoBPmf4gygdFmqQPaDivc3DpYS6aIY6KiG/PgThrGvii5YZJqRsPz0aPyhoFzmZgg==",
"engines": {
"node": ">=16.x",
"pnpm": ">=7.x"
@@ -16251,12 +15993,11 @@
}
},
"node_modules/vite": {
- "version": "7.2.7",
- "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.7.tgz",
- "integrity": "sha512-ITcnkFeR3+fI8P1wMgItjGrR10170d8auB4EpMLPqmx6uxElH3a/hHGQabSHKdqd4FXWO1nFIp9rRn7JQ34ACQ==",
- "license": "MIT",
+ "version": "7.3.0",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.0.tgz",
+ "integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==",
"dependencies": {
- "esbuild": "^0.25.0",
+ "esbuild": "^0.27.0",
"fdir": "^6.5.0",
"picomatch": "^4.0.3",
"postcss": "^8.5.6",
@@ -16370,11 +16111,10 @@
}
},
"node_modules/vite-tsconfig-paths": {
- "version": "5.1.4",
- "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-5.1.4.tgz",
- "integrity": "sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==",
+ "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==",
"dev": true,
- "license": "MIT",
"dependencies": {
"debug": "^4.1.1",
"globrex": "^0.1.2",
@@ -16433,19 +16173,18 @@
}
},
"node_modules/vitest": {
- "version": "4.0.15",
- "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.15.tgz",
- "integrity": "sha512-n1RxDp8UJm6N0IbJLQo+yzLZ2sQCDyl1o0LeugbPWf8+8Fttp29GghsQBjYJVmWq3gBFfe9Hs1spR44vovn2wA==",
+ "version": "4.0.16",
+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.16.tgz",
+ "integrity": "sha512-E4t7DJ9pESL6E3I8nFjPa4xGUd3PmiWDLsDztS2qXSJWfHtbQnwAWylaBvSNY48I3vr8PTqIZlyK8TE3V3CA4Q==",
"dev": true,
- "license": "MIT",
"dependencies": {
- "@vitest/expect": "4.0.15",
- "@vitest/mocker": "4.0.15",
- "@vitest/pretty-format": "4.0.15",
- "@vitest/runner": "4.0.15",
- "@vitest/snapshot": "4.0.15",
- "@vitest/spy": "4.0.15",
- "@vitest/utils": "4.0.15",
+ "@vitest/expect": "4.0.16",
+ "@vitest/mocker": "4.0.16",
+ "@vitest/pretty-format": "4.0.16",
+ "@vitest/runner": "4.0.16",
+ "@vitest/snapshot": "4.0.16",
+ "@vitest/spy": "4.0.16",
+ "@vitest/utils": "4.0.16",
"es-module-lexer": "^1.7.0",
"expect-type": "^1.2.2",
"magic-string": "^0.30.21",
@@ -16473,10 +16212,10 @@
"@edge-runtime/vm": "*",
"@opentelemetry/api": "^1.9.0",
"@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
- "@vitest/browser-playwright": "4.0.15",
- "@vitest/browser-preview": "4.0.15",
- "@vitest/browser-webdriverio": "4.0.15",
- "@vitest/ui": "4.0.15",
+ "@vitest/browser-playwright": "4.0.16",
+ "@vitest/browser-preview": "4.0.16",
+ "@vitest/browser-webdriverio": "4.0.16",
+ "@vitest/ui": "4.0.16",
"happy-dom": "*",
"jsdom": "*"
},
diff --git a/frontend/package.json b/frontend/package.json
index 64892afc5c..90636fed77 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -1,13 +1,13 @@
{
"name": "openhands-frontend",
- "version": "0.62.0",
+ "version": "1.0.0",
"private": true,
"type": "module",
"engines": {
"node": ">=22.0.0"
},
"dependencies": {
- "@heroui/react": "2.8.5",
+ "@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",
@@ -23,13 +23,13 @@
"downshift": "^9.0.13",
"eslint-config-airbnb-typescript": "^18.0.0",
"framer-motion": "^12.23.25",
- "i18next": "^25.7.2",
+ "i18next": "^25.7.3",
"i18next-browser-languagedetector": "^8.2.0",
"i18next-http-backend": "^3.0.2",
"isbot": "^5.1.32",
"lucide-react": "^0.561.0",
"monaco-editor": "^0.55.1",
- "posthog-js": "^1.306.0",
+ "posthog-js": "^1.309.0",
"react": "^19.2.3",
"react-dom": "^19.2.3",
"react-hot-toast": "^2.6.0",
@@ -44,7 +44,7 @@
"socket.io-client": "^4.8.1",
"tailwind-merge": "^3.4.0",
"tailwind-scrollbar": "^4.0.2",
- "vite": "^7.2.7",
+ "vite": "^7.3.0",
"zustand": "^5.0.9"
},
"scripts": {
@@ -87,15 +87,15 @@
"@tanstack/eslint-plugin-query": "^5.91.0",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.9.1",
- "@testing-library/react": "^16.3.0",
+ "@testing-library/react": "^16.3.1",
"@testing-library/user-event": "^14.6.1",
- "@types/node": "^25.0.1",
+ "@types/node": "^25.0.3",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"@types/react-syntax-highlighter": "^15.5.13",
"@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0",
- "@vitest/coverage-v8": "^4.0.14",
+ "@vitest/coverage-v8": "^4.0.16",
"cross-env": "^10.1.0",
"eslint": "^8.57.0",
"eslint-config-airbnb": "^19.0.4",
@@ -116,7 +116,7 @@
"tailwindcss": "^4.1.8",
"typescript": "^5.9.3",
"vite-plugin-svgr": "^4.5.0",
- "vite-tsconfig-paths": "^5.1.4",
+ "vite-tsconfig-paths": "^6.0.2",
"vitest": "^4.0.14"
},
"packageManager": "npm@10.5.0",
diff --git a/frontend/src/api/conversation-service/v1-conversation-service.api.ts b/frontend/src/api/conversation-service/v1-conversation-service.api.ts
index bd37fa8180..d2f8f51ff5 100644
--- a/frontend/src/api/conversation-service/v1-conversation-service.api.ts
+++ b/frontend/src/api/conversation-service/v1-conversation-service.api.ts
@@ -11,6 +11,7 @@ import type {
V1AppConversationStartTask,
V1AppConversationStartTaskPage,
V1AppConversation,
+ GetSkillsResponse,
} from "./v1-conversation-service.types";
class V1ConversationService {
@@ -315,6 +316,18 @@ class V1ConversationService {
);
return data;
}
+
+ /**
+ * Get all skills associated with a V1 conversation
+ * @param conversationId The conversation ID
+ * @returns The available skills associated with the conversation
+ */
+ static async getSkills(conversationId: string): Promise {
+ const { data } = await openHands.get(
+ `/api/v1/app-conversations/${conversationId}/skills`,
+ );
+ return data;
+ }
}
export default V1ConversationService;
diff --git a/frontend/src/api/conversation-service/v1-conversation-service.types.ts b/frontend/src/api/conversation-service/v1-conversation-service.types.ts
index 621283c274..7c8b04ccbf 100644
--- a/frontend/src/api/conversation-service/v1-conversation-service.types.ts
+++ b/frontend/src/api/conversation-service/v1-conversation-service.types.ts
@@ -99,3 +99,14 @@ export interface V1AppConversation {
conversation_url: string | null;
session_api_key: string | null;
}
+
+export interface Skill {
+ name: string;
+ type: "repo" | "knowledge";
+ content: string;
+ triggers: string[];
+}
+
+export interface GetSkillsResponse {
+ skills: Skill[];
+}
diff --git a/frontend/src/components/features/controls/agent-status.tsx b/frontend/src/components/features/controls/agent-status.tsx
index 078eb5f40f..675b5881a3 100644
--- a/frontend/src/components/features/controls/agent-status.tsx
+++ b/frontend/src/components/features/controls/agent-status.tsx
@@ -59,13 +59,15 @@ export function AgentStatus({
);
const shouldShownAgentLoading =
- isPausing ||
curAgentState === AgentState.INIT ||
curAgentState === AgentState.LOADING ||
(webSocketStatus === "CONNECTING" && taskStatus !== "ERROR") ||
isTaskPolling(taskStatus) ||
isTaskPolling(subConversationTaskStatus);
+ // For UI rendering - includes pause state
+ const isLoading = shouldShownAgentLoading || isPausing;
+
const shouldShownAgentError =
curAgentState === AgentState.ERROR ||
curAgentState === AgentState.RATE_LIMITED ||
@@ -93,25 +95,28 @@ export function AgentStatus({
- {shouldShownAgentLoading &&
}
- {!shouldShownAgentLoading && shouldShownAgentStop && (
+ {isLoading &&
}
+ {!isLoading && shouldShownAgentStop && (
)}
- {!shouldShownAgentLoading && shouldShownAgentResume && (
+ {!isLoading && shouldShownAgentResume && (
)}
- {!shouldShownAgentLoading && shouldShownAgentError && (
-
+ {!isLoading && shouldShownAgentError && (
+
)}
- {!shouldShownAgentLoading &&
+ {!isLoading &&
!shouldShownAgentStop &&
!shouldShownAgentResume &&
!shouldShownAgentError &&
}
diff --git a/frontend/src/components/features/controls/tools-context-menu.tsx b/frontend/src/components/features/controls/tools-context-menu.tsx
index 39330e25e4..2089f95111 100644
--- a/frontend/src/components/features/controls/tools-context-menu.tsx
+++ b/frontend/src/components/features/controls/tools-context-menu.tsx
@@ -26,14 +26,14 @@ const contextMenuListItemClassName = cn(
interface ToolsContextMenuProps {
onClose: () => void;
- onShowMicroagents: (event: React.MouseEvent
) => void;
+ onShowSkills: (event: React.MouseEvent) => void;
onShowAgentTools: (event: React.MouseEvent) => void;
shouldShowAgentTools?: boolean;
}
export function ToolsContextMenu({
onClose,
- onShowMicroagents,
+ onShowSkills,
onShowAgentTools,
shouldShowAgentTools = true,
}: ToolsContextMenuProps) {
@@ -41,7 +41,6 @@ export function ToolsContextMenu({
const { data: conversation } = useActiveConversation();
const { providers } = useUserProviders();
- // TODO: Hide microagent menu items for V1 conversations
// This is a temporary measure and may be re-enabled in the future
const isV1Conversation = conversation?.conversation_version === "V1";
@@ -130,20 +129,17 @@ export function ToolsContextMenu({
{(!isV1Conversation || shouldShowAgentTools) && }
- {/* Show Available Microagents - Hidden for V1 conversations */}
- {!isV1Conversation && (
-
- }
- text={t(I18nKey.CONVERSATION$SHOW_MICROAGENTS)}
- className={CONTEXT_MENU_ICON_TEXT_CLASSNAME}
- />
-
- )}
+
+ }
+ text={t(I18nKey.CONVERSATION$SHOW_SKILLS)}
+ className={CONTEXT_MENU_ICON_TEXT_CLASSNAME}
+ />
+
{/* Show Agent Tools and Metadata - Only show if system message is available */}
{shouldShowAgentTools && (
diff --git a/frontend/src/components/features/controls/tools.tsx b/frontend/src/components/features/controls/tools.tsx
index 56ef58bc8e..80994cbe65 100644
--- a/frontend/src/components/features/controls/tools.tsx
+++ b/frontend/src/components/features/controls/tools.tsx
@@ -7,7 +7,7 @@ import { ToolsContextMenu } from "./tools-context-menu";
import { useConversationNameContextMenu } from "#/hooks/use-conversation-name-context-menu";
import { useActiveConversation } from "#/hooks/query/use-active-conversation";
import { SystemMessageModal } from "../conversation-panel/system-message-modal";
-import { MicroagentsModal } from "../conversation-panel/microagents-modal";
+import { SkillsModal } from "../conversation-panel/skills-modal";
export function Tools() {
const { t } = useTranslation();
@@ -17,11 +17,11 @@ export function Tools() {
const {
handleShowAgentTools,
- handleShowMicroagents,
+ handleShowSkills,
systemModalVisible,
setSystemModalVisible,
- microagentsModalVisible,
- setMicroagentsModalVisible,
+ skillsModalVisible,
+ setSkillsModalVisible,
systemMessage,
shouldShowAgentTools,
} = useConversationNameContextMenu({
@@ -51,7 +51,7 @@ export function Tools() {
{contextMenuOpen && (
setContextMenuOpen(false)}
- onShowMicroagents={handleShowMicroagents}
+ onShowSkills={handleShowSkills}
onShowAgentTools={handleShowAgentTools}
shouldShowAgentTools={shouldShowAgentTools}
/>
@@ -64,9 +64,9 @@ export function Tools() {
systemMessage={systemMessage ? systemMessage.args : null}
/>
- {/* Microagents Modal */}
- {microagentsModalVisible && (
- setMicroagentsModalVisible(false)} />
+ {/* Skills Modal */}
+ {skillsModalVisible && (
+ setSkillsModalVisible(false)} />
)}
);
diff --git a/frontend/src/components/features/conversation-panel/conversation-card-context-menu.tsx b/frontend/src/components/features/conversation-panel/conversation-card-context-menu.tsx
deleted file mode 100644
index 63ea33152b..0000000000
--- a/frontend/src/components/features/conversation-panel/conversation-card-context-menu.tsx
+++ /dev/null
@@ -1,147 +0,0 @@
-import {
- Trash,
- Power,
- Pencil,
- Download,
- Wallet,
- Wrench,
- Bot,
-} from "lucide-react";
-import { useTranslation } from "react-i18next";
-import { useClickOutsideElement } from "#/hooks/use-click-outside-element";
-import { cn } from "#/utils/utils";
-import { ContextMenu } from "#/ui/context-menu";
-import { ContextMenuListItem } from "../context-menu/context-menu-list-item";
-import { Divider } from "#/ui/divider";
-import { I18nKey } from "#/i18n/declaration";
-import { ContextMenuIconText } from "../context-menu/context-menu-icon-text";
-import { useActiveConversation } from "#/hooks/query/use-active-conversation";
-
-interface ConversationCardContextMenuProps {
- onClose: () => void;
- onDelete?: (event: React.MouseEvent) => void;
- onStop?: (event: React.MouseEvent) => void;
- onEdit?: (event: React.MouseEvent) => void;
- onDisplayCost?: (event: React.MouseEvent) => void;
- onShowAgentTools?: (event: React.MouseEvent) => void;
- onShowMicroagents?: (event: React.MouseEvent) => void;
- onDownloadViaVSCode?: (event: React.MouseEvent) => void;
- position?: "top" | "bottom";
-}
-
-export function ConversationCardContextMenu({
- onClose,
- onDelete,
- onStop,
- onEdit,
- onDisplayCost,
- onShowAgentTools,
- onShowMicroagents,
- onDownloadViaVSCode,
- position = "bottom",
-}: ConversationCardContextMenuProps) {
- const { t } = useTranslation();
- const ref = useClickOutsideElement(onClose);
- const { data: conversation } = useActiveConversation();
-
- // TODO: Hide microagent menu items for V1 conversations
- // This is a temporary measure and may be re-enabled in the future
- const isV1Conversation = conversation?.conversation_version === "V1";
-
- const hasEdit = Boolean(onEdit);
- const hasDownload = Boolean(onDownloadViaVSCode);
- const hasTools = Boolean(onShowAgentTools || onShowMicroagents);
- const hasInfo = Boolean(onDisplayCost);
- const hasControl = Boolean(onStop || onDelete);
-
- return (
-
- {onEdit && (
-
-
-
- )}
-
- {hasEdit && (hasDownload || hasTools || hasInfo || hasControl) && (
-
- )}
-
- {onDownloadViaVSCode && (
-
-
-
- )}
-
- {hasDownload && (hasTools || hasInfo || hasControl) && }
-
- {onShowAgentTools && (
-
-
-
- )}
-
- {onShowMicroagents && !isV1Conversation && (
-
-
-
- )}
-
- {hasTools && (hasInfo || hasControl) && }
-
- {onDisplayCost && (
-
-
-
- )}
-
- {hasInfo && hasControl && }
-
- {onStop && (
-
-
-
- )}
-
- {onDelete && (
-
-
-
- )}
-
- );
-}
diff --git a/frontend/src/components/features/conversation-panel/conversation-card/conversation-card-context-menu.tsx b/frontend/src/components/features/conversation-panel/conversation-card/conversation-card-context-menu.tsx
index 6565a83a10..30a7ec42cb 100644
--- a/frontend/src/components/features/conversation-panel/conversation-card/conversation-card-context-menu.tsx
+++ b/frontend/src/components/features/conversation-panel/conversation-card/conversation-card-context-menu.tsx
@@ -22,7 +22,7 @@ interface ConversationCardContextMenuProps {
onEdit?: (event: React.MouseEvent) => void;
onDisplayCost?: (event: React.MouseEvent) => void;
onShowAgentTools?: (event: React.MouseEvent) => void;
- onShowMicroagents?: (event: React.MouseEvent) => void;
+ onShowSkills?: (event: React.MouseEvent) => void;
onDownloadViaVSCode?: (event: React.MouseEvent) => void;
position?: "top" | "bottom";
}
@@ -37,7 +37,7 @@ export function ConversationCardContextMenu({
onEdit,
onDisplayCost,
onShowAgentTools,
- onShowMicroagents,
+ onShowSkills,
onDownloadViaVSCode,
position = "bottom",
}: ConversationCardContextMenuProps) {
@@ -96,15 +96,15 @@ export function ConversationCardContextMenu({
/>
),
- onShowMicroagents && (
+ onShowSkills && (
}
- text={t(I18nKey.CONVERSATION$SHOW_MICROAGENTS)}
+ text={t(I18nKey.CONVERSATION$SHOW_SKILLS)}
/>
),
diff --git a/frontend/src/components/features/conversation-panel/microagent-content.tsx b/frontend/src/components/features/conversation-panel/skill-content.tsx
similarity index 76%
rename from frontend/src/components/features/conversation-panel/microagent-content.tsx
rename to frontend/src/components/features/conversation-panel/skill-content.tsx
index fad0485607..9303047e3a 100644
--- a/frontend/src/components/features/conversation-panel/microagent-content.tsx
+++ b/frontend/src/components/features/conversation-panel/skill-content.tsx
@@ -3,17 +3,17 @@ import { I18nKey } from "#/i18n/declaration";
import { Typography } from "#/ui/typography";
import { Pre } from "#/ui/pre";
-interface MicroagentContentProps {
+interface SkillContentProps {
content: string;
}
-export function MicroagentContent({ content }: MicroagentContentProps) {
+export function SkillContent({ content }: SkillContentProps) {
const { t } = useTranslation();
return (
- {t(I18nKey.MICROAGENTS_MODAL$CONTENT)}
+ {t(I18nKey.COMMON$CONTENT)}
- {content || t(I18nKey.MICROAGENTS_MODAL$NO_CONTENT)}
+ {content || t(I18nKey.SKILLS_MODAL$NO_CONTENT)}
);
diff --git a/frontend/src/components/features/conversation-panel/microagent-item.tsx b/frontend/src/components/features/conversation-panel/skill-item.tsx
similarity index 65%
rename from frontend/src/components/features/conversation-panel/microagent-item.tsx
rename to frontend/src/components/features/conversation-panel/skill-item.tsx
index d23febb099..c76bf10be9 100644
--- a/frontend/src/components/features/conversation-panel/microagent-item.tsx
+++ b/frontend/src/components/features/conversation-panel/skill-item.tsx
@@ -1,35 +1,31 @@
import { ChevronDown, ChevronRight } from "lucide-react";
-import { Microagent } from "#/api/open-hands.types";
import { Typography } from "#/ui/typography";
-import { MicroagentTriggers } from "./microagent-triggers";
-import { MicroagentContent } from "./microagent-content";
+import { SkillTriggers } from "./skill-triggers";
+import { SkillContent } from "./skill-content";
+import { Skill } from "#/api/conversation-service/v1-conversation-service.types";
-interface MicroagentItemProps {
- agent: Microagent;
+interface SkillItemProps {
+ skill: Skill;
isExpanded: boolean;
onToggle: (agentName: string) => void;
}
-export function MicroagentItem({
- agent,
- isExpanded,
- onToggle,
-}: MicroagentItemProps) {
+export function SkillItem({ skill, isExpanded, onToggle }: SkillItemProps) {
return (
onToggle(agent.name)}
+ onClick={() => onToggle(skill.name)}
className="w-full py-3 px-2 text-left flex items-center justify-between hover:bg-gray-700 transition-colors"
>
- {agent.name}
+ {skill.name}
- {agent.type === "repo" ? "Repository" : "Knowledge"}
+ {skill.type === "repo" ? "Repository" : "Knowledge"}
{isExpanded ? (
@@ -43,8 +39,8 @@ export function MicroagentItem({
{isExpanded && (
-
-
+
+
)}
diff --git a/frontend/src/components/features/conversation-panel/microagent-triggers.tsx b/frontend/src/components/features/conversation-panel/skill-triggers.tsx
similarity index 81%
rename from frontend/src/components/features/conversation-panel/microagent-triggers.tsx
rename to frontend/src/components/features/conversation-panel/skill-triggers.tsx
index ef2b547b7b..f2fa51d3a2 100644
--- a/frontend/src/components/features/conversation-panel/microagent-triggers.tsx
+++ b/frontend/src/components/features/conversation-panel/skill-triggers.tsx
@@ -2,11 +2,11 @@ import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
import { Typography } from "#/ui/typography";
-interface MicroagentTriggersProps {
+interface SkillTriggersProps {
triggers: string[];
}
-export function MicroagentTriggers({ triggers }: MicroagentTriggersProps) {
+export function SkillTriggers({ triggers }: SkillTriggersProps) {
const { t } = useTranslation();
if (!triggers || triggers.length === 0) {
@@ -16,7 +16,7 @@ export function MicroagentTriggers({ triggers }: MicroagentTriggersProps) {
return (
- {t(I18nKey.MICROAGENTS_MODAL$TRIGGERS)}
+ {t(I18nKey.COMMON$TRIGGERS)}
{triggers.map((trigger) => (
diff --git a/frontend/src/components/features/conversation-panel/microagents-empty-state.tsx b/frontend/src/components/features/conversation-panel/skills-empty-state.tsx
similarity index 63%
rename from frontend/src/components/features/conversation-panel/microagents-empty-state.tsx
rename to frontend/src/components/features/conversation-panel/skills-empty-state.tsx
index 5ef535e178..5a148568a4 100644
--- a/frontend/src/components/features/conversation-panel/microagents-empty-state.tsx
+++ b/frontend/src/components/features/conversation-panel/skills-empty-state.tsx
@@ -2,19 +2,19 @@ import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
import { Typography } from "#/ui/typography";
-interface MicroagentsEmptyStateProps {
+interface SkillsEmptyStateProps {
isError: boolean;
}
-export function MicroagentsEmptyState({ isError }: MicroagentsEmptyStateProps) {
+export function SkillsEmptyState({ isError }: SkillsEmptyStateProps) {
const { t } = useTranslation();
return (
{isError
- ? t(I18nKey.MICROAGENTS_MODAL$FETCH_ERROR)
- : t(I18nKey.CONVERSATION$NO_MICROAGENTS)}
+ ? t(I18nKey.COMMON$FETCH_ERROR)
+ : t(I18nKey.CONVERSATION$NO_SKILLS)}
);
diff --git a/frontend/src/components/features/conversation-panel/microagents-loading-state.tsx b/frontend/src/components/features/conversation-panel/skills-loading-state.tsx
similarity index 80%
rename from frontend/src/components/features/conversation-panel/microagents-loading-state.tsx
rename to frontend/src/components/features/conversation-panel/skills-loading-state.tsx
index 9851b82bed..29ea6c9754 100644
--- a/frontend/src/components/features/conversation-panel/microagents-loading-state.tsx
+++ b/frontend/src/components/features/conversation-panel/skills-loading-state.tsx
@@ -1,4 +1,4 @@
-export function MicroagentsLoadingState() {
+export function SkillsLoadingState() {
return (
diff --git a/frontend/src/components/features/conversation-panel/microagents-modal-header.tsx b/frontend/src/components/features/conversation-panel/skills-modal-header.tsx
similarity index 82%
rename from frontend/src/components/features/conversation-panel/microagents-modal-header.tsx
rename to frontend/src/components/features/conversation-panel/skills-modal-header.tsx
index 858f877287..0a0a93fee4 100644
--- a/frontend/src/components/features/conversation-panel/microagents-modal-header.tsx
+++ b/frontend/src/components/features/conversation-panel/skills-modal-header.tsx
@@ -4,28 +4,28 @@ import { BaseModalTitle } from "#/components/shared/modals/confirmation-modals/b
import { I18nKey } from "#/i18n/declaration";
import { BrandButton } from "../settings/brand-button";
-interface MicroagentsModalHeaderProps {
+interface SkillsModalHeaderProps {
isAgentReady: boolean;
isLoading: boolean;
isRefetching: boolean;
onRefresh: () => void;
}
-export function MicroagentsModalHeader({
+export function SkillsModalHeader({
isAgentReady,
isLoading,
isRefetching,
onRefresh,
-}: MicroagentsModalHeaderProps) {
+}: SkillsModalHeaderProps) {
const { t } = useTranslation();
return (
-
+
{isAgentReady && (
void;
}
-export function MicroagentsModal({ onClose }: MicroagentsModalProps) {
+export function SkillsModal({ onClose }: SkillsModalProps) {
const { t } = useTranslation();
const { curAgentState } = useAgentState();
- const { data: conversation } = useActiveConversation();
const [expandedAgents, setExpandedAgents] = useState>(
{},
);
const {
- data: microagents,
+ data: skills,
isLoading,
isError,
refetch,
isRefetching,
- } = useConversationMicroagents();
-
- // TODO: Hide MicroagentsModal for V1 conversations
- // This is a temporary measure and may be re-enabled in the future
- const isV1Conversation = conversation?.conversation_version === "V1";
-
- // Don't render anything for V1 conversations
- if (isV1Conversation) {
- return null;
- }
+ } = useConversationSkills();
const toggleAgent = (agentName: string) => {
setExpandedAgents((prev) => ({
@@ -57,9 +46,9 @@ export function MicroagentsModal({ onClose }: MicroagentsModalProps) {
-
- {t(I18nKey.MICROAGENTS_MODAL$WARNING)}
+ {t(I18nKey.SKILLS_MODAL$WARNING)}
)}
@@ -81,33 +70,30 @@ export function MicroagentsModal({ onClose }: MicroagentsModalProps) {
)}
- {isLoading &&
}
+ {isLoading &&
}
{!isLoading &&
isAgentReady &&
- (isError || !microagents || microagents.length === 0) && (
-
+ (isError || !skills || skills.length === 0) && (
+
)}
- {!isLoading &&
- isAgentReady &&
- microagents &&
- microagents.length > 0 && (
-
- {microagents.map((agent) => {
- const isExpanded = expandedAgents[agent.name] || false;
+ {!isLoading && isAgentReady && skills && skills.length > 0 && (
+
+ {skills.map((skill) => {
+ const isExpanded = expandedAgents[skill.name] || false;
- return (
-
- );
- })}
-
- )}
+ return (
+
+ );
+ })}
+
+ )}
diff --git a/frontend/src/components/features/conversation/conversation-name-context-menu.tsx b/frontend/src/components/features/conversation/conversation-name-context-menu.tsx
index 97ade1edb5..95de15b37e 100644
--- a/frontend/src/components/features/conversation/conversation-name-context-menu.tsx
+++ b/frontend/src/components/features/conversation/conversation-name-context-menu.tsx
@@ -31,7 +31,7 @@ interface ConversationNameContextMenuProps {
onStop?: (event: React.MouseEvent
) => void;
onDisplayCost?: (event: React.MouseEvent) => void;
onShowAgentTools?: (event: React.MouseEvent) => void;
- onShowMicroagents?: (event: React.MouseEvent) => void;
+ onShowSkills?: (event: React.MouseEvent) => void;
onExportConversation?: (event: React.MouseEvent) => void;
onDownloadViaVSCode?: (event: React.MouseEvent) => void;
position?: "top" | "bottom";
@@ -44,7 +44,7 @@ export function ConversationNameContextMenu({
onStop,
onDisplayCost,
onShowAgentTools,
- onShowMicroagents,
+ onShowSkills,
onExportConversation,
onDownloadViaVSCode,
position = "bottom",
@@ -55,13 +55,12 @@ export function ConversationNameContextMenu({
const ref = useClickOutsideElement(onClose);
const { data: conversation } = useActiveConversation();
- // TODO: Hide microagent menu items for V1 conversations
// This is a temporary measure and may be re-enabled in the future
const isV1Conversation = conversation?.conversation_version === "V1";
const hasDownload = Boolean(onDownloadViaVSCode);
const hasExport = Boolean(onExportConversation);
- const hasTools = Boolean(onShowAgentTools || onShowMicroagents);
+ const hasTools = Boolean(onShowAgentTools || onShowSkills);
const hasInfo = Boolean(onDisplayCost);
const hasControl = Boolean(onStop || onDelete);
@@ -91,15 +90,15 @@ export function ConversationNameContextMenu({
{hasTools && }
- {onShowMicroagents && !isV1Conversation && (
+ {onShowSkills && (
}
- text={t(I18nKey.CONVERSATION$SHOW_MICROAGENTS)}
+ text={t(I18nKey.CONVERSATION$SHOW_SKILLS)}
className={CONTEXT_MENU_ICON_TEXT_CLASSNAME}
/>
diff --git a/frontend/src/components/features/conversation/conversation-name.tsx b/frontend/src/components/features/conversation/conversation-name.tsx
index 2b5c06398c..1dabfe259e 100644
--- a/frontend/src/components/features/conversation/conversation-name.tsx
+++ b/frontend/src/components/features/conversation/conversation-name.tsx
@@ -9,7 +9,7 @@ import { I18nKey } from "#/i18n/declaration";
import { EllipsisButton } from "../conversation-panel/ellipsis-button";
import { ConversationNameContextMenu } from "./conversation-name-context-menu";
import { SystemMessageModal } from "../conversation-panel/system-message-modal";
-import { MicroagentsModal } from "../conversation-panel/microagents-modal";
+import { SkillsModal } from "../conversation-panel/skills-modal";
import { ConfirmDeleteModal } from "../conversation-panel/confirm-delete-modal";
import { ConfirmStopModal } from "../conversation-panel/confirm-stop-modal";
import { MetricsModal } from "./metrics-modal/metrics-modal";
@@ -32,7 +32,7 @@ export function ConversationName() {
handleDownloadViaVSCode,
handleDisplayCost,
handleShowAgentTools,
- handleShowMicroagents,
+ handleShowSkills,
handleExportConversation,
handleConfirmDelete,
handleConfirmStop,
@@ -40,8 +40,8 @@ export function ConversationName() {
setMetricsModalVisible,
systemModalVisible,
setSystemModalVisible,
- microagentsModalVisible,
- setMicroagentsModalVisible,
+ skillsModalVisible,
+ setSkillsModalVisible,
confirmDeleteModalVisible,
setConfirmDeleteModalVisible,
confirmStopModalVisible,
@@ -52,7 +52,7 @@ export function ConversationName() {
shouldShowExport,
shouldShowDisplayCost,
shouldShowAgentTools,
- shouldShowMicroagents,
+ shouldShowSkills,
} = useConversationNameContextMenu({
conversationId,
conversationStatus: conversation?.status,
@@ -170,9 +170,7 @@ export function ConversationName() {
onShowAgentTools={
shouldShowAgentTools ? handleShowAgentTools : undefined
}
- onShowMicroagents={
- shouldShowMicroagents ? handleShowMicroagents : undefined
- }
+ onShowSkills={shouldShowSkills ? handleShowSkills : undefined}
onExportConversation={
shouldShowExport ? handleExportConversation : undefined
}
@@ -199,9 +197,9 @@ export function ConversationName() {
systemMessage={systemMessage ? systemMessage.args : null}
/>
- {/* Microagents Modal */}
- {microagentsModalVisible && (
- setMicroagentsModalVisible(false)} />
+ {/* Skills Modal */}
+ {skillsModalVisible && (
+ setSkillsModalVisible(false)} />
)}
{/* Confirm Delete Modal */}
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 70b45ea73a..39b68c9033 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
@@ -82,13 +82,45 @@ export function ConversationTabContent() {
isPlannerActive,
]);
+ const conversationKey = useMemo(() => {
+ if (isEditorActive) {
+ return "editor";
+ }
+ if (isBrowserActive) {
+ return "browser";
+ }
+ if (isServedActive) {
+ return "served";
+ }
+ if (isVSCodeActive) {
+ return "vscode";
+ }
+ if (isTerminalActive) {
+ return "terminal";
+ }
+ if (isPlannerActive) {
+ return "planner";
+ }
+ return "";
+ }, [
+ isEditorActive,
+ isBrowserActive,
+ isServedActive,
+ isVSCodeActive,
+ isTerminalActive,
+ isPlannerActive,
+ ]);
+
if (shouldShownAgentLoading) {
return ;
}
return (
-
+
{tabs.map(({ key, component: Component, isActive }) => (
{
+ refetch();
+ };
+
return (
{title}
+ {conversationKey === "editor" && (
+
+
+
+ )}
);
}
diff --git a/frontend/src/components/features/payment/setup-payment-modal.tsx b/frontend/src/components/features/payment/setup-payment-modal.tsx
index 30cb0a4e54..7d8883a719 100644
--- a/frontend/src/components/features/payment/setup-payment-modal.tsx
+++ b/frontend/src/components/features/payment/setup-payment-modal.tsx
@@ -1,24 +1,14 @@
-import { useMutation } from "@tanstack/react-query";
import { Trans, useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
import OpenHandsLogo from "#/assets/branding/openhands-logo.svg?react";
import { ModalBackdrop } from "#/components/shared/modals/modal-backdrop";
import { ModalBody } from "#/components/shared/modals/modal-body";
-import BillingService from "#/api/billing-service/billing-service.api";
import { BrandButton } from "../settings/brand-button";
-import { displayErrorToast } from "#/utils/custom-toast-handlers";
+import { useCreateBillingSession } from "#/hooks/mutation/use-create-billing-session";
export function SetupPaymentModal() {
const { t } = useTranslation();
- const { mutate, isPending } = useMutation({
- mutationFn: BillingService.createBillingSessionResponse,
- onSuccess: (data) => {
- window.location.href = data;
- },
- onError: () => {
- displayErrorToast(t(I18nKey.BILLING$ERROR_WHILE_CREATING_SESSION));
- },
- });
+ const { mutate, isPending } = useCreateBillingSession();
return (
diff --git a/frontend/src/components/features/settings/api-keys-manager.tsx b/frontend/src/components/features/settings/api-keys-manager.tsx
index 82d86fb4a9..20a8807aa0 100644
--- a/frontend/src/components/features/settings/api-keys-manager.tsx
+++ b/frontend/src/components/features/settings/api-keys-manager.tsx
@@ -13,10 +13,8 @@ import { CreateApiKeyModal } from "./create-api-key-modal";
import { DeleteApiKeyModal } from "./delete-api-key-modal";
import { NewApiKeyModal } from "./new-api-key-modal";
import { useApiKeys } from "#/hooks/query/use-api-keys";
-import {
- useLlmApiKey,
- useRefreshLlmApiKey,
-} from "#/hooks/query/use-llm-api-key";
+import { useLlmApiKey } from "#/hooks/query/use-llm-api-key";
+import { useRefreshLlmApiKey } from "#/hooks/mutation/use-refresh-llm-api-key";
interface LlmApiKeyManagerProps {
llmApiKey: { key: string | null } | undefined;
diff --git a/frontend/src/components/v1/chat/event-content-helpers/get-event-content.tsx b/frontend/src/components/v1/chat/event-content-helpers/get-event-content.tsx
index d9b9bf2d2d..dec57f385f 100644
--- a/frontend/src/components/v1/chat/event-content-helpers/get-event-content.tsx
+++ b/frontend/src/components/v1/chat/event-content-helpers/get-event-content.tsx
@@ -159,6 +159,9 @@ const getObservationEventTitle = (event: OpenHandsEvent): React.ReactNode => {
}
break;
}
+ case "ThinkObservation":
+ observationKey = "OBSERVATION_MESSAGE$THINK";
+ break;
default:
// For unknown observations, use the type name
return observationType.replace("Observation", "").toUpperCase();
diff --git a/frontend/src/components/v1/chat/event-content-helpers/get-observation-content.ts b/frontend/src/components/v1/chat/event-content-helpers/get-observation-content.ts
index bf443ea71c..7fb1c2ce1c 100644
--- a/frontend/src/components/v1/chat/event-content-helpers/get-observation-content.ts
+++ b/frontend/src/components/v1/chat/event-content-helpers/get-observation-content.ts
@@ -190,7 +190,13 @@ const getThinkObservationContent = (
event: ObservationEvent,
): string => {
const { observation } = event;
- return observation.content || "";
+
+ const textContent = observation.content
+ .filter((c) => c.type === "text")
+ .map((c) => c.text)
+ .join("\n");
+
+ return textContent || "";
};
const getFinishObservationContent = (
diff --git a/frontend/src/components/v1/chat/event-content-helpers/should-render-event.ts b/frontend/src/components/v1/chat/event-content-helpers/should-render-event.ts
index a5fdc62252..1171c21c92 100644
--- a/frontend/src/components/v1/chat/event-content-helpers/should-render-event.ts
+++ b/frontend/src/components/v1/chat/event-content-helpers/should-render-event.ts
@@ -18,6 +18,10 @@ export const shouldRenderEvent = (event: OpenHandsEvent) => {
// For V1, action is an object with kind property
const actionType = event.action.kind;
+ if (!actionType) {
+ return false;
+ }
+
// Hide user commands from the chat interface
if (actionType === "ExecuteBashAction" && event.source === "user") {
return false;
diff --git a/frontend/src/components/v1/chat/event-message-components/observation-pair-event-message.tsx b/frontend/src/components/v1/chat/event-message-components/observation-pair-event-message.tsx
index aa0bbc09b4..221d758dd6 100644
--- a/frontend/src/components/v1/chat/event-message-components/observation-pair-event-message.tsx
+++ b/frontend/src/components/v1/chat/event-message-components/observation-pair-event-message.tsx
@@ -34,7 +34,12 @@ export function ObservationPairEventMessage({
.map((t) => t.text)
.join("\n");
- if (thoughtContent && event.action.kind !== "ThinkAction") {
+ // Defensive check: ensure action exists and has kind property
+ if (
+ thoughtContent &&
+ event.action?.kind &&
+ event.action.kind !== "ThinkAction"
+ ) {
return (
diff --git a/frontend/src/hooks/mutation/use-accept-tos.ts b/frontend/src/hooks/mutation/use-accept-tos.ts
new file mode 100644
index 0000000000..a159b1458c
--- /dev/null
+++ b/frontend/src/hooks/mutation/use-accept-tos.ts
@@ -0,0 +1,54 @@
+import { useMutation } from "@tanstack/react-query";
+import { usePostHog } from "posthog-js/react";
+import { useNavigate } from "react-router";
+import { openHands } from "#/api/open-hands-axios";
+import { handleCaptureConsent } from "#/utils/handle-capture-consent";
+import { useTracking } from "#/hooks/use-tracking";
+
+interface AcceptTosVariables {
+ redirectUrl: string;
+}
+
+interface AcceptTosResponse {
+ redirect_url?: string;
+}
+
+export const useAcceptTos = () => {
+ const posthog = usePostHog();
+ const navigate = useNavigate();
+ const { trackUserSignupCompleted } = useTracking();
+
+ return useMutation({
+ mutationFn: async ({ redirectUrl }: AcceptTosVariables) => {
+ // Set consent for analytics
+ handleCaptureConsent(posthog, true);
+
+ // Call the API to record TOS acceptance in the database
+ return openHands.post
("/api/accept_tos", {
+ redirect_url: redirectUrl,
+ });
+ },
+ onSuccess: (response, { redirectUrl }) => {
+ // Track user signup completion
+ trackUserSignupCompleted();
+
+ // Get the redirect URL from the response
+ const finalRedirectUrl = response.data.redirect_url || redirectUrl;
+
+ // Check if the redirect URL is an external URL (starts with http or https)
+ if (
+ finalRedirectUrl.startsWith("http://") ||
+ finalRedirectUrl.startsWith("https://")
+ ) {
+ // For external URLs, redirect using window.location
+ window.location.href = finalRedirectUrl;
+ } else {
+ // For internal routes, use navigate
+ navigate(finalRedirectUrl);
+ }
+ },
+ onError: () => {
+ window.location.href = "/";
+ },
+ });
+};
diff --git a/frontend/src/hooks/mutation/use-create-billing-session.ts b/frontend/src/hooks/mutation/use-create-billing-session.ts
new file mode 100644
index 0000000000..f8f0716cb2
--- /dev/null
+++ b/frontend/src/hooks/mutation/use-create-billing-session.ts
@@ -0,0 +1,19 @@
+import { useMutation } from "@tanstack/react-query";
+import { useTranslation } from "react-i18next";
+import { I18nKey } from "#/i18n/declaration";
+import BillingService from "#/api/billing-service/billing-service.api";
+import { displayErrorToast } from "#/utils/custom-toast-handlers";
+
+export const useCreateBillingSession = () => {
+ const { t } = useTranslation();
+
+ return useMutation({
+ mutationFn: BillingService.createBillingSessionResponse,
+ onSuccess: (data) => {
+ window.location.href = data;
+ },
+ onError: () => {
+ displayErrorToast(t(I18nKey.BILLING$ERROR_WHILE_CREATING_SESSION));
+ },
+ });
+};
diff --git a/frontend/src/hooks/mutation/use-refresh-llm-api-key.ts b/frontend/src/hooks/mutation/use-refresh-llm-api-key.ts
new file mode 100644
index 0000000000..11a112e182
--- /dev/null
+++ b/frontend/src/hooks/mutation/use-refresh-llm-api-key.ts
@@ -0,0 +1,23 @@
+import { useMutation, useQueryClient } from "@tanstack/react-query";
+import { openHands } from "#/api/open-hands-axios";
+import {
+ LLM_API_KEY_QUERY_KEY,
+ LlmApiKeyResponse,
+} from "#/hooks/query/use-llm-api-key";
+
+export function useRefreshLlmApiKey() {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: async () => {
+ const { data } = await openHands.post(
+ "/api/keys/llm/byor/refresh",
+ );
+ return data;
+ },
+ onSuccess: () => {
+ // Invalidate the LLM API key query to trigger a refetch
+ queryClient.invalidateQueries({ queryKey: [LLM_API_KEY_QUERY_KEY] });
+ },
+ });
+}
diff --git a/frontend/src/hooks/query/use-conversation-microagents.ts b/frontend/src/hooks/query/use-conversation-skills.ts
similarity index 62%
rename from frontend/src/hooks/query/use-conversation-microagents.ts
rename to frontend/src/hooks/query/use-conversation-skills.ts
index d51b2b311d..43cf23bd37 100644
--- a/frontend/src/hooks/query/use-conversation-microagents.ts
+++ b/frontend/src/hooks/query/use-conversation-skills.ts
@@ -1,19 +1,29 @@
import { useQuery } from "@tanstack/react-query";
import ConversationService from "#/api/conversation-service/conversation-service.api";
+import V1ConversationService from "#/api/conversation-service/v1-conversation-service.api";
import { useConversationId } from "../use-conversation-id";
import { AgentState } from "#/types/agent-state";
import { useAgentState } from "#/hooks/use-agent-state";
+import { useSettings } from "./use-settings";
-export const useConversationMicroagents = () => {
+export const useConversationSkills = () => {
const { conversationId } = useConversationId();
const { curAgentState } = useAgentState();
+ const { data: settings } = useSettings();
return useQuery({
- queryKey: ["conversation", conversationId, "microagents"],
+ queryKey: ["conversation", conversationId, "skills", settings?.v1_enabled],
queryFn: async () => {
if (!conversationId) {
throw new Error("No conversation ID provided");
}
+
+ // Check if V1 is enabled and use the appropriate API
+ if (settings?.v1_enabled) {
+ const data = await V1ConversationService.getSkills(conversationId);
+ return data.skills;
+ }
+
const data = await ConversationService.getMicroagents(conversationId);
return data.microagents;
},
diff --git a/frontend/src/hooks/query/use-llm-api-key.ts b/frontend/src/hooks/query/use-llm-api-key.ts
index 5dcea9f714..58dee11411 100644
--- a/frontend/src/hooks/query/use-llm-api-key.ts
+++ b/frontend/src/hooks/query/use-llm-api-key.ts
@@ -1,4 +1,4 @@
-import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
+import { useQuery } from "@tanstack/react-query";
import { openHands } from "#/api/open-hands-axios";
import { useConfig } from "./use-config";
@@ -23,20 +23,3 @@ export function useLlmApiKey() {
gcTime: 1000 * 60 * 15, // 15 minutes
});
}
-
-export function useRefreshLlmApiKey() {
- const queryClient = useQueryClient();
-
- return useMutation({
- mutationFn: async () => {
- const { data } = await openHands.post(
- "/api/keys/llm/byor/refresh",
- );
- return data;
- },
- onSuccess: () => {
- // Invalidate the LLM API key query to trigger a refetch
- queryClient.invalidateQueries({ queryKey: [LLM_API_KEY_QUERY_KEY] });
- },
- });
-}
diff --git a/frontend/src/hooks/query/use-unified-get-git-changes.ts b/frontend/src/hooks/query/use-unified-get-git-changes.ts
index ae5600469a..6b0856031c 100644
--- a/frontend/src/hooks/query/use-unified-get-git-changes.ts
+++ b/frontend/src/hooks/query/use-unified-get-git-changes.ts
@@ -103,5 +103,6 @@ export const useUnifiedGetGitChanges = () => {
isSuccess: result.isSuccess,
isError: result.isError,
error: result.error,
+ refetch: result.refetch,
};
};
diff --git a/frontend/src/hooks/use-conversation-name-context-menu.ts b/frontend/src/hooks/use-conversation-name-context-menu.ts
index 0e2c3e837e..6072d5331e 100644
--- a/frontend/src/hooks/use-conversation-name-context-menu.ts
+++ b/frontend/src/hooks/use-conversation-name-context-menu.ts
@@ -41,8 +41,7 @@ export function useConversationNameContextMenu({
const [metricsModalVisible, setMetricsModalVisible] = React.useState(false);
const [systemModalVisible, setSystemModalVisible] = React.useState(false);
- const [microagentsModalVisible, setMicroagentsModalVisible] =
- React.useState(false);
+ const [skillsModalVisible, setSkillsModalVisible] = React.useState(false);
const [confirmDeleteModalVisible, setConfirmDeleteModalVisible] =
React.useState(false);
const [confirmStopModalVisible, setConfirmStopModalVisible] =
@@ -161,11 +160,9 @@ export function useConversationNameContextMenu({
onContextMenuToggle?.(false);
};
- const handleShowMicroagents = (
- event: React.MouseEvent,
- ) => {
+ const handleShowSkills = (event: React.MouseEvent) => {
event.stopPropagation();
- setMicroagentsModalVisible(true);
+ setSkillsModalVisible(true);
onContextMenuToggle?.(false);
};
@@ -178,7 +175,7 @@ export function useConversationNameContextMenu({
handleDownloadViaVSCode,
handleDisplayCost,
handleShowAgentTools,
- handleShowMicroagents,
+ handleShowSkills,
handleConfirmDelete,
handleConfirmStop,
@@ -187,8 +184,8 @@ export function useConversationNameContextMenu({
setMetricsModalVisible,
systemModalVisible,
setSystemModalVisible,
- microagentsModalVisible,
- setMicroagentsModalVisible,
+ skillsModalVisible,
+ setSkillsModalVisible,
confirmDeleteModalVisible,
setConfirmDeleteModalVisible,
confirmStopModalVisible,
@@ -204,6 +201,6 @@ export function useConversationNameContextMenu({
shouldShowExport: Boolean(conversationId && showOptions),
shouldShowDisplayCost: showOptions,
shouldShowAgentTools: Boolean(showOptions && systemMessage),
- shouldShowMicroagents: Boolean(showOptions && conversationId),
+ shouldShowSkills: Boolean(showOptions && conversationId),
};
}
diff --git a/frontend/src/i18n/declaration.ts b/frontend/src/i18n/declaration.ts
index f5a6cacfec..2f99b1aef6 100644
--- a/frontend/src/i18n/declaration.ts
+++ b/frontend/src/i18n/declaration.ts
@@ -640,17 +640,16 @@ export enum I18nKey {
TOS$CONTINUE = "TOS$CONTINUE",
TOS$ERROR_ACCEPTING = "TOS$ERROR_ACCEPTING",
TIPS$CUSTOMIZE_MICROAGENT = "TIPS$CUSTOMIZE_MICROAGENT",
- CONVERSATION$SHOW_MICROAGENTS = "CONVERSATION$SHOW_MICROAGENTS",
- CONVERSATION$NO_MICROAGENTS = "CONVERSATION$NO_MICROAGENTS",
+ CONVERSATION$NO_SKILLS = "CONVERSATION$NO_SKILLS",
CONVERSATION$FAILED_TO_FETCH_MICROAGENTS = "CONVERSATION$FAILED_TO_FETCH_MICROAGENTS",
MICROAGENTS_MODAL$TITLE = "MICROAGENTS_MODAL$TITLE",
- MICROAGENTS_MODAL$WARNING = "MICROAGENTS_MODAL$WARNING",
- MICROAGENTS_MODAL$TRIGGERS = "MICROAGENTS_MODAL$TRIGGERS",
+ SKILLS_MODAL$WARNING = "SKILLS_MODAL$WARNING",
+ COMMON$TRIGGERS = "COMMON$TRIGGERS",
MICROAGENTS_MODAL$INPUTS = "MICROAGENTS_MODAL$INPUTS",
MICROAGENTS_MODAL$TOOLS = "MICROAGENTS_MODAL$TOOLS",
- MICROAGENTS_MODAL$CONTENT = "MICROAGENTS_MODAL$CONTENT",
- MICROAGENTS_MODAL$NO_CONTENT = "MICROAGENTS_MODAL$NO_CONTENT",
- MICROAGENTS_MODAL$FETCH_ERROR = "MICROAGENTS_MODAL$FETCH_ERROR",
+ COMMON$CONTENT = "COMMON$CONTENT",
+ SKILLS_MODAL$NO_CONTENT = "SKILLS_MODAL$NO_CONTENT",
+ COMMON$FETCH_ERROR = "COMMON$FETCH_ERROR",
TIPS$SETUP_SCRIPT = "TIPS$SETUP_SCRIPT",
TIPS$VSCODE_INSTANCE = "TIPS$VSCODE_INSTANCE",
TIPS$SAVE_WORK = "TIPS$SAVE_WORK",
@@ -957,4 +956,6 @@ export enum I18nKey {
COMMON$PLAN_AGENT_DESCRIPTION = "COMMON$PLAN_AGENT_DESCRIPTION",
PLANNING_AGENTT$PLANNING_AGENT_INITIALIZED = "PLANNING_AGENTT$PLANNING_AGENT_INITIALIZED",
OBSERVATION_MESSAGE$SKILL_READY = "OBSERVATION_MESSAGE$SKILL_READY",
+ CONVERSATION$SHOW_SKILLS = "CONVERSATION$SHOW_SKILLS",
+ SKILLS_MODAL$TITLE = "SKILLS_MODAL$TITLE",
}
diff --git a/frontend/src/i18n/translation.json b/frontend/src/i18n/translation.json
index 2966c1aa5f..fc4ca89dbc 100644
--- a/frontend/src/i18n/translation.json
+++ b/frontend/src/i18n/translation.json
@@ -10239,37 +10239,21 @@
"tr": "Kullanılabilir bir mikro ajan kullanarak OpenHands'i deponuz için özelleştirebilirsiniz. OpenHands'ten deponun açıklamasını, kodun nasıl çalıştırılacağı dahil, .openhands/microagents/repo.md dosyasına koymasını isteyin.",
"uk": "Ви можете налаштувати OpenHands для свого репозиторію за допомогою доступного мікроагента. Попросіть OpenHands розмістити опис репозиторію, включаючи інформацію про те, як запустити код, у файлі .openhands/microagents/repo.md."
},
- "CONVERSATION$SHOW_MICROAGENTS": {
- "en": "Show Available Microagents",
- "ja": "利用可能なマイクロエージェントを表示",
- "zh-CN": "显示可用微代理",
- "zh-TW": "顯示可用微代理",
- "ko-KR": "사용 가능한 마이크로에이전트 표시",
- "no": "Vis tilgjengelige mikroagenter",
- "ar": "عرض الوكلاء المصغرين المتاحة",
- "de": "Verfügbare Mikroagenten anzeigen",
- "fr": "Afficher les micro-agents disponibles",
- "it": "Mostra microagenti disponibili",
- "pt": "Mostrar microagentes disponíveis",
- "es": "Mostrar microagentes disponibles",
- "tr": "Kullanılabilir mikro ajanları göster",
- "uk": "Показати доступних мікроагентів"
- },
- "CONVERSATION$NO_MICROAGENTS": {
- "en": "No available microagents found for this conversation.",
- "ja": "この会話用の利用可能なマイクロエージェントが見つかりませんでした。",
- "zh-CN": "未找到此对话的可用微代理。",
- "zh-TW": "未找到此對話的可用微代理。",
- "ko-KR": "이 대화에 대한 사용 가능한 마이크로에이전트를 찾을 수 없습니다.",
- "no": "Ingen tilgjengelige mikroagenter funnet for denne samtalen.",
- "ar": "لم يتم العثور على وكلاء مصغرين متاحة لهذه المحادثة.",
- "de": "Keine verfügbaren Mikroagenten für dieses Gespräch gefunden.",
- "fr": "Aucun micro-agent disponible trouvé pour cette conversation.",
- "it": "Nessun microagente disponibile trovato per questa conversazione.",
- "pt": "Nenhum microagente disponível encontrado para esta conversa.",
- "es": "No se encontraron microagentes disponibles para esta conversación.",
- "tr": "Bu konuşma için kullanılabilir mikro ajan bulunamadı.",
- "uk": "Для цієї розмови не знайдено доступних мікроагентів."
+ "CONVERSATION$NO_SKILLS": {
+ "en": "No available skills found for this conversation.",
+ "ja": "この会話には利用可能なスキルが見つかりません。",
+ "zh-CN": "本会话未找到可用技能。",
+ "zh-TW": "此對話中未找到可用技能。",
+ "ko-KR": "이 대화에서 사용 가능한 스킬을 찾을 수 없습니다.",
+ "no": "Ingen tilgjengelige ferdigheter ble funnet for denne samtalen.",
+ "ar": "لم يتم العثور على مهارات متاحة لهذه المحادثة.",
+ "de": "Für diese Unterhaltung wurden keine verfügbaren Skills gefunden.",
+ "fr": "Aucune compétence disponible trouvée pour cette conversation.",
+ "it": "Nessuna abilità disponibile trovata per questa conversazione.",
+ "pt": "Nenhuma habilidade disponível encontrada para esta conversa.",
+ "es": "No se encontraron habilidades disponibles para esta conversación.",
+ "tr": "Bu sohbet için kullanılabilir yetenek bulunamadı.",
+ "uk": "У цій розмові не знайдено доступних навичок."
},
"CONVERSATION$FAILED_TO_FETCH_MICROAGENTS": {
"en": "Failed to fetch available microagents",
@@ -10303,23 +10287,23 @@
"tr": "Kullanılabilir mikro ajanlar",
"uk": "Доступні мікроагенти"
},
- "MICROAGENTS_MODAL$WARNING": {
- "en": "If you update the microagents, you will need to stop the conversation and then click on the refresh button to see the changes.",
- "ja": "マイクロエージェントを更新する場合、会話を停止してから更新ボタンをクリックして変更を確認する必要があります。",
- "zh-CN": "如果您更新微代理,您需要停止对话,然后点击刷新按钮以查看更改。",
- "zh-TW": "如果您更新微代理,您需要停止對話,然後點擊重新整理按鈕以查看更改。",
- "ko-KR": "마이크로에이전트를 업데이트하는 경우 대화를 중지한 후 새로고침 버튼을 클릭하여 변경사항을 확인해야 합니다.",
- "no": "Hvis du oppdaterer mikroagentene, må du stoppe samtalen og deretter klikke på oppdater-knappen for å se endringene.",
- "ar": "إذا قمت بتحديث الوكلاء المصغرين، فستحتاج إلى إيقاف المحادثة ثم النقر على زر التحديث لرؤية التغييرات.",
- "de": "Wenn Sie die Mikroagenten aktualisieren, müssen Sie das Gespräch beenden und dann auf die Aktualisieren-Schaltfläche klicken, um die Änderungen zu sehen.",
- "fr": "Si vous mettez à jour les micro-agents, vous devrez arrêter la conversation puis cliquer sur le bouton actualiser pour voir les changements.",
- "it": "Se aggiorni i microagenti, dovrai fermare la conversazione e poi cliccare sul pulsante aggiorna per vedere le modifiche.",
- "pt": "Se você atualizar os microagentes, precisará parar a conversa e depois clicar no botão atualizar para ver as alterações.",
- "es": "Si actualiza los microagentes, necesitará detener la conversación y luego hacer clic en el botón actualizar para ver los cambios.",
- "tr": "Mikro ajanları güncellerseniz, konuşmayı durdurmanız ve ardından değişiklikleri görmek için yenile düğmesine tıklamanız gerekecektir.",
- "uk": "Якщо ви оновите мікроагенти, вам потрібно буде зупинити розмову, а потім натиснути кнопку оновлення, щоб побачити зміни."
+ "SKILLS_MODAL$WARNING": {
+ "en": "If you update the skills, you will need to stop the conversation and then click on the refresh button to see the changes.",
+ "ja": "スキルを更新する場合、会話を停止し、その後、更新ボタンをクリックして変更を反映させる必要があります。",
+ "zh-CN": "如果您更新技能,需要先停止对话,然后点击刷新按钮以查看更改。",
+ "zh-TW": "如果您更新技能,需要先停止對話,然後點擊刷新按鈕以查看更改。",
+ "ko-KR": "스킬을 업데이트하면 대화를 중단한 후 새로 고침 버튼을 클릭해야 변경 사항을 볼 수 있습니다.",
+ "no": "Hvis du oppdaterer ferdighetene, må du stoppe samtalen og deretter klikke på oppdateringsknappen for å se endringene.",
+ "ar": "إذا قمت بتحديث المهارات، ستحتاج إلى إيقاف المحادثة ثم النقر على زر التحديث لرؤية التغييرات.",
+ "de": "Wenn Sie die Fähigkeiten aktualisieren, müssen Sie das Gespräch beenden und dann auf die Schaltfläche 'Aktualisieren' klicken, um die Änderungen zu sehen.",
+ "fr": "Si vous mettez à jour les compétences, vous devrez arrêter la conversation, puis cliquer sur le bouton d’actualisation pour voir les modifications.",
+ "it": "Se aggiorni le competenze, dovrai interrompere la conversazione e poi cliccare sul pulsante di aggiornamento per vedere le modifiche.",
+ "pt": "Se você atualizar as habilidades, precisará interromper a conversa e clicar no botão de atualizar para ver as mudanças.",
+ "es": "Si actualizas las habilidades, deberás detener la conversación y luego hacer clic en el botón de actualizar para ver los cambios.",
+ "tr": "Yetenekleri güncellerseniz, değişiklikleri görmek için sohbeti durdurmalı ve ardından yenile düğmesine tıklamalısınız.",
+ "uk": "Якщо ви оновите навички, вам потрібно буде зупинити розмову, а потім натиснути кнопку оновлення, щоб побачити зміни."
},
- "MICROAGENTS_MODAL$TRIGGERS": {
+ "COMMON$TRIGGERS": {
"en": "Triggers",
"ja": "トリガー",
"zh-CN": "触发器",
@@ -10367,7 +10351,7 @@
"tr": "Araçlar",
"uk": "Інструменти"
},
- "MICROAGENTS_MODAL$CONTENT": {
+ "COMMON$CONTENT": {
"en": "Content",
"ja": "コンテンツ",
"zh-CN": "内容",
@@ -10383,37 +10367,37 @@
"tr": "İçerik",
"uk": "Вміст"
},
- "MICROAGENTS_MODAL$NO_CONTENT": {
- "en": "Microagent has no content",
- "ja": "マイクロエージェントにコンテンツがありません",
- "zh-CN": "微代理没有内容",
- "zh-TW": "微代理沒有內容",
- "ko-KR": "마이크로에이전트에 콘텐츠가 없습니다",
- "no": "Mikroagenten har ikke innhold",
- "ar": "الوكيل المصغر ليس لديه محتوى",
- "de": "Mikroagent hat keinen Inhalt",
- "fr": "Le micro-agent n'a pas de contenu",
- "it": "Il microagente non ha contenuto",
- "pt": "Microagente não tem conteúdo",
- "es": "El microagente no tiene contenido",
- "tr": "Mikroajanın içeriği yok",
- "uk": "Мікроагент не має вмісту"
+ "SKILLS_MODAL$NO_CONTENT": {
+ "en": "Skill has no content",
+ "ja": "スキルにはコンテンツがありません",
+ "zh-CN": "技能没有内容",
+ "zh-TW": "技能沒有內容",
+ "ko-KR": "스킬에 컨텐츠가 없습니다",
+ "no": "Ferdighet har ikke noe innhold",
+ "ar": "المهارة ليس لديها محتوى",
+ "de": "Die Fähigkeit hat keinen Inhalt",
+ "fr": "La compétence n'a pas de contenu",
+ "it": "La competenza non ha contenuti",
+ "pt": "A habilidade não possui conteúdo",
+ "es": "La habilidad no tiene contenido",
+ "tr": "Beceride içerik yok",
+ "uk": "У навички немає вмісту"
},
- "MICROAGENTS_MODAL$FETCH_ERROR": {
- "en": "Failed to fetch microagents. Please try again later.",
- "ja": "マイクロエージェントの取得に失敗しました。後でもう一度お試しください。",
- "zh-CN": "获取微代理失败。请稍后再试。",
- "zh-TW": "獲取微代理失敗。請稍後再試。",
- "ko-KR": "마이크로에이전트를 가져오지 못했습니다. 나중에 다시 시도해 주세요.",
- "no": "Kunne ikke hente mikroagenter. Prøv igjen senere.",
- "ar": "فشل في جلب الوكلاء المصغرين. يرجى المحاولة مرة أخرى لاحقًا.",
- "de": "Mikroagenten konnten nicht abgerufen werden. Bitte versuchen Sie es später erneut.",
- "fr": "Échec de la récupération des micro-agents. Veuillez réessayer plus tard.",
- "it": "Impossibile recuperare i microagenti. Riprova più tardi.",
- "pt": "Falha ao buscar microagentes. Por favor, tente novamente mais tarde.",
- "es": "Error al obtener microagentes. Por favor, inténtelo de nuevo más tarde.",
- "tr": "Mikroajanlar getirilemedi. Lütfen daha sonra tekrar deneyin.",
- "uk": "Не вдалося отримати мікроагентів. Будь ласка, спробуйте пізніше."
+ "COMMON$FETCH_ERROR": {
+ "en": "Failed to fetch skills. Please try again later.",
+ "ja": "スキルの取得に失敗しました。後でもう一度お試しください。",
+ "zh-CN": "获取技能失败。请稍后再试。",
+ "zh-TW": "取得技能失敗。請稍後再試。",
+ "ko-KR": "스킬을 가져오지 못했습니다. 나중에 다시 시도해주세요.",
+ "no": "Kunne ikke hente ferdigheter. Prøv igjen senere.",
+ "ar": "فشل في جلب المهارات. يرجى المحاولة لاحقًا.",
+ "de": "Die Fähigkeiten konnten nicht abgerufen werden. Bitte versuchen Sie es später erneut.",
+ "fr": "Échec de la récupération des compétences. Veuillez réessayer plus tard.",
+ "it": "Impossibile recuperare le competenze. Riprova più tardi.",
+ "pt": "Falha ao buscar as habilidades. Por favor, tente novamente mais tarde.",
+ "es": "No se pudieron obtener las habilidades. Por favor, inténtalo de nuevo más tarde.",
+ "tr": "Beceriler alınamadı. Lütfen daha sonra tekrar deneyin.",
+ "uk": "Не вдалося отримати навички. Будь ласка, спробуйте пізніше."
},
"TIPS$SETUP_SCRIPT": {
"en": "You can add .openhands/setup.sh to your repository to automatically run a setup script every time you start an OpenHands conversation.",
@@ -15310,5 +15294,37 @@
"tr": "Yetenek hazır",
"de": "Fähigkeit bereit",
"uk": "Навичка готова"
+ },
+ "CONVERSATION$SHOW_SKILLS": {
+ "en": "Show Available Skills",
+ "ja": "利用可能なスキルを表示",
+ "zh-CN": "显示可用技能",
+ "zh-TW": "顯示可用技能",
+ "ko-KR": "사용 가능한 스킬 표시",
+ "no": "Vis tilgjengelige ferdigheter",
+ "ar": "عرض المهارات المتاحة",
+ "de": "Verfügbare Fähigkeiten anzeigen",
+ "fr": "Afficher les compétences disponibles",
+ "it": "Mostra abilità disponibili",
+ "pt": "Mostrar habilidades disponíveis",
+ "es": "Mostrar habilidades disponibles",
+ "tr": "Kullanılabilir yetenekleri göster",
+ "uk": "Показати доступні навички"
+ },
+ "SKILLS_MODAL$TITLE": {
+ "en": "Available Skills",
+ "ja": "利用可能なスキル",
+ "zh-CN": "可用技能",
+ "zh-TW": "可用技能",
+ "ko-KR": "사용 가능한 스킬",
+ "no": "Tilgjengelige ferdigheter",
+ "ar": "المهارات المتاحة",
+ "de": "Verfügbare Fähigkeiten",
+ "fr": "Compétences disponibles",
+ "it": "Abilità disponibili",
+ "pt": "Habilidades disponíveis",
+ "es": "Habilidades disponibles",
+ "tr": "Kullanılabilir yetenekler",
+ "uk": "Доступні навички"
}
}
diff --git a/frontend/src/icons/u-refresh.svg b/frontend/src/icons/u-refresh.svg
new file mode 100644
index 0000000000..9e3a2051d2
--- /dev/null
+++ b/frontend/src/icons/u-refresh.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/frontend/src/routes.ts b/frontend/src/routes.ts
index 4c3c48adc5..ecee511688 100644
--- a/frontend/src/routes.ts
+++ b/frontend/src/routes.ts
@@ -21,5 +21,6 @@ export default [
]),
route("conversations/:conversationId", "routes/conversation.tsx"),
route("microagent-management", "routes/microagent-management.tsx"),
+ route("oauth/device/verify", "routes/device-verify.tsx"),
]),
] satisfies RouteConfig;
diff --git a/frontend/src/routes/accept-tos.tsx b/frontend/src/routes/accept-tos.tsx
index f723f2a5f6..a3732273e3 100644
--- a/frontend/src/routes/accept-tos.tsx
+++ b/frontend/src/routes/accept-tos.tsx
@@ -1,66 +1,27 @@
import React from "react";
import { useTranslation } from "react-i18next";
-import { useNavigate, useSearchParams } from "react-router";
-import { useMutation } from "@tanstack/react-query";
-import { usePostHog } from "posthog-js/react";
+import { useSearchParams } from "react-router";
import { I18nKey } from "#/i18n/declaration";
import OpenHandsLogo from "#/assets/branding/openhands-logo.svg?react";
import { TOSCheckbox } from "#/components/features/waitlist/tos-checkbox";
import { BrandButton } from "#/components/features/settings/brand-button";
-import { handleCaptureConsent } from "#/utils/handle-capture-consent";
-import { openHands } from "#/api/open-hands-axios";
import { ModalBackdrop } from "#/components/shared/modals/modal-backdrop";
-import { useTracking } from "#/hooks/use-tracking";
+import { useAcceptTos } from "#/hooks/mutation/use-accept-tos";
export default function AcceptTOS() {
- const posthog = usePostHog();
const { t } = useTranslation();
- const navigate = useNavigate();
const [searchParams] = useSearchParams();
const [isTosAccepted, setIsTosAccepted] = React.useState(false);
- const { trackUserSignupCompleted } = useTracking();
// Get the redirect URL from the query parameters
const redirectUrl = searchParams.get("redirect_url") || "/";
// Use mutation for accepting TOS
- const { mutate: acceptTOS, isPending: isSubmitting } = useMutation({
- mutationFn: async () => {
- // Set consent for analytics
- handleCaptureConsent(posthog, true);
-
- // Call the API to record TOS acceptance in the database
- return openHands.post("/api/accept_tos", {
- redirect_url: redirectUrl,
- });
- },
- onSuccess: (response) => {
- // Track user signup completion
- trackUserSignupCompleted();
-
- // Get the redirect URL from the response
- const finalRedirectUrl = response.data.redirect_url || redirectUrl;
-
- // Check if the redirect URL is an external URL (starts with http or https)
- if (
- finalRedirectUrl.startsWith("http://") ||
- finalRedirectUrl.startsWith("https://")
- ) {
- // For external URLs, redirect using window.location
- window.location.href = finalRedirectUrl;
- } else {
- // For internal routes, use navigate
- navigate(finalRedirectUrl);
- }
- },
- onError: () => {
- window.location.href = "/";
- },
- });
+ const { mutate: acceptTOS, isPending: isSubmitting } = useAcceptTos();
const handleAcceptTOS = () => {
if (isTosAccepted && !isSubmitting) {
- acceptTOS();
+ acceptTOS({ redirectUrl });
}
};
diff --git a/frontend/src/routes/device-verify.tsx b/frontend/src/routes/device-verify.tsx
new file mode 100644
index 0000000000..f306d660a5
--- /dev/null
+++ b/frontend/src/routes/device-verify.tsx
@@ -0,0 +1,274 @@
+/* eslint-disable i18next/no-literal-string */
+import React, { useState } from "react";
+import { useSearchParams } from "react-router";
+import { useIsAuthed } from "#/hooks/query/use-is-authed";
+
+export default function DeviceVerify() {
+ const [searchParams] = useSearchParams();
+ const { data: isAuthed, isLoading: isAuthLoading } = useIsAuthed();
+ const [verificationResult, setVerificationResult] = useState<{
+ success: boolean;
+ message: string;
+ } | null>(null);
+ const [isProcessing, setIsProcessing] = useState(false);
+
+ // Get user_code from URL parameters
+ const userCode = searchParams.get("user_code");
+
+ const processDeviceVerification = async (code: string) => {
+ try {
+ setIsProcessing(true);
+
+ // Call the backend API endpoint to process device verification
+ const response = await fetch("/oauth/device/verify-authenticated", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/x-www-form-urlencoded",
+ },
+ body: `user_code=${encodeURIComponent(code)}`,
+ credentials: "include", // Include cookies for authentication
+ });
+
+ if (response.ok) {
+ // Show success message
+ setVerificationResult({
+ success: true,
+ message:
+ "Device authorized successfully! You can now return to your CLI and close this window.",
+ });
+ } else {
+ const errorText = await response.text();
+ setVerificationResult({
+ success: false,
+ message: errorText || "Failed to authorize device. Please try again.",
+ });
+ }
+ } catch (error) {
+ setVerificationResult({
+ success: false,
+ message:
+ "An error occurred while authorizing the device. Please try again.",
+ });
+ } finally {
+ setIsProcessing(false);
+ }
+ };
+
+ // Remove automatic verification - require explicit user consent
+
+ const handleManualSubmit = (event: React.FormEvent) => {
+ event.preventDefault();
+ const formData = new FormData(event.currentTarget);
+ const code = formData.get("user_code") as string;
+ if (code && isAuthed) {
+ processDeviceVerification(code);
+ }
+ };
+
+ // Show verification result if we have one
+ if (verificationResult) {
+ return (
+
+
+
+
+ {verificationResult.success ? (
+
+
+
+ ) : (
+
+
+
+ )}
+
+
+ {verificationResult.success ? "Success!" : "Error"}
+
+
+ {verificationResult.message}
+
+ {!verificationResult.success && (
+
window.location.reload()}
+ className="px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90"
+ >
+ Try Again
+
+ )}
+
+
+
+ );
+ }
+
+ // Show processing state
+ if (isProcessing) {
+ return (
+
+
+
+
+
+ Processing device verification...
+
+
+
+
+ );
+ }
+
+ // Show device authorization confirmation if user is authenticated and code is provided
+ if (isAuthed && userCode) {
+ return (
+
+
+
+ Device Authorization Request
+
+
+
Device Code:
+
+ {userCode}
+
+
+
+
+
+
+
+
+
+ Security Notice
+
+
+ Only authorize this device if you initiated this request from
+ your CLI or application.
+
+
+
+
+
+ Do you want to authorize this device to access your OpenHands
+ account?
+
+
+ window.close()}
+ className="flex-1 px-4 py-2 border border-input rounded-md hover:bg-muted"
+ >
+ Cancel
+
+ processDeviceVerification(userCode)}
+ className="flex-1 px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90"
+ >
+ Authorize Device
+
+
+
+
+ );
+ }
+
+ // Show manual code entry form if no code in URL but user is authenticated
+ if (isAuthed && !userCode) {
+ return (
+
+
+
+ Device Authorization
+
+
+ Enter the code displayed on your device:
+
+
+
+
+ );
+ }
+
+ // Show loading state while checking authentication
+ if (isAuthLoading) {
+ return (
+
+
+
+
+ Processing device verification...
+
+
+
+ );
+ }
+
+ // Show authentication required message (this will trigger the auth modal via root layout)
+ return (
+
+
+
Authentication Required
+
+ Please sign in to authorize your device.
+
+
+
+ );
+}
diff --git a/frontend/src/types/v1/core/base/observation.ts b/frontend/src/types/v1/core/base/observation.ts
index 42726c2b32..a1c8a1a48d 100644
--- a/frontend/src/types/v1/core/base/observation.ts
+++ b/frontend/src/types/v1/core/base/observation.ts
@@ -36,7 +36,7 @@ export interface ThinkObservation extends ObservationBase<"ThinkObservation"> {
/**
* Confirmation message. DEFAULT: "Your thought has been logged."
*/
- content: string;
+ content: Array;
}
export interface BrowserObservation extends ObservationBase<"BrowserObservation"> {
diff --git a/frontend/src/types/v1/type-guards.ts b/frontend/src/types/v1/type-guards.ts
index ee831ea489..dec1816209 100644
--- a/frontend/src/types/v1/type-guards.ts
+++ b/frontend/src/types/v1/type-guards.ts
@@ -54,7 +54,10 @@ export const isObservationEvent = (
): event is ObservationEvent =>
event.source === "environment" &&
"action_id" in event &&
- "observation" in event;
+ "observation" in event &&
+ event.observation !== null &&
+ typeof event.observation === "object" &&
+ "kind" in event.observation;
/**
* Type guard function to check if an event is an agent error event
@@ -94,6 +97,9 @@ export const isUserMessageEvent = (
export const isActionEvent = (event: OpenHandsEvent): event is ActionEvent =>
event.source === "agent" &&
"action" in event &&
+ event.action !== null &&
+ typeof event.action === "object" &&
+ "kind" in event.action &&
"tool_name" in event &&
"tool_call_id" in event &&
typeof event.tool_name === "string" &&
diff --git a/frontend/src/utils/parse-terminal-output.ts b/frontend/src/utils/parse-terminal-output.ts
index a6ccc73cfc..1cd54eb858 100644
--- a/frontend/src/utils/parse-terminal-output.ts
+++ b/frontend/src/utils/parse-terminal-output.ts
@@ -1,3 +1,5 @@
+const START = "[Python Interpreter: ";
+
/**
* Parses the raw output from the terminal into the command and symbol
* @param raw The raw output to be displayed in the terminal
@@ -13,9 +15,14 @@
* console.log(parsed.symbol); // openhands@659478cb008c:/workspace $
*/
export const parseTerminalOutput = (raw: string) => {
- const envRegex = /(.*)\[Python Interpreter: (.*)\]/s;
- const match = raw.match(envRegex);
-
- if (!match) return raw;
- return match[1]?.trim() || "";
+ const start = raw.indexOf(START);
+ if (start < 0) {
+ return raw;
+ }
+ const offset = start + START.length;
+ const end = raw.indexOf("]", offset);
+ if (end <= offset) {
+ return raw;
+ }
+ return raw.substring(0, start).trim();
};
diff --git a/openhands/agenthub/codeact_agent/codeact_agent.py b/openhands/agenthub/codeact_agent/codeact_agent.py
index 85e5f88cbc..9dd814e9cf 100644
--- a/openhands/agenthub/codeact_agent/codeact_agent.py
+++ b/openhands/agenthub/codeact_agent/codeact_agent.py
@@ -194,9 +194,12 @@ class CodeActAgent(Agent):
# event we'll just return that instead of an action. The controller will
# immediately ask the agent to step again with the new view.
condensed_history: list[Event] = []
+ # Track which event IDs have been forgotten/condensed
+ forgotten_event_ids: set[int] = set()
match self.condenser.condensed_history(state):
- case View(events=events):
+ case View(events=events, forgotten_event_ids=forgotten_ids):
condensed_history = events
+ forgotten_event_ids = forgotten_ids
case Condensation(action=condensation_action):
return condensation_action
@@ -206,7 +209,9 @@ class CodeActAgent(Agent):
)
initial_user_message = self._get_initial_user_message(state.history)
- messages = self._get_messages(condensed_history, initial_user_message)
+ messages = self._get_messages(
+ condensed_history, initial_user_message, forgotten_event_ids
+ )
params: dict = {
'messages': messages,
}
@@ -245,7 +250,10 @@ class CodeActAgent(Agent):
return initial_user_message
def _get_messages(
- self, events: list[Event], initial_user_message: MessageAction
+ self,
+ events: list[Event],
+ initial_user_message: MessageAction,
+ forgotten_event_ids: set[int],
) -> list[Message]:
"""Constructs the message history for the LLM conversation.
@@ -284,6 +292,7 @@ class CodeActAgent(Agent):
messages = self.conversation_memory.process_events(
condensed_history=events,
initial_user_action=initial_user_message,
+ forgotten_event_ids=forgotten_event_ids,
max_message_chars=self.llm.config.max_message_chars,
vision_is_active=self.llm.vision_is_active(),
)
diff --git a/openhands/app_server/app_conversation/app_conversation_models.py b/openhands/app_server/app_conversation/app_conversation_models.py
index 96cc953806..58a63a95d6 100644
--- a/openhands/app_server/app_conversation/app_conversation_models.py
+++ b/openhands/app_server/app_conversation/app_conversation_models.py
@@ -1,5 +1,6 @@
from datetime import datetime
from enum import Enum
+from typing import Literal
from uuid import UUID, uuid4
from pydantic import BaseModel, Field
@@ -169,3 +170,12 @@ class AppConversationStartTask(BaseModel):
class AppConversationStartTaskPage(BaseModel):
items: list[AppConversationStartTask]
next_page_id: str | None = None
+
+
+class SkillResponse(BaseModel):
+ """Response model for skills endpoint."""
+
+ name: str
+ type: Literal['repo', 'knowledge']
+ content: str
+ triggers: list[str] = []
diff --git a/openhands/app_server/app_conversation/app_conversation_router.py b/openhands/app_server/app_conversation/app_conversation_router.py
index c8b96359e4..0f79be439a 100644
--- a/openhands/app_server/app_conversation/app_conversation_router.py
+++ b/openhands/app_server/app_conversation/app_conversation_router.py
@@ -1,11 +1,12 @@
"""Sandboxed Conversation router for OpenHands Server."""
import asyncio
+import logging
import os
import sys
import tempfile
from datetime import datetime
-from typing import Annotated, AsyncGenerator
+from typing import Annotated, AsyncGenerator, Literal
from uuid import UUID
import httpx
@@ -28,8 +29,8 @@ else:
return await async_iterator.__anext__()
-from fastapi import APIRouter, HTTPException, Query, Request
-from fastapi.responses import StreamingResponse
+from fastapi import APIRouter,HTTPException, Query, Request, status
+from fastapi.responses import JSONResponse, StreamingResponse
from sqlalchemy.ext.asyncio import AsyncSession
from openhands.app_server.app_conversation.app_conversation_models import (
@@ -40,10 +41,14 @@ from openhands.app_server.app_conversation.app_conversation_models import (
AppConversationStartTaskPage,
AppConversationStartTaskSortOrder,
AppConversationUpdateRequest,
+ SkillResponse,
)
from openhands.app_server.app_conversation.app_conversation_service import (
AppConversationService,
)
+from openhands.app_server.app_conversation.app_conversation_service_base import (
+ AppConversationServiceBase,
+)
from openhands.app_server.app_conversation.app_conversation_start_task_service import (
AppConversationStartTaskService,
)
@@ -66,9 +71,11 @@ from openhands.app_server.sandbox.sandbox_spec_service import SandboxSpecService
from openhands.app_server.utils.docker_utils import (
replace_localhost_hostname_for_docker,
)
+from openhands.sdk.context.skills import KeywordTrigger, TaskTrigger
from openhands.sdk.workspace.remote.async_remote_workspace import AsyncRemoteWorkspace
router = APIRouter(prefix='/app-conversations', tags=['Conversations'])
+logger = logging.getLogger(__name__)
app_conversation_service_dependency = depends_app_conversation_service()
app_conversation_start_task_service_dependency = (
depends_app_conversation_start_task_service()
@@ -417,6 +424,145 @@ async def read_conversation_file(
return ''
+@router.get('/{conversation_id}/skills')
+async def get_conversation_skills(
+ conversation_id: UUID,
+ app_conversation_service: AppConversationService = (
+ app_conversation_service_dependency
+ ),
+ sandbox_service: SandboxService = sandbox_service_dependency,
+ sandbox_spec_service: SandboxSpecService = sandbox_spec_service_dependency,
+) -> JSONResponse:
+ """Get all skills associated with the conversation.
+
+ This endpoint returns all skills that are loaded for the v1 conversation.
+ Skills are loaded from multiple sources:
+ - Sandbox skills (exposed URLs)
+ - Global skills (OpenHands/skills/)
+ - User skills (~/.openhands/skills/)
+ - Organization skills (org/.openhands repository)
+ - Repository skills (repo/.openhands/skills/ or .openhands/microagents/)
+
+ Returns:
+ JSONResponse: A JSON response containing the list of skills.
+ """
+ try:
+ # Get the conversation info
+ conversation = await app_conversation_service.get_app_conversation(
+ conversation_id
+ )
+ if not conversation:
+ return JSONResponse(
+ status_code=status.HTTP_404_NOT_FOUND,
+ content={'error': f'Conversation {conversation_id} not found'},
+ )
+
+ # Get the sandbox info
+ sandbox = await sandbox_service.get_sandbox(conversation.sandbox_id)
+ if not sandbox or sandbox.status != SandboxStatus.RUNNING:
+ return JSONResponse(
+ status_code=status.HTTP_404_NOT_FOUND,
+ content={
+ 'error': f'Sandbox not found or not running for conversation {conversation_id}'
+ },
+ )
+
+ # Get the sandbox spec to find the working directory
+ sandbox_spec = await sandbox_spec_service.get_sandbox_spec(
+ sandbox.sandbox_spec_id
+ )
+ if not sandbox_spec:
+ return JSONResponse(
+ status_code=status.HTTP_404_NOT_FOUND,
+ content={'error': 'Sandbox spec not found'},
+ )
+
+ # Get the agent server URL
+ if not sandbox.exposed_urls:
+ return JSONResponse(
+ status_code=status.HTTP_404_NOT_FOUND,
+ content={'error': 'No agent server URL found for sandbox'},
+ )
+
+ agent_server_url = None
+ for exposed_url in sandbox.exposed_urls:
+ if exposed_url.name == AGENT_SERVER:
+ agent_server_url = exposed_url.url
+ break
+
+ if not agent_server_url:
+ return JSONResponse(
+ status_code=status.HTTP_404_NOT_FOUND,
+ content={'error': 'Agent server URL not found in sandbox'},
+ )
+
+ agent_server_url = replace_localhost_hostname_for_docker(agent_server_url)
+
+ # Create remote workspace
+ remote_workspace = AsyncRemoteWorkspace(
+ host=agent_server_url,
+ api_key=sandbox.session_api_key,
+ working_dir=sandbox_spec.working_dir,
+ )
+
+ # Load skills from all sources
+ logger.info(f'Loading skills for conversation {conversation_id}')
+
+ # Prefer the shared loader to avoid duplication; otherwise return empty list.
+ all_skills: list = []
+ if isinstance(app_conversation_service, AppConversationServiceBase):
+ all_skills = await app_conversation_service.load_and_merge_all_skills(
+ sandbox,
+ remote_workspace,
+ conversation.selected_repository,
+ sandbox_spec.working_dir,
+ )
+
+ logger.info(
+ f'Loaded {len(all_skills)} skills for conversation {conversation_id}: '
+ f'{[s.name for s in all_skills]}'
+ )
+
+ # Transform skills to response format
+ skills_response = []
+ for skill in all_skills:
+ # Determine type based on trigger
+ skill_type: Literal['repo', 'knowledge']
+ if skill.trigger is None:
+ skill_type = 'repo'
+ else:
+ skill_type = 'knowledge'
+
+ # Extract triggers
+ triggers = []
+ if isinstance(skill.trigger, (KeywordTrigger, TaskTrigger)):
+ if hasattr(skill.trigger, 'keywords'):
+ triggers = skill.trigger.keywords
+ elif hasattr(skill.trigger, 'triggers'):
+ triggers = skill.trigger.triggers
+
+ skills_response.append(
+ SkillResponse(
+ name=skill.name,
+ type=skill_type,
+ content=skill.content,
+ triggers=triggers,
+ )
+ )
+
+ return JSONResponse(
+ status_code=status.HTTP_200_OK,
+ content={'skills': [s.model_dump() for s in skills_response]},
+ )
+
+ except Exception as e:
+ logger.error(f'Error getting skills for conversation {conversation_id}: {e}')
+ return JSONResponse(
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+ content={'error': f'Error getting skills: {str(e)}'},
+ )
+
+
async def _consume_remaining(
async_iter, db_session: AsyncSession, httpx_client: httpx.AsyncClient
):
diff --git a/openhands/app_server/app_conversation/app_conversation_service_base.py b/openhands/app_server/app_conversation/app_conversation_service_base.py
index fb1eb0001e..aa6add73fe 100644
--- a/openhands/app_server/app_conversation/app_conversation_service_base.py
+++ b/openhands/app_server/app_conversation/app_conversation_service_base.py
@@ -58,7 +58,7 @@ class AppConversationServiceBase(AppConversationService, ABC):
init_git_in_empty_workspace: bool
user_context: UserContext
- async def _load_and_merge_all_skills(
+ async def load_and_merge_all_skills(
self,
sandbox: SandboxInfo,
remote_workspace: AsyncRemoteWorkspace,
@@ -169,7 +169,7 @@ class AppConversationServiceBase(AppConversationService, ABC):
Updated agent with skills loaded into context
"""
# Load and merge all skills
- all_skills = await self._load_and_merge_all_skills(
+ all_skills = await self.load_and_merge_all_skills(
sandbox, remote_workspace, selected_repository, working_dir
)
@@ -198,7 +198,7 @@ class AppConversationServiceBase(AppConversationService, ABC):
task.status = AppConversationStartTaskStatus.SETTING_UP_SKILLS
yield task
- await self._load_and_merge_all_skills(
+ await self.load_and_merge_all_skills(
sandbox,
workspace,
task.request.selected_repository,
diff --git a/openhands/app_server/sandbox/sandbox_spec_service.py b/openhands/app_server/sandbox/sandbox_spec_service.py
index edaecc1b76..fe9d1653a9 100644
--- a/openhands/app_server/sandbox/sandbox_spec_service.py
+++ b/openhands/app_server/sandbox/sandbox_spec_service.py
@@ -12,7 +12,7 @@ from openhands.sdk.utils.models import DiscriminatedUnionMixin
# The version of the agent server to use for deployments.
# Typically this will be the same as the values from the pyproject.toml
-AGENT_SERVER_IMAGE = 'ghcr.io/openhands/agent-server:8f90b92-python'
+AGENT_SERVER_IMAGE = 'ghcr.io/openhands/agent-server:97652be-python'
class SandboxSpecService(ABC):
diff --git a/openhands/memory/conversation_memory.py b/openhands/memory/conversation_memory.py
index 5ff6ec7e58..5ae1a2cd71 100644
--- a/openhands/memory/conversation_memory.py
+++ b/openhands/memory/conversation_memory.py
@@ -76,6 +76,7 @@ class ConversationMemory:
self,
condensed_history: list[Event],
initial_user_action: MessageAction,
+ forgotten_event_ids: set[int] | None = None,
max_message_chars: int | None = None,
vision_is_active: bool = False,
) -> list[Message]:
@@ -85,16 +86,23 @@ class ConversationMemory:
Args:
condensed_history: The condensed history of events to convert
+ initial_user_action: The initial user message action, if available. Used to ensure the conversation starts correctly.
+ forgotten_event_ids: Set of event IDs that have been forgotten/condensed. If the initial user action's ID
+ is in this set, it will not be re-inserted to prevent re-execution of old instructions.
max_message_chars: The maximum number of characters in the content of an event included
in the prompt to the LLM. Larger observations are truncated.
vision_is_active: Whether vision is active in the LLM. If True, image URLs will be included.
- initial_user_action: The initial user message action, if available. Used to ensure the conversation starts correctly.
"""
events = condensed_history
+ # Default to empty set if not provided
+ if forgotten_event_ids is None:
+ forgotten_event_ids = set()
# Ensure the event list starts with SystemMessageAction, then MessageAction(source='user')
self._ensure_system_message(events)
- self._ensure_initial_user_message(events, initial_user_action)
+ self._ensure_initial_user_message(
+ events, initial_user_action, forgotten_event_ids
+ )
# log visual browsing status
logger.debug(f'Visual browsing: {self.agent_config.enable_som_visual_browsing}')
@@ -827,9 +835,23 @@ class ConversationMemory:
)
def _ensure_initial_user_message(
- self, events: list[Event], initial_user_action: MessageAction
+ self,
+ events: list[Event],
+ initial_user_action: MessageAction,
+ forgotten_event_ids: set[int],
) -> None:
- """Checks if the second event is a user MessageAction and inserts the provided one if needed."""
+ """Checks if the second event is a user MessageAction and inserts the provided one if needed.
+
+ IMPORTANT: If the initial user action has been condensed (its ID is in forgotten_event_ids),
+ we do NOT re-insert it. This prevents old instructions from being re-executed after
+ conversation condensation. The condensation summary already contains the context of
+ what was requested and completed.
+
+ Args:
+ events: The list of events to modify in-place
+ initial_user_action: The initial user message action from the full history
+ forgotten_event_ids: Set of event IDs that have been forgotten/condensed
+ """
if (
not events
): # Should have system message from previous step, but safety check
@@ -837,6 +859,17 @@ class ConversationMemory:
# Or raise? Let's log for now, _ensure_system_message should handle this.
return
+ # Check if the initial user action has been condensed/forgotten.
+ # If so, we should NOT re-insert it to prevent re-execution of old instructions.
+ # The condensation summary already contains the context of what was requested.
+ initial_user_action_id = initial_user_action.id
+ if initial_user_action_id in forgotten_event_ids:
+ logger.info(
+ f'Initial user action (id={initial_user_action_id}) has been condensed. '
+ 'Not re-inserting to prevent re-execution of old instructions.'
+ )
+ return
+
# We expect events[0] to be SystemMessageAction after _ensure_system_message
if len(events) == 1:
# Only system message exists
diff --git a/openhands/memory/view.py b/openhands/memory/view.py
index 87a20b6340..81dd8bab5d 100644
--- a/openhands/memory/view.py
+++ b/openhands/memory/view.py
@@ -18,6 +18,8 @@ class View(BaseModel):
events: list[Event]
unhandled_condensation_request: bool = False
+ # Set of event IDs that have been forgotten/condensed
+ forgotten_event_ids: set[int] = set()
def __len__(self) -> int:
return len(self.events)
@@ -90,4 +92,5 @@ class View(BaseModel):
return View(
events=kept_events,
unhandled_condensation_request=unhandled_condensation_request,
+ forgotten_event_ids=forgotten_event_ids,
)
diff --git a/openhands/runtime/base.py b/openhands/runtime/base.py
index 5eb5429f71..c7a332166b 100644
--- a/openhands/runtime/base.py
+++ b/openhands/runtime/base.py
@@ -76,6 +76,8 @@ from openhands.utils.async_utils import (
call_sync_from_async,
)
+DISABLE_VSCODE_PLUGIN = os.getenv('DISABLE_VSCODE_PLUGIN', 'false').lower() == 'true'
+
def _default_env_vars(sandbox_config: SandboxConfig) -> dict[str, str]:
ret = {}
@@ -153,9 +155,11 @@ class Runtime(FileEditRuntimeMixin):
self.plugins = (
copy.deepcopy(plugins) if plugins is not None and len(plugins) > 0 else []
)
+
# add VSCode plugin if not in headless mode
- if not headless_mode:
+ if not headless_mode and not DISABLE_VSCODE_PLUGIN:
self.plugins.append(VSCodeRequirement())
+ logger.info(f'Loaded plugins for runtime {self.sid}: {self.plugins}')
self.status_callback = status_callback
self.attach_to_existing = attach_to_existing
diff --git a/openhands/runtime/impl/kubernetes/README.md b/openhands/runtime/impl/kubernetes/README.md
index d16247389d..36b7452d1e 100644
--- a/openhands/runtime/impl/kubernetes/README.md
+++ b/openhands/runtime/impl/kubernetes/README.md
@@ -40,7 +40,7 @@ Two configuration options are required to use the Kubernetes runtime:
2. **Runtime Container Image**: Specify the container image to use for the runtime environment
```toml
[sandbox]
- runtime_container_image = "docker.openhands.dev/openhands/runtime:0.62-nikolaik"
+ runtime_container_image = "docker.openhands.dev/openhands/runtime:1.0-nikolaik"
```
#### Additional Kubernetes Options
diff --git a/openhands/runtime/impl/local/local_runtime.py b/openhands/runtime/impl/local/local_runtime.py
index ed8d26996a..f6ef26a1cf 100644
--- a/openhands/runtime/impl/local/local_runtime.py
+++ b/openhands/runtime/impl/local/local_runtime.py
@@ -45,6 +45,8 @@ from openhands.utils.async_utils import call_sync_from_async
from openhands.utils.http_session import httpx_verify_option
from openhands.utils.tenacity_stop import stop_if_should_exit
+DISABLE_VSCODE_PLUGIN = os.getenv('DISABLE_VSCODE_PLUGIN', 'false').lower() == 'true'
+
@dataclass
class ActionExecutionServerInfo:
@@ -406,7 +408,7 @@ class LocalRuntime(ActionExecutionClient):
plugins = _get_plugins(config)
# Copy the logic from Runtime where we add a VSCodePlugin on init if missing
- if not headless_mode:
+ if not headless_mode and not DISABLE_VSCODE_PLUGIN:
plugins.append(VSCodeRequirement())
for _ in range(initial_num_warm_servers):
diff --git a/openhands/storage/settings/file_settings_store.py b/openhands/storage/settings/file_settings_store.py
index 3acedeb16f..5b43bf6b80 100644
--- a/openhands/storage/settings/file_settings_store.py
+++ b/openhands/storage/settings/file_settings_store.py
@@ -21,6 +21,11 @@ class FileSettingsStore(SettingsStore):
json_str = await call_sync_from_async(self.file_store.read, self.path)
kwargs = json.loads(json_str)
settings = Settings(**kwargs)
+
+ # Turn on V1 in OpenHands
+ # We can simplify / remove this as part of V0 removal
+ settings.v1_enabled = True
+
return settings
except FileNotFoundError:
return None
diff --git a/poetry.lock b/poetry.lock
index 5d87e75d42..23789d3285 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,4 +1,4 @@
-# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand.
+# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand.
[[package]]
name = "aiofiles"
@@ -5675,14 +5675,14 @@ utils = ["numpydoc"]
[[package]]
name = "lmnr"
-version = "0.7.20"
+version = "0.7.24"
description = "Python SDK for Laminar"
optional = false
python-versions = "<4,>=3.10"
groups = ["main"]
files = [
- {file = "lmnr-0.7.20-py3-none-any.whl", hash = "sha256:5f9fa7444e6f96c25e097f66484ff29e632bdd1de0e9346948bf5595f4a8af38"},
- {file = "lmnr-0.7.20.tar.gz", hash = "sha256:1f484cd618db2d71af65f90a0b8b36d20d80dc91a5138b811575c8677bf7c4fd"},
+ {file = "lmnr-0.7.24-py3-none-any.whl", hash = "sha256:ad780d4a62ece897048811f3368639c240a9329ab31027da8c96545137a3a08a"},
+ {file = "lmnr-0.7.24.tar.gz", hash = "sha256:aa6973f46fc4ba95c9061c1feceb58afc02eb43c9376c21e32545371ff6123d7"},
]
[package.dependencies]
@@ -5705,14 +5705,15 @@ tqdm = ">=4.0"
[package.extras]
alephalpha = ["opentelemetry-instrumentation-alephalpha (>=0.47.1)"]
-all = ["opentelemetry-instrumentation-alephalpha (>=0.47.1)", "opentelemetry-instrumentation-bedrock (>=0.47.1)", "opentelemetry-instrumentation-chromadb (>=0.47.1)", "opentelemetry-instrumentation-cohere (>=0.47.1)", "opentelemetry-instrumentation-crewai (>=0.47.1)", "opentelemetry-instrumentation-haystack (>=0.47.1)", "opentelemetry-instrumentation-lancedb (>=0.47.1)", "opentelemetry-instrumentation-langchain (>=0.47.1)", "opentelemetry-instrumentation-llamaindex (>=0.47.1)", "opentelemetry-instrumentation-marqo (>=0.47.1)", "opentelemetry-instrumentation-mcp (>=0.47.1)", "opentelemetry-instrumentation-milvus (>=0.47.1)", "opentelemetry-instrumentation-mistralai (>=0.47.1)", "opentelemetry-instrumentation-ollama (>=0.47.1)", "opentelemetry-instrumentation-pinecone (>=0.47.1)", "opentelemetry-instrumentation-qdrant (>=0.47.1)", "opentelemetry-instrumentation-replicate (>=0.47.1)", "opentelemetry-instrumentation-sagemaker (>=0.47.1)", "opentelemetry-instrumentation-together (>=0.47.1)", "opentelemetry-instrumentation-transformers (>=0.47.1)", "opentelemetry-instrumentation-vertexai (>=0.47.1)", "opentelemetry-instrumentation-watsonx (>=0.47.1)", "opentelemetry-instrumentation-weaviate (>=0.47.1)"]
+all = ["opentelemetry-instrumentation-alephalpha (>=0.47.1)", "opentelemetry-instrumentation-bedrock (>=0.47.1)", "opentelemetry-instrumentation-chromadb (>=0.47.1)", "opentelemetry-instrumentation-cohere (>=0.47.1)", "opentelemetry-instrumentation-crewai (>=0.47.1)", "opentelemetry-instrumentation-haystack (>=0.47.1)", "opentelemetry-instrumentation-lancedb (>=0.47.1)", "opentelemetry-instrumentation-langchain (>=0.47.1,<0.48.0)", "opentelemetry-instrumentation-llamaindex (>=0.47.1)", "opentelemetry-instrumentation-marqo (>=0.47.1)", "opentelemetry-instrumentation-mcp (>=0.47.1)", "opentelemetry-instrumentation-milvus (>=0.47.1)", "opentelemetry-instrumentation-mistralai (>=0.47.1)", "opentelemetry-instrumentation-ollama (>=0.47.1)", "opentelemetry-instrumentation-pinecone (>=0.47.1)", "opentelemetry-instrumentation-qdrant (>=0.47.1)", "opentelemetry-instrumentation-replicate (>=0.47.1)", "opentelemetry-instrumentation-sagemaker (>=0.47.1)", "opentelemetry-instrumentation-together (>=0.47.1)", "opentelemetry-instrumentation-transformers (>=0.47.1)", "opentelemetry-instrumentation-vertexai (>=0.47.1)", "opentelemetry-instrumentation-watsonx (>=0.47.1)", "opentelemetry-instrumentation-weaviate (>=0.47.1)"]
bedrock = ["opentelemetry-instrumentation-bedrock (>=0.47.1)"]
chromadb = ["opentelemetry-instrumentation-chromadb (>=0.47.1)"]
+claude-agent-sdk = ["lmnr-claude-code-proxy (>=0.1.0a5)"]
cohere = ["opentelemetry-instrumentation-cohere (>=0.47.1)"]
crewai = ["opentelemetry-instrumentation-crewai (>=0.47.1)"]
haystack = ["opentelemetry-instrumentation-haystack (>=0.47.1)"]
lancedb = ["opentelemetry-instrumentation-lancedb (>=0.47.1)"]
-langchain = ["opentelemetry-instrumentation-langchain (>=0.47.1)"]
+langchain = ["opentelemetry-instrumentation-langchain (>=0.47.1,<0.48.0)"]
llamaindex = ["opentelemetry-instrumentation-llamaindex (>=0.47.1)"]
marqo = ["opentelemetry-instrumentation-marqo (>=0.47.1)"]
mcp = ["opentelemetry-instrumentation-mcp (>=0.47.1)"]
@@ -7379,14 +7380,14 @@ llama = ["llama-index (>=0.12.29,<0.13.0)", "llama-index-core (>=0.12.29,<0.13.0
[[package]]
name = "openhands-agent-server"
-version = "1.5.2"
+version = "1.6.0"
description = "OpenHands Agent Server - REST/WebSocket interface for OpenHands AI Agent"
optional = false
python-versions = ">=3.12"
groups = ["main"]
files = [
- {file = "openhands_agent_server-1.5.2-py3-none-any.whl", hash = "sha256:7a368f61036f85446f566b9f6f9d6c7318684776cf2293daa5bce3ee19ac077d"},
- {file = "openhands_agent_server-1.5.2.tar.gz", hash = "sha256:dfaf5583dd71dae933643a8f8160156ce6fa7ed20db5cc3c45465b079bc576cd"},
+ {file = "openhands_agent_server-1.6.0-py3-none-any.whl", hash = "sha256:e6ae865ac3e7a96b234e10a0faad23f6210e025bbf7721cb66bc7a71d160848c"},
+ {file = "openhands_agent_server-1.6.0.tar.gz", hash = "sha256:44ce7694ae2d4bb0666d318ef13e6618bd4dc73022c60354839fe6130e67d02a"},
]
[package.dependencies]
@@ -7403,14 +7404,14 @@ wsproto = ">=1.2.0"
[[package]]
name = "openhands-sdk"
-version = "1.5.2"
+version = "1.6.0"
description = "OpenHands SDK - Core functionality for building AI agents"
optional = false
python-versions = ">=3.12"
groups = ["main"]
files = [
- {file = "openhands_sdk-1.5.2-py3-none-any.whl", hash = "sha256:593430e9c8729e345fce3fca7e9a9a7ef084a08222d6ba42113e6ba5f6e9f15d"},
- {file = "openhands_sdk-1.5.2.tar.gz", hash = "sha256:798aa8f8ccd84b15deb418c4301d00f33da288bc1a8d41efa5cc47c10aaf3fd6"},
+ {file = "openhands_sdk-1.6.0-py3-none-any.whl", hash = "sha256:94d2f87fb35406373da6728ae2d88584137f9e9b67fa0e940444c72f2e44e7d3"},
+ {file = "openhands_sdk-1.6.0.tar.gz", hash = "sha256:f45742350e3874a7f5b08befc4a9d5adc7e4454f7ab5f8391c519eee3116090f"},
]
[package.dependencies]
@@ -7418,7 +7419,7 @@ deprecation = ">=2.1.0"
fastmcp = ">=2.11.3"
httpx = ">=0.27.0"
litellm = ">=1.80.7"
-lmnr = ">=0.7.20"
+lmnr = ">=0.7.24"
pydantic = ">=2.11.7"
python-frontmatter = ">=1.1.0"
python-json-logger = ">=3.3.0"
@@ -7430,14 +7431,14 @@ boto3 = ["boto3 (>=1.35.0)"]
[[package]]
name = "openhands-tools"
-version = "1.5.2"
+version = "1.6.0"
description = "OpenHands Tools - Runtime tools for AI agents"
optional = false
python-versions = ">=3.12"
groups = ["main"]
files = [
- {file = "openhands_tools-1.5.2-py3-none-any.whl", hash = "sha256:33e9c2af65aaa7b6b9a10b42d2fb11137e6b35e7ac02a4b9269ef37b5c79cc01"},
- {file = "openhands_tools-1.5.2.tar.gz", hash = "sha256:4644a24144fbdf630fb0edc303526b4add61b3fbe7a7434da73f231312c34846"},
+ {file = "openhands_tools-1.6.0-py3-none-any.whl", hash = "sha256:176556d44186536751b23fe052d3505492cc2afb8d52db20fb7a2cc0169cd57a"},
+ {file = "openhands_tools-1.6.0.tar.gz", hash = "sha256:d07ba31050fd4a7891a4c48388aa53ce9f703e17064ddbd59146d6c77e5980b3"},
]
[package.dependencies]
@@ -12706,19 +12707,18 @@ dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests
[[package]]
name = "pytest-asyncio"
-version = "1.3.0"
+version = "1.1.0"
description = "Pytest support for asyncio"
optional = false
-python-versions = ">=3.10"
-groups = ["dev", "test"]
+python-versions = ">=3.9"
+groups = ["test"]
files = [
- {file = "pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5"},
- {file = "pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5"},
+ {file = "pytest_asyncio-1.1.0-py3-none-any.whl", hash = "sha256:5fe2d69607b0bd75c656d1211f969cadba035030156745ee09e7d71740e58ecf"},
+ {file = "pytest_asyncio-1.1.0.tar.gz", hash = "sha256:796aa822981e01b68c12e4827b8697108f7205020f24b5793b3c41555dab68ea"},
]
[package.dependencies]
-pytest = ">=8.2,<10"
-typing-extensions = {version = ">=4.12", markers = "python_version < \"3.13\""}
+pytest = ">=8.2,<9"
[package.extras]
docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1)"]
@@ -16823,4 +16823,4 @@ third-party-runtimes = ["daytona", "e2b-code-interpreter", "modal", "runloop-api
[metadata]
lock-version = "2.1"
python-versions = "^3.12,<3.14"
-content-hash = "b1324a91228de944961a2fd42f47cdd7823758eb51c32b5e99f1f63615330d70"
+content-hash = "9764f3b69ec8ed35feebd78a826bbc6bfa4ac6d5b56bc999be8bc738b644e538"
diff --git a/pyproject.toml b/pyproject.toml
index ed7545fca8..fda3cc9b96 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -6,7 +6,7 @@ requires = [
[tool.poetry]
name = "openhands-ai"
-version = "0.62.0"
+version = "1.0.0"
description = "OpenHands: Code Less, Make More"
authors = [ "OpenHands" ]
license = "MIT"
@@ -116,9 +116,9 @@ pybase62 = "^1.0.0"
#openhands-agent-server = { git = "https://github.com/OpenHands/agent-sdk.git", subdirectory = "openhands-agent-server", rev = "15f565b8ac38876e40dc05c08e2b04ccaae4a66d" }
#openhands-sdk = { git = "https://github.com/OpenHands/agent-sdk.git", subdirectory = "openhands-sdk", rev = "15f565b8ac38876e40dc05c08e2b04ccaae4a66d" }
#openhands-tools = { git = "https://github.com/OpenHands/agent-sdk.git", subdirectory = "openhands-tools", rev = "15f565b8ac38876e40dc05c08e2b04ccaae4a66d" }
-openhands-sdk = "1.5.2"
-openhands-agent-server = "1.5.2"
-openhands-tools = "1.5.2"
+openhands-sdk = "1.6.0"
+openhands-agent-server = "1.6.0"
+openhands-tools = "1.6.0"
python-jose = { version = ">=3.3", extras = [ "cryptography" ] }
sqlalchemy = { extras = [ "asyncio" ], version = "^2.0.40" }
pg8000 = "^1.31.5"
diff --git a/tests/unit/agenthub/test_agents.py b/tests/unit/agenthub/test_agents.py
index 2a90dcb668..09f28e991c 100644
--- a/tests/unit/agenthub/test_agents.py
+++ b/tests/unit/agenthub/test_agents.py
@@ -393,7 +393,7 @@ def test_mismatched_tool_call_events_and_auto_add_system_message(
# 2. The action message
# 3. The observation message
mock_state.history = [initial_user_message, action, observation]
- messages = agent._get_messages(mock_state.history, initial_user_message)
+ messages = agent._get_messages(mock_state.history, initial_user_message, set())
assert len(messages) == 4 # System + initial user + action + observation
assert messages[0].role == 'system' # First message should be the system message
assert (
@@ -404,7 +404,7 @@ def test_mismatched_tool_call_events_and_auto_add_system_message(
# The same should hold if the events are presented out-of-order
mock_state.history = [initial_user_message, observation, action]
- messages = agent._get_messages(mock_state.history, initial_user_message)
+ messages = agent._get_messages(mock_state.history, initial_user_message, set())
assert len(messages) == 4
assert messages[0].role == 'system' # First message should be the system message
assert (
@@ -414,7 +414,7 @@ def test_mismatched_tool_call_events_and_auto_add_system_message(
# If only one of the two events is present, then we should just get the system message
# plus any valid message from the event
mock_state.history = [initial_user_message, action]
- messages = agent._get_messages(mock_state.history, initial_user_message)
+ messages = agent._get_messages(mock_state.history, initial_user_message, set())
assert (
len(messages) == 2
) # System + initial user message, action is waiting for its observation
@@ -422,7 +422,7 @@ def test_mismatched_tool_call_events_and_auto_add_system_message(
assert messages[1].role == 'user'
mock_state.history = [initial_user_message, observation]
- messages = agent._get_messages(mock_state.history, initial_user_message)
+ messages = agent._get_messages(mock_state.history, initial_user_message, set())
assert (
len(messages) == 2
) # System + initial user message, observation has no matching action
diff --git a/tests/unit/agenthub/test_prompt_caching.py b/tests/unit/agenthub/test_prompt_caching.py
index 60cc0bb16f..2435b1320a 100644
--- a/tests/unit/agenthub/test_prompt_caching.py
+++ b/tests/unit/agenthub/test_prompt_caching.py
@@ -80,7 +80,7 @@ def test_get_messages(codeact_agent: CodeActAgent):
history.append(message_action_5)
codeact_agent.reset()
- messages = codeact_agent._get_messages(history, message_action_1)
+ messages = codeact_agent._get_messages(history, message_action_1, set())
assert (
len(messages) == 6
@@ -122,7 +122,7 @@ def test_get_messages_prompt_caching(codeact_agent: CodeActAgent):
history.append(message_action_agent)
codeact_agent.reset()
- messages = codeact_agent._get_messages(history, initial_user_message)
+ messages = codeact_agent._get_messages(history, initial_user_message, set())
# Check that only the last two user messages have cache_prompt=True
cached_user_messages = [
diff --git a/tests/unit/app_server/test_app_conversation_service_base.py b/tests/unit/app_server/test_app_conversation_service_base.py
index 01fc63bc5d..db31d8d3d2 100644
--- a/tests/unit/app_server/test_app_conversation_service_base.py
+++ b/tests/unit/app_server/test_app_conversation_service_base.py
@@ -920,12 +920,12 @@ async def test_configure_git_user_settings_special_characters_in_name(mock_works
# =============================================================================
-# Tests for _load_and_merge_all_skills with org skills
+# Tests for load_and_merge_all_skills with org skills
# =============================================================================
class TestLoadAndMergeAllSkillsWithOrgSkills:
- """Test _load_and_merge_all_skills includes organization skills."""
+ """Test load_and_merge_all_skills includes organization skills."""
@pytest.mark.asyncio
@patch(
@@ -951,7 +951,7 @@ class TestLoadAndMergeAllSkillsWithOrgSkills:
mock_load_global,
mock_load_sandbox,
):
- """Test that _load_and_merge_all_skills loads and merges org skills."""
+ """Test that load_and_merge_all_skills loads and merges org skills."""
# Arrange
mock_user_context = Mock(spec=UserContext)
with patch.object(
@@ -987,7 +987,7 @@ class TestLoadAndMergeAllSkillsWithOrgSkills:
mock_load_repo.return_value = [repo_skill]
# Act
- result = await service._load_and_merge_all_skills(
+ result = await service.load_and_merge_all_skills(
sandbox, remote_workspace, 'owner/repo', '/workspace'
)
@@ -1066,7 +1066,7 @@ class TestLoadAndMergeAllSkillsWithOrgSkills:
mock_load_repo.return_value = [repo_skill]
# Act
- result = await service._load_and_merge_all_skills(
+ result = await service.load_and_merge_all_skills(
sandbox, remote_workspace, 'owner/repo', '/workspace'
)
@@ -1132,7 +1132,7 @@ class TestLoadAndMergeAllSkillsWithOrgSkills:
mock_load_repo.return_value = []
# Act
- result = await service._load_and_merge_all_skills(
+ result = await service.load_and_merge_all_skills(
sandbox, remote_workspace, 'owner/repo', '/workspace'
)
@@ -1193,7 +1193,7 @@ class TestLoadAndMergeAllSkillsWithOrgSkills:
mock_load_repo.return_value = [repo_skill]
# Act
- result = await service._load_and_merge_all_skills(
+ result = await service.load_and_merge_all_skills(
sandbox, remote_workspace, 'owner/repo', '/workspace'
)
@@ -1254,7 +1254,7 @@ class TestLoadAndMergeAllSkillsWithOrgSkills:
mock_load_repo.return_value = []
# Act
- result = await service._load_and_merge_all_skills(
+ result = await service.load_and_merge_all_skills(
sandbox, remote_workspace, None, '/workspace'
)
diff --git a/tests/unit/app_server/test_app_conversation_skills_endpoint.py b/tests/unit/app_server/test_app_conversation_skills_endpoint.py
new file mode 100644
index 0000000000..e84412bcd0
--- /dev/null
+++ b/tests/unit/app_server/test_app_conversation_skills_endpoint.py
@@ -0,0 +1,503 @@
+"""Unit tests for the V1 skills endpoint in app_conversation_router.
+
+This module tests the GET /{conversation_id}/skills endpoint functionality,
+following TDD best practices with AAA structure.
+"""
+
+from unittest.mock import AsyncMock, MagicMock
+from uuid import uuid4
+
+import pytest
+from fastapi import status
+
+from openhands.app_server.app_conversation.app_conversation_models import (
+ AppConversation,
+)
+from openhands.app_server.app_conversation.app_conversation_router import (
+ get_conversation_skills,
+)
+from openhands.app_server.app_conversation.app_conversation_service_base import (
+ AppConversationServiceBase,
+)
+from openhands.app_server.sandbox.sandbox_models import (
+ AGENT_SERVER,
+ ExposedUrl,
+ SandboxInfo,
+ SandboxStatus,
+)
+from openhands.app_server.sandbox.sandbox_spec_models import SandboxSpecInfo
+from openhands.app_server.user.user_context import UserContext
+from openhands.sdk.context.skills import KeywordTrigger, Skill, TaskTrigger
+
+
+def _make_service_mock(
+ *,
+ user_context: UserContext,
+ conversation_return: AppConversation | None = None,
+ skills_return: list[Skill] | None = None,
+ raise_on_load: bool = False,
+):
+ """Create a mock service that passes the isinstance check and returns the desired values."""
+
+ mock_cls = type('AppConversationServiceMock', (MagicMock,), {})
+ AppConversationServiceBase.register(mock_cls)
+
+ service = mock_cls()
+ service.user_context = user_context
+ service.get_app_conversation = AsyncMock(return_value=conversation_return)
+
+ async def _load_skills(*_args, **_kwargs):
+ if raise_on_load:
+ raise Exception('Skill loading failed')
+ return skills_return or []
+
+ service.load_and_merge_all_skills = AsyncMock(side_effect=_load_skills)
+ return service
+
+
+@pytest.mark.asyncio
+class TestGetConversationSkills:
+ """Test suite for get_conversation_skills endpoint."""
+
+ async def test_get_skills_returns_repo_and_knowledge_skills(self):
+ """Test successful retrieval of both repo and knowledge skills.
+
+ Arrange: Setup conversation, sandbox, and skills with different types
+ Act: Call get_conversation_skills endpoint
+ Assert: Response contains both repo and knowledge skills with correct types
+ """
+ # Arrange
+ conversation_id = uuid4()
+ sandbox_id = str(uuid4())
+ working_dir = '/workspace'
+
+ # Create mock conversation
+ mock_conversation = AppConversation(
+ id=conversation_id,
+ created_by_user_id='test-user',
+ sandbox_id=sandbox_id,
+ selected_repository='owner/repo',
+ sandbox_status=SandboxStatus.RUNNING,
+ )
+
+ # Create mock sandbox with agent server URL
+ mock_sandbox = SandboxInfo(
+ id=sandbox_id,
+ created_by_user_id='test-user',
+ status=SandboxStatus.RUNNING,
+ sandbox_spec_id=str(uuid4()),
+ session_api_key='test-api-key',
+ exposed_urls=[
+ ExposedUrl(name=AGENT_SERVER, url='http://localhost:8000', port=8000)
+ ],
+ )
+
+ # Create mock sandbox spec
+ mock_sandbox_spec = SandboxSpecInfo(
+ id=str(uuid4()), command=None, working_dir=working_dir
+ )
+
+ # Create mock skills - repo skill (no trigger)
+ repo_skill = Skill(
+ name='repo_skill',
+ content='Repository skill content',
+ trigger=None,
+ )
+
+ # Create mock skills - knowledge skill (with KeywordTrigger)
+ knowledge_skill = Skill(
+ name='knowledge_skill',
+ content='Knowledge skill content',
+ trigger=KeywordTrigger(keywords=['test', 'help']),
+ )
+
+ # Mock services
+ mock_user_context = MagicMock(spec=UserContext)
+ mock_app_conversation_service = _make_service_mock(
+ user_context=mock_user_context,
+ conversation_return=mock_conversation,
+ skills_return=[repo_skill, knowledge_skill],
+ )
+
+ mock_sandbox_service = MagicMock()
+ mock_sandbox_service.get_sandbox = AsyncMock(return_value=mock_sandbox)
+
+ mock_sandbox_spec_service = MagicMock()
+ mock_sandbox_spec_service.get_sandbox_spec = AsyncMock(
+ return_value=mock_sandbox_spec
+ )
+
+ # Act
+ response = await get_conversation_skills(
+ conversation_id=conversation_id,
+ app_conversation_service=mock_app_conversation_service,
+ sandbox_service=mock_sandbox_service,
+ sandbox_spec_service=mock_sandbox_spec_service,
+ )
+
+ # Assert
+ assert response.status_code == status.HTTP_200_OK
+ content = response.body.decode('utf-8')
+ import json
+
+ data = json.loads(content)
+ assert 'skills' in data
+ assert len(data['skills']) == 2
+
+ # Check repo skill
+ repo_skill_data = next(
+ (s for s in data['skills'] if s['name'] == 'repo_skill'), None
+ )
+ assert repo_skill_data is not None
+ assert repo_skill_data['type'] == 'repo'
+ assert repo_skill_data['content'] == 'Repository skill content'
+ assert repo_skill_data['triggers'] == []
+
+ # Check knowledge skill
+ knowledge_skill_data = next(
+ (s for s in data['skills'] if s['name'] == 'knowledge_skill'), None
+ )
+ assert knowledge_skill_data is not None
+ assert knowledge_skill_data['type'] == 'knowledge'
+ assert knowledge_skill_data['content'] == 'Knowledge skill content'
+ assert knowledge_skill_data['triggers'] == ['test', 'help']
+
+ async def test_get_skills_returns_404_when_conversation_not_found(self):
+ """Test endpoint returns 404 when conversation doesn't exist.
+
+ Arrange: Setup mocks to return None for conversation
+ Act: Call get_conversation_skills endpoint
+ Assert: Response is 404 with appropriate error message
+ """
+ # Arrange
+ conversation_id = uuid4()
+
+ mock_user_context = MagicMock(spec=UserContext)
+ mock_app_conversation_service = _make_service_mock(
+ user_context=mock_user_context,
+ conversation_return=None,
+ )
+
+ mock_sandbox_service = MagicMock()
+ mock_sandbox_spec_service = MagicMock()
+
+ # Act
+ response = await get_conversation_skills(
+ conversation_id=conversation_id,
+ app_conversation_service=mock_app_conversation_service,
+ sandbox_service=mock_sandbox_service,
+ sandbox_spec_service=mock_sandbox_spec_service,
+ )
+
+ # Assert
+ assert response.status_code == status.HTTP_404_NOT_FOUND
+ content = response.body.decode('utf-8')
+ import json
+
+ data = json.loads(content)
+ assert 'error' in data
+ assert str(conversation_id) in data['error']
+
+ async def test_get_skills_returns_404_when_sandbox_not_found(self):
+ """Test endpoint returns 404 when sandbox doesn't exist.
+
+ Arrange: Setup conversation but no sandbox
+ Act: Call get_conversation_skills endpoint
+ Assert: Response is 404 with sandbox error message
+ """
+ # Arrange
+ conversation_id = uuid4()
+ sandbox_id = str(uuid4())
+
+ mock_conversation = AppConversation(
+ id=conversation_id,
+ created_by_user_id='test-user',
+ sandbox_id=sandbox_id,
+ sandbox_status=SandboxStatus.RUNNING,
+ )
+
+ mock_user_context = MagicMock(spec=UserContext)
+ mock_app_conversation_service = _make_service_mock(
+ user_context=mock_user_context,
+ conversation_return=mock_conversation,
+ )
+
+ mock_sandbox_service = MagicMock()
+ mock_sandbox_service.get_sandbox = AsyncMock(return_value=None)
+
+ mock_sandbox_spec_service = MagicMock()
+
+ # Act
+ response = await get_conversation_skills(
+ conversation_id=conversation_id,
+ app_conversation_service=mock_app_conversation_service,
+ sandbox_service=mock_sandbox_service,
+ sandbox_spec_service=mock_sandbox_spec_service,
+ )
+
+ # Assert
+ assert response.status_code == status.HTTP_404_NOT_FOUND
+ content = response.body.decode('utf-8')
+ import json
+
+ data = json.loads(content)
+ assert 'error' in data
+ assert 'Sandbox not found' in data['error']
+
+ async def test_get_skills_returns_404_when_sandbox_not_running(self):
+ """Test endpoint returns 404 when sandbox is not in RUNNING state.
+
+ Arrange: Setup conversation with stopped sandbox
+ Act: Call get_conversation_skills endpoint
+ Assert: Response is 404 with sandbox not running message
+ """
+ # Arrange
+ conversation_id = uuid4()
+ sandbox_id = str(uuid4())
+
+ mock_conversation = AppConversation(
+ id=conversation_id,
+ created_by_user_id='test-user',
+ sandbox_id=sandbox_id,
+ sandbox_status=SandboxStatus.PAUSED,
+ )
+
+ mock_sandbox = SandboxInfo(
+ id=sandbox_id,
+ created_by_user_id='test-user',
+ status=SandboxStatus.PAUSED,
+ sandbox_spec_id=str(uuid4()),
+ session_api_key='test-api-key',
+ )
+
+ mock_user_context = MagicMock(spec=UserContext)
+ mock_app_conversation_service = _make_service_mock(
+ user_context=mock_user_context,
+ conversation_return=mock_conversation,
+ )
+
+ mock_sandbox_service = MagicMock()
+ mock_sandbox_service.get_sandbox = AsyncMock(return_value=mock_sandbox)
+
+ mock_sandbox_spec_service = MagicMock()
+
+ # Act
+ response = await get_conversation_skills(
+ conversation_id=conversation_id,
+ app_conversation_service=mock_app_conversation_service,
+ sandbox_service=mock_sandbox_service,
+ sandbox_spec_service=mock_sandbox_spec_service,
+ )
+
+ # Assert
+ assert response.status_code == status.HTTP_404_NOT_FOUND
+ content = response.body.decode('utf-8')
+ import json
+
+ data = json.loads(content)
+ assert 'error' in data
+ assert 'not running' in data['error']
+
+ async def test_get_skills_handles_task_trigger_skills(self):
+ """Test endpoint correctly handles skills with TaskTrigger.
+
+ Arrange: Setup skill with TaskTrigger
+ Act: Call get_conversation_skills endpoint
+ Assert: Skill is categorized as knowledge type with correct triggers
+ """
+ # Arrange
+ conversation_id = uuid4()
+ sandbox_id = str(uuid4())
+
+ mock_conversation = AppConversation(
+ id=conversation_id,
+ created_by_user_id='test-user',
+ sandbox_id=sandbox_id,
+ sandbox_status=SandboxStatus.RUNNING,
+ )
+
+ mock_sandbox = SandboxInfo(
+ id=sandbox_id,
+ created_by_user_id='test-user',
+ status=SandboxStatus.RUNNING,
+ sandbox_spec_id=str(uuid4()),
+ session_api_key='test-api-key',
+ exposed_urls=[
+ ExposedUrl(name=AGENT_SERVER, url='http://localhost:8000', port=8000)
+ ],
+ )
+
+ mock_sandbox_spec = SandboxSpecInfo(
+ id=str(uuid4()), command=None, working_dir='/workspace'
+ )
+
+ # Create task skill with TaskTrigger
+ task_skill = Skill(
+ name='task_skill',
+ content='Task skill content',
+ trigger=TaskTrigger(triggers=['task', 'execute']),
+ )
+
+ mock_user_context = MagicMock(spec=UserContext)
+ mock_app_conversation_service = _make_service_mock(
+ user_context=mock_user_context,
+ conversation_return=mock_conversation,
+ skills_return=[task_skill],
+ )
+
+ mock_sandbox_service = MagicMock()
+ mock_sandbox_service.get_sandbox = AsyncMock(return_value=mock_sandbox)
+
+ mock_sandbox_spec_service = MagicMock()
+ mock_sandbox_spec_service.get_sandbox_spec = AsyncMock(
+ return_value=mock_sandbox_spec
+ )
+
+ # Act
+ response = await get_conversation_skills(
+ conversation_id=conversation_id,
+ app_conversation_service=mock_app_conversation_service,
+ sandbox_service=mock_sandbox_service,
+ sandbox_spec_service=mock_sandbox_spec_service,
+ )
+
+ # Assert
+ assert response.status_code == status.HTTP_200_OK
+ content = response.body.decode('utf-8')
+ import json
+
+ data = json.loads(content)
+ assert len(data['skills']) == 1
+ skill_data = data['skills'][0]
+ assert skill_data['type'] == 'knowledge'
+ assert skill_data['triggers'] == ['task', 'execute']
+
+ async def test_get_skills_returns_500_on_skill_loading_error(self):
+ """Test endpoint returns 500 when skill loading fails.
+
+ Arrange: Setup mocks to raise exception during skill loading
+ Act: Call get_conversation_skills endpoint
+ Assert: Response is 500 with error message
+ """
+ # Arrange
+ conversation_id = uuid4()
+ sandbox_id = str(uuid4())
+
+ mock_conversation = AppConversation(
+ id=conversation_id,
+ created_by_user_id='test-user',
+ sandbox_id=sandbox_id,
+ sandbox_status=SandboxStatus.RUNNING,
+ )
+
+ mock_sandbox = SandboxInfo(
+ id=sandbox_id,
+ created_by_user_id='test-user',
+ status=SandboxStatus.RUNNING,
+ sandbox_spec_id=str(uuid4()),
+ session_api_key='test-api-key',
+ exposed_urls=[
+ ExposedUrl(name=AGENT_SERVER, url='http://localhost:8000', port=8000)
+ ],
+ )
+
+ mock_sandbox_spec = SandboxSpecInfo(
+ id=str(uuid4()), command=None, working_dir='/workspace'
+ )
+
+ mock_user_context = MagicMock(spec=UserContext)
+ mock_app_conversation_service = _make_service_mock(
+ user_context=mock_user_context,
+ conversation_return=mock_conversation,
+ raise_on_load=True,
+ )
+
+ mock_sandbox_service = MagicMock()
+ mock_sandbox_service.get_sandbox = AsyncMock(return_value=mock_sandbox)
+
+ mock_sandbox_spec_service = MagicMock()
+ mock_sandbox_spec_service.get_sandbox_spec = AsyncMock(
+ return_value=mock_sandbox_spec
+ )
+
+ # Act
+ response = await get_conversation_skills(
+ conversation_id=conversation_id,
+ app_conversation_service=mock_app_conversation_service,
+ sandbox_service=mock_sandbox_service,
+ sandbox_spec_service=mock_sandbox_spec_service,
+ )
+
+ # Assert
+ assert response.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR
+ content = response.body.decode('utf-8')
+ import json
+
+ data = json.loads(content)
+ assert 'error' in data
+ assert 'Error getting skills' in data['error']
+
+ async def test_get_skills_returns_empty_list_when_no_skills_loaded(self):
+ """Test endpoint returns empty skills list when no skills are found.
+
+ Arrange: Setup all skill loaders to return empty lists
+ Act: Call get_conversation_skills endpoint
+ Assert: Response contains empty skills array
+ """
+ # Arrange
+ conversation_id = uuid4()
+ sandbox_id = str(uuid4())
+
+ mock_conversation = AppConversation(
+ id=conversation_id,
+ created_by_user_id='test-user',
+ sandbox_id=sandbox_id,
+ sandbox_status=SandboxStatus.RUNNING,
+ )
+
+ mock_sandbox = SandboxInfo(
+ id=sandbox_id,
+ created_by_user_id='test-user',
+ status=SandboxStatus.RUNNING,
+ sandbox_spec_id=str(uuid4()),
+ session_api_key='test-api-key',
+ exposed_urls=[
+ ExposedUrl(name=AGENT_SERVER, url='http://localhost:8000', port=8000)
+ ],
+ )
+
+ mock_sandbox_spec = SandboxSpecInfo(
+ id=str(uuid4()), command=None, working_dir='/workspace'
+ )
+
+ mock_user_context = MagicMock(spec=UserContext)
+ mock_app_conversation_service = _make_service_mock(
+ user_context=mock_user_context,
+ conversation_return=mock_conversation,
+ skills_return=[],
+ )
+
+ mock_sandbox_service = MagicMock()
+ mock_sandbox_service.get_sandbox = AsyncMock(return_value=mock_sandbox)
+
+ mock_sandbox_spec_service = MagicMock()
+ mock_sandbox_spec_service.get_sandbox_spec = AsyncMock(
+ return_value=mock_sandbox_spec
+ )
+
+ # Act
+ response = await get_conversation_skills(
+ conversation_id=conversation_id,
+ app_conversation_service=mock_app_conversation_service,
+ sandbox_service=mock_sandbox_service,
+ sandbox_spec_service=mock_sandbox_spec_service,
+ )
+
+ # Assert
+ assert response.status_code == status.HTTP_200_OK
+ content = response.body.decode('utf-8')
+ import json
+
+ data = json.loads(content)
+ assert 'skills' in data
+ assert len(data['skills']) == 0
diff --git a/tests/unit/memory/test_conversation_memory.py b/tests/unit/memory/test_conversation_memory.py
index abaa8d9a3d..50fd48f49a 100644
--- a/tests/unit/memory/test_conversation_memory.py
+++ b/tests/unit/memory/test_conversation_memory.py
@@ -158,7 +158,8 @@ def test_ensure_initial_user_message_adds_if_only_system(
system_message = SystemMessageAction(content='System')
system_message._source = EventSource.AGENT
events = [system_message]
- conversation_memory._ensure_initial_user_message(events, initial_user_action)
+ # Pass empty set for forgotten_event_ids (no events have been condensed)
+ conversation_memory._ensure_initial_user_message(events, initial_user_action, set())
assert len(events) == 2
assert events[0] == system_message
assert events[1] == initial_user_action
@@ -177,7 +178,8 @@ def test_ensure_initial_user_message_correct_already_present(
agent_message,
]
original_events = list(events)
- conversation_memory._ensure_initial_user_message(events, initial_user_action)
+ # Pass empty set for forgotten_event_ids (no events have been condensed)
+ conversation_memory._ensure_initial_user_message(events, initial_user_action, set())
assert events == original_events
@@ -189,7 +191,8 @@ def test_ensure_initial_user_message_incorrect_at_index_1(
incorrect_second_message = MessageAction(content='Assistant')
incorrect_second_message._source = EventSource.AGENT
events = [system_message, incorrect_second_message]
- conversation_memory._ensure_initial_user_message(events, initial_user_action)
+ # Pass empty set for forgotten_event_ids (no events have been condensed)
+ conversation_memory._ensure_initial_user_message(events, initial_user_action, set())
assert len(events) == 3
assert events[0] == system_message
assert events[1] == initial_user_action # Correct one inserted
@@ -206,7 +209,8 @@ def test_ensure_initial_user_message_correct_present_later(
# Correct initial message is present, but later in the list
events = [system_message, incorrect_second_message]
conversation_memory._ensure_system_message(events)
- conversation_memory._ensure_initial_user_message(events, initial_user_action)
+ # Pass empty set for forgotten_event_ids (no events have been condensed)
+ conversation_memory._ensure_initial_user_message(events, initial_user_action, set())
assert len(events) == 3 # Should still insert at index 1, not remove the later one
assert events[0] == system_message
assert events[1] == initial_user_action # Correct one inserted at index 1
@@ -222,7 +226,8 @@ def test_ensure_initial_user_message_different_user_msg_at_index_1(
different_user_message = MessageAction(content='Different User Message')
different_user_message._source = EventSource.USER
events = [system_message, different_user_message]
- conversation_memory._ensure_initial_user_message(events, initial_user_action)
+ # Pass empty set for forgotten_event_ids (no events have been condensed)
+ conversation_memory._ensure_initial_user_message(events, initial_user_action, set())
assert len(events) == 2
assert events[0] == system_message
assert events[1] == different_user_message # Original second message remains
@@ -1583,3 +1588,132 @@ def test_process_ipython_observation_with_vision_disabled(
assert isinstance(message.content[1], ImageContent)
# Check that NO explanatory text about filtered images was added when vision is disabled
assert 'invalid or empty image(s) were filtered' not in message.content[0].text
+
+
+def test_ensure_initial_user_message_not_reinserted_when_condensed(
+ conversation_memory, initial_user_action
+):
+ """Test that initial user message is NOT re-inserted when it has been condensed.
+
+ This is a critical test for bug #11910: Old instructions should not be re-executed
+ after conversation condensation. If the initial user message has been condensed
+ (its ID is in the forgotten_event_ids set), we should NOT re-insert it to prevent
+ the LLM from seeing old instructions as fresh commands.
+ """
+ system_message = SystemMessageAction(content='System')
+ system_message._source = EventSource.AGENT
+
+ # Simulate that the initial_user_action has been condensed by adding its ID
+ # to the forgotten_event_ids set
+ initial_user_action._id = 1 # Assign an ID to the initial user action
+ forgotten_event_ids = {1} # The initial user action's ID is in the forgotten set
+
+ events = [system_message] # Only system message, no user message
+
+ # Call _ensure_initial_user_message with the condensed event ID
+ conversation_memory._ensure_initial_user_message(
+ events, initial_user_action, forgotten_event_ids
+ )
+
+ # The initial user action should NOT be inserted because it was condensed
+ assert len(events) == 1
+ assert events[0] == system_message
+ # Verify the initial user action was NOT added
+ assert initial_user_action not in events
+
+
+def test_ensure_initial_user_message_reinserted_when_not_condensed(
+ conversation_memory, initial_user_action
+):
+ """Test that initial user message IS re-inserted when it has NOT been condensed.
+
+ This ensures backward compatibility: when no condensation has happened,
+ the initial user message should still be inserted as before.
+ """
+ system_message = SystemMessageAction(content='System')
+ system_message._source = EventSource.AGENT
+
+ # The initial user action has NOT been condensed
+ initial_user_action._id = 1
+ forgotten_event_ids = {5, 10, 15} # Different IDs, not including the initial action
+
+ events = [system_message]
+
+ # Call _ensure_initial_user_message with non-matching forgotten IDs
+ conversation_memory._ensure_initial_user_message(
+ events, initial_user_action, forgotten_event_ids
+ )
+
+ # The initial user action SHOULD be inserted because it was NOT condensed
+ assert len(events) == 2
+ assert events[0] == system_message
+ assert events[1] == initial_user_action
+
+
+def test_process_events_does_not_reinsert_condensed_initial_message(
+ conversation_memory,
+):
+ """Test that process_events does not re-insert initial user message when condensed.
+
+ This is an integration test for the full process_events flow, verifying that
+ when the initial user message has been condensed, it is not re-inserted into
+ the conversation sent to the LLM.
+ """
+ # Create a system message
+ system_message = SystemMessageAction(content='System message')
+ system_message._source = EventSource.AGENT
+ system_message._id = 0
+
+ # Create the initial user message (will be marked as condensed)
+ initial_user_message = MessageAction(content='Do task A, B, and C')
+ initial_user_message._source = EventSource.USER
+ initial_user_message._id = 1
+
+ # Create a condensation summary observation
+ from openhands.events.observation.agent import AgentCondensationObservation
+
+ condensation_summary = AgentCondensationObservation(
+ content='Summary: User requested tasks A, B, C. Task A was completed successfully.'
+ )
+ condensation_summary._id = 2
+
+ # Create a recent user message (not condensed)
+ recent_user_message = MessageAction(content='Now continue with task D')
+ recent_user_message._source = EventSource.USER
+ recent_user_message._id = 3
+
+ # Simulate condensed history: system + summary + recent message
+ # The initial user message (id=1) has been condensed/forgotten
+ condensed_history = [system_message, condensation_summary, recent_user_message]
+
+ # The initial user message's ID is in the forgotten set
+ forgotten_event_ids = {1}
+
+ messages = conversation_memory.process_events(
+ condensed_history=condensed_history,
+ initial_user_action=initial_user_message,
+ forgotten_event_ids=forgotten_event_ids,
+ max_message_chars=None,
+ vision_is_active=False,
+ )
+
+ # Verify the structure of messages
+ # Should have: system, condensation summary, recent user message
+ # Should NOT have the initial user message "Do task A, B, and C"
+ assert len(messages) == 3
+ assert messages[0].role == 'system'
+ assert messages[0].content[0].text == 'System message'
+
+ # The second message should be the condensation summary, NOT the initial user message
+ assert messages[1].role == 'user'
+ assert 'Summary: User requested tasks A, B, C' in messages[1].content[0].text
+
+ # The third message should be the recent user message
+ assert messages[2].role == 'user'
+ assert 'Now continue with task D' in messages[2].content[0].text
+
+ # Critically, the old instruction should NOT appear
+ for msg in messages:
+ for content in msg.content:
+ if hasattr(content, 'text'):
+ assert 'Do task A, B, and C' not in content.text