Compare commits

..

2 Commits

Author SHA1 Message Date
openhands a6e91d1021 Add support for organization-level setup.sh with minimal changes 2025-07-10 15:20:05 +00:00
openhands c7e9f99759 Add support for organization-level setup.sh 2025-07-10 15:11:56 +00:00
89 changed files with 1737 additions and 7267 deletions
+111 -17
View File
@@ -40,8 +40,7 @@ jobs:
# Only build nikolaik on PRs, otherwise build both nikolaik and ubuntu.
if [[ "$GITHUB_EVENT_NAME" == "pull_request" ]]; then
json=$(jq -n -c '[
{ image: "nikolaik/python-nodejs:python3.12-nodejs22", tag: "nikolaik" },
{ image: "ubuntu:24.04", tag: "ubuntu" }
{ image: "nikolaik/python-nodejs:python3.12-nodejs22", tag: "nikolaik" }
]')
else
json=$(jq -n -c '[
@@ -59,6 +58,9 @@ jobs:
permissions:
contents: read
packages: write
outputs:
# Since this job uses outputs it cannot use matrix
hash_from_app_image: ${{ steps.get_hash_in_app_image.outputs.hash_from_app_image }}
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -84,11 +86,24 @@ jobs:
if: "!github.event.pull_request.head.repo.fork"
run: |
./containers/build.sh -i openhands -o ${{ env.REPO_OWNER }} --push
- name: Build app image
if: "github.event.pull_request.head.repo.fork"
run: |
./containers/build.sh -i openhands -o ${{ env.REPO_OWNER }} --load
- name: Get hash in App Image
id: get_hash_in_app_image
run: |
# Run the build script in the app image
docker run -e SANDBOX_USER_ID=0 -v /var/run/docker.sock:/var/run/docker.sock ghcr.io/${{ env.REPO_OWNER }}/openhands:${{ env.RELEVANT_SHA }} /bin/bash -c "mkdir -p containers/runtime; python3 openhands/runtime/utils/runtime_build.py --base_image ${{ env.BASE_IMAGE_FOR_HASH_EQUIVALENCE_TEST }} --build_folder containers/runtime --force_rebuild" 2>&1 | tee docker-outputs.txt
# Get the hash from the build script
hash_from_app_image=$(cat docker-outputs.txt | grep "Hash for docker build directory" | awk -F "): " '{print $2}' | uniq | head -n1)
echo "hash_from_app_image=$hash_from_app_image" >> $GITHUB_OUTPUT
echo "Hash from app image: $hash_from_app_image"
# Builds the runtime Docker images
ghcr_build_runtime:
name: Build Image
runs-on: blacksmith-8vcpu-ubuntu-2204
runs-on: blacksmith-4vcpu-ubuntu-2204
if: "!(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/ext-v'))"
permissions:
contents: read
@@ -115,13 +130,22 @@ jobs:
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3
- name: Install poetry via pipx
run: pipx install poetry
- name: Set up Python
uses: useblacksmith/setup-python@v6
with:
python-version: '3.12'
cache: poetry
- name: Cache Poetry dependencies
uses: useblacksmith/cache@v5
with:
path: |
~/.cache/pypoetry
~/.virtualenvs
key: ${{ runner.os }}-poetry-${{ hashFiles('**/poetry.lock') }}
# This is the one that saves the cache, the others set 'lookup-only: true'
restore-keys: |
${{ runner.os }}-poetry-
- name: Install poetry via pipx
run: pipx install poetry
- name: Install Python dependencies using Poetry
run: make install-python-dependencies POETRY_GROUP=main INSTALL_PLAYWRIGHT=0
- name: Create source distribution and Dockerfile
@@ -166,6 +190,61 @@ jobs:
name: runtime-src-${{ matrix.base_image.tag }}
path: containers/runtime
verify_hash_equivalence_in_runtime_and_app:
name: Verify Hash Equivalence in Runtime and Docker images
runs-on: blacksmith-4vcpu-ubuntu-2204
needs: [ghcr_build_runtime, ghcr_build_app]
strategy:
fail-fast: false
matrix:
base_image: ['nikolaik']
env:
BASE_IMAGE_FOR_HASH_EQUIVALENCE_TEST: nikolaik/python-nodejs:python3.12-nodejs22
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Cache Poetry dependencies
uses: useblacksmith/cache@v5
with:
path: |
~/.cache/pypoetry
~/.virtualenvs
key: ${{ runner.os }}-poetry-${{ hashFiles('**/poetry.lock') }}
lookup-only: true
restore-keys: |
${{ runner.os }}-poetry-
- name: Set up Python
uses: useblacksmith/setup-python@v6
with:
python-version: '3.12'
- name: Install poetry via pipx
run: pipx install poetry
- name: Install Python dependencies using Poetry
run: make install-python-dependencies POETRY_GROUP=main INSTALL_PLAYWRIGHT=0
- name: Get hash in App Image
run: |
echo "Hash from app image: ${{ needs.ghcr_build_app.outputs.hash_from_app_image }}"
echo "hash_from_app_image=${{ needs.ghcr_build_app.outputs.hash_from_app_image }}" >> $GITHUB_ENV
- name: Get hash using code (development mode)
run: |
mkdir -p containers/runtime
poetry run python3 openhands/runtime/utils/runtime_build.py --base_image ${{ env.BASE_IMAGE_FOR_HASH_EQUIVALENCE_TEST }} --build_folder containers/runtime --force_rebuild > output.txt 2>&1
hash_from_code=$(cat output.txt | grep "Hash for docker build directory" | awk -F "): " '{print $2}' | uniq | head -n1)
echo "hash_from_code=$hash_from_code" >> $GITHUB_ENV
- name: Compare hashes
run: |
echo "Hash from App Image: ${{ env.hash_from_app_image }}"
echo "Hash from Code: ${{ env.hash_from_code }}"
if [ "${{ env.hash_from_app_image }}" = "${{ env.hash_from_code }}" ]; then
echo "Hashes match!"
else
echo "Hashes do not match!"
exit 1
fi
# Run unit tests with the Docker runtime Docker images as root
test_runtime_root:
name: RT Unit Tests (Root)
@@ -197,17 +276,25 @@ jobs:
load: true
tags: ghcr.io/${{ env.REPO_OWNER }}/runtime:${{ env.RELEVANT_SHA }}-${{ matrix.base_image.tag }}
context: containers/runtime
- name: Install poetry via pipx
run: pipx install poetry
- name: Cache Poetry dependencies
uses: useblacksmith/cache@v5
with:
path: |
~/.cache/pypoetry
~/.virtualenvs
key: ${{ runner.os }}-poetry-${{ hashFiles('**/poetry.lock') }}
lookup-only: true
restore-keys: |
${{ runner.os }}-poetry-
- name: Set up Python
uses: useblacksmith/setup-python@v6
with:
python-version: '3.12'
cache: poetry
- name: Install poetry via pipx
run: pipx install poetry
- name: Install Python dependencies using Poetry
run: make install-python-dependencies INSTALL_PLAYWRIGHT=0
- name: Run docker runtime tests
shell: bash
run: |
# We install pytest-xdist in order to run tests across CPUs
poetry run pip install pytest-xdist
@@ -259,17 +346,25 @@ jobs:
load: true
tags: ghcr.io/${{ env.REPO_OWNER }}/runtime:${{ env.RELEVANT_SHA }}-${{ matrix.base_image.tag }}
context: containers/runtime
- name: Install poetry via pipx
run: pipx install poetry
- name: Cache Poetry dependencies
uses: useblacksmith/cache@v5
with:
path: |
~/.cache/pypoetry
~/.virtualenvs
key: ${{ runner.os }}-poetry-${{ hashFiles('**/poetry.lock') }}
lookup-only: true
restore-keys: |
${{ runner.os }}-poetry-
- name: Set up Python
uses: useblacksmith/setup-python@v6
with:
python-version: '3.12'
cache: poetry
- name: Install poetry via pipx
run: pipx install poetry
- name: Install Python dependencies using Poetry
run: make install-python-dependencies POETRY_GROUP=main,test,runtime INSTALL_PLAYWRIGHT=0
- name: Run runtime tests
shell: bash
run: |
# We install pytest-xdist in order to run tests across CPUs
poetry run pip install pytest-xdist
@@ -296,7 +391,7 @@ jobs:
name: All Runtime Tests Passed
if: ${{ !cancelled() && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') }}
runs-on: blacksmith-4vcpu-ubuntu-2204
needs: [test_runtime_root, test_runtime_oh]
needs: [test_runtime_root, test_runtime_oh, verify_hash_equivalence_in_runtime_and_app]
steps:
- name: All tests passed
run: echo "All runtime tests have passed successfully!"
@@ -305,7 +400,7 @@ jobs:
name: All Runtime Tests Passed
if: ${{ cancelled() || contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}
runs-on: blacksmith-4vcpu-ubuntu-2204
needs: [test_runtime_root, test_runtime_oh]
needs: [test_runtime_root, test_runtime_oh, verify_hash_equivalence_in_runtime_and_app]
steps:
- name: Some tests failed
run: |
@@ -330,7 +425,6 @@ jobs:
PR_NUMBER: ${{ github.event.pull_request.number }}
REPO: ${{ github.repository }}
SHORT_SHA: ${{ steps.short_sha.outputs.SHORT_SHA }}
shell: bash
run: |
echo "updating PR description"
DOCKER_RUN_COMMAND="docker run -it --rm \
-108
View File
@@ -1,108 +0,0 @@
name: Publish OpenHands UI Package
# * Always run on "main"
# * Run on PRs that have changes in the "openhands-ui" folder or this workflow
on:
push:
branches:
- main
paths:
- "openhands-ui/**"
- ".github/workflows/npm-publish-ui.yml"
# If triggered by a PR, it will be in the same group. However, each commit on main will be in its own unique group
concurrency:
group: npm-publish-ui
cancel-in-progress: false
jobs:
check-version:
name: Check if version has changed
runs-on: blacksmith-4vcpu-ubuntu-2204
defaults:
run:
shell: bash
outputs:
should-publish: ${{ steps.version-check.outputs.should-publish }}
current-version: ${{ steps.version-check.outputs.current-version }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 2 # Need previous commit to compare
- name: Check if version changed
id: version-check
run: |
# Get current version from package.json
CURRENT_VERSION=$(jq -r .version openhands-ui/package.json)
echo "current-version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
# Check if package.json version changed in this commit
if git diff HEAD~1 HEAD --name-only | grep -q "openhands-ui/package.json"; then
# Check if the version field specifically changed
if git diff HEAD~1 HEAD openhands-ui/package.json | grep -q '"version"'; then
echo "Version changed in package.json, will publish"
echo "should-publish=true" >> $GITHUB_OUTPUT
else
echo "package.json changed but version did not change, skipping publish"
echo "should-publish=false" >> $GITHUB_OUTPUT
fi
else
echo "package.json did not change, skipping publish"
echo "should-publish=false" >> $GITHUB_OUTPUT
fi
publish:
name: Publish to npm
runs-on: blacksmith-4vcpu-ubuntu-2204
needs: check-version
if: needs.check-version.outputs.should-publish == 'true'
defaults:
run:
shell: bash
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version-file: "openhands-ui/.bun-version"
- name: Install dependencies
working-directory: ./openhands-ui
run: bun install --frozen-lockfile
- name: Build package
working-directory: ./openhands-ui
run: bun run build
- name: Check if package already exists on npm
id: npm-check
working-directory: ./openhands-ui
run: |
PACKAGE_NAME=$(jq -r .name package.json)
VERSION="${{ needs.check-version.outputs.current-version }}"
# Check if this version already exists on npm
if npm view "$PACKAGE_NAME@$VERSION" version 2>/dev/null; then
echo "Version $VERSION already exists on npm, skipping publish"
echo "already-exists=true" >> $GITHUB_OUTPUT
else
echo "Version $VERSION does not exist on npm, proceeding with publish"
echo "already-exists=false" >> $GITHUB_OUTPUT
fi
- name: Setup npm authentication
if: steps.npm-check.outputs.already-exists == 'false'
run: |
echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc
- name: Publish to npm
if: steps.npm-check.outputs.already-exists == 'false'
working-directory: ./openhands-ui
run: |
# The prepublishOnly script will run automatically and build the package
npm publish
echo "✅ Successfully published @openhands/ui@${{ needs.check-version.outputs.current-version }} to npm"
-34
View File
@@ -1,34 +0,0 @@
name: Run UI Component Build
# * Always run on "main"
# * Run on PRs that have changes in the "openhands-ui" folder or this workflow
on:
push:
branches:
- main
pull_request:
paths:
- 'openhands-ui/**'
- '.github/workflows/ui-build.yml'
# If triggered by a PR, it will be in the same group. However, each commit on main will be in its own unique group
concurrency:
group: ${{ github.workflow }}-${{ (github.head_ref && github.ref) || github.run_id }}
cancel-in-progress: true
jobs:
ui-build:
name: Build openhands-ui
runs-on: blacksmith-4vcpu-ubuntu-2204
steps:
- name: Checkout
uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
with:
bun-version-file: "openhands-ui/.bun-version"
- name: Install dependencies
working-directory: ./openhands-ui
run: bun install --frozen-lockfile
- name: Build package
working-directory: ./openhands-ui
run: bun run build
+1 -1
View File
@@ -159,7 +159,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/all-hands-ai/runtime:0.49-nikolaik`
Example: `export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.48-nikolaik`
## Develop inside Docker container
+3 -3
View File
@@ -62,17 +62,17 @@ system requirements and more information.
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.49-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.48-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.49-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.48-nikolaik \
-e LOG_ALL_EVENTS=true \
-v /var/run/docker.sock:/var/run/docker.sock \
-v ~/.openhands:/.openhands \
-p 3000:3000 \
--add-host host.docker.internal:host-gateway \
--name openhands-app \
docker.all-hands.dev/all-hands-ai/openhands:0.49
docker.all-hands.dev/all-hands-ai/openhands:0.48
```
> **Note**: If you used OpenHands before version 0.44, you may want to run `mv ~/.openhands-state ~/.openhands` to migrate your conversation history to the new location.
+3 -3
View File
@@ -51,17 +51,17 @@ OpenHands也可以使用Docker在本地系统上运行。
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.49-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.48-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.49-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.48-nikolaik \
-e LOG_ALL_EVENTS=true \
-v /var/run/docker.sock:/var/run/docker.sock \
-v ~/.openhands:/.openhands \
-p 3000:3000 \
--add-host host.docker.internal:host-gateway \
--name openhands-app \
docker.all-hands.dev/all-hands-ai/openhands:0.49
docker.all-hands.dev/all-hands-ai/openhands:0.48
```
> **注意**: 如果您在0.44版本之前使用过OpenHands,您可能需要运行 `mv ~/.openhands-state ~/.openhands` 来将对话历史迁移到新位置。
+3 -3
View File
@@ -42,17 +42,17 @@ OpenHandsはDockerを利用してローカル環境でも実行できます。
> 公共ネットワークで実行していますか?[Hardened Docker Installation Guide](https://docs.all-hands.dev/usage/runtimes/docker#hardened-docker-installation)を参照して、ネットワークバインディングの制限や追加のセキュリティ対策を実施してください。
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.49-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.48-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.49-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.48-nikolaik \
-e LOG_ALL_EVENTS=true \
-v /var/run/docker.sock:/var/run/docker.sock \
-v ~/.openhands:/.openhands \
-p 3000:3000 \
--add-host host.docker.internal:host-gateway \
--name openhands-app \
docker.all-hands.dev/all-hands-ai/openhands:0.49
docker.all-hands.dev/all-hands-ai/openhands:0.48
```
**注**: バージョン0.44以前のOpenHandsを使用していた場合は、会話履歴を移行するために `mv ~/.openhands-state ~/.openhands` を実行してください。
-1
View File
@@ -45,7 +45,6 @@ ENV OPENHANDS_BUILD_VERSION=$OPENHANDS_BUILD_VERSION
ENV SANDBOX_USER_ID=0
ENV FILE_STORE=local
ENV FILE_STORE_PATH=/.openhands
ENV INIT_GIT_IN_EMPTY_WORKSPACE=1
RUN mkdir -p $FILE_STORE_PATH
RUN mkdir -p $WORKSPACE_BASE
+1 -1
View File
@@ -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/all-hands-ai/runtime:0.49-nikolaik}
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.48-nikolaik}
- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234}
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
ports:
+1 -1
View File
@@ -7,7 +7,7 @@ services:
image: openhands:latest
container_name: openhands-app-${DATE:-}
environment:
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-docker.all-hands.dev/all-hands-ai/runtime:0.49-nikolaik}
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-docker.all-hands.dev/all-hands-ai/runtime:0.48-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:
+2 -2
View File
@@ -103,7 +103,7 @@ The conversation history will be saved in `~/.openhands/sessions`.
```bash
docker run -it \
--pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.49-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.48-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e SANDBOX_VOLUMES=$SANDBOX_VOLUMES \
-e LLM_API_KEY=$LLM_API_KEY \
@@ -112,7 +112,7 @@ docker run -it \
-v ~/.openhands:/.openhands \
--add-host host.docker.internal:host-gateway \
--name openhands-app-$(date +%Y%m%d%H%M%S) \
docker.all-hands.dev/all-hands-ai/openhands:0.49 \
docker.all-hands.dev/all-hands-ai/openhands:0.48 \
python -m openhands.cli.main --override-cli-mode true
```
+2 -2
View File
@@ -61,7 +61,7 @@ export GITHUB_TOKEN="your-token" # Required for repository operations
# Run OpenHands
docker run -it \
--pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.49-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.48-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e SANDBOX_VOLUMES=$SANDBOX_VOLUMES \
-e LLM_API_KEY=$LLM_API_KEY \
@@ -73,7 +73,7 @@ docker run -it \
-v ~/.openhands:/.openhands \
--add-host host.docker.internal:host-gateway \
--name openhands-app-$(date +%Y%m%d%H%M%S) \
docker.all-hands.dev/all-hands-ai/openhands:0.49 \
docker.all-hands.dev/all-hands-ai/openhands:0.48 \
python -m openhands.core.main -t "write a bash script that prints hi"
```
+4 -4
View File
@@ -68,23 +68,23 @@ Download and install the LM Studio desktop app from [lmstudio.ai](https://lmstud
1. Check [the installation guide](/usage/local-setup) and ensure all prerequisites are met before running OpenHands, then run:
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.49-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.48-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.49-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.48-nikolaik \
-e LOG_ALL_EVENTS=true \
-v /var/run/docker.sock:/var/run/docker.sock \
-v ~/.openhands:/.openhands \
-p 3000:3000 \
--add-host host.docker.internal:host-gateway \
--name openhands-app \
docker.all-hands.dev/all-hands-ai/openhands:0.49
docker.all-hands.dev/all-hands-ai/openhands:0.48
```
2. Wait until the server is running (see log below):
```
Digest: sha256:e72f9baecb458aedb9afc2cd5bc935118d1868719e55d50da73190d3a85c674f
Status: Image is up to date for docker.all-hands.dev/all-hands-ai/openhands:0.49
Status: Image is up to date for docker.all-hands.dev/all-hands-ai/openhands:0.48
Starting OpenHands...
Running OpenHands as root
14:22:13 - openhands:INFO: server_config.py:50 - Using config class None
+3 -3
View File
@@ -67,17 +67,17 @@ A system with a modern processor and a minimum of **4GB RAM** is recommended to
### Start the App
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.49-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.48-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.49-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.48-nikolaik \
-e LOG_ALL_EVENTS=true \
-v /var/run/docker.sock:/var/run/docker.sock \
-v ~/.openhands:/.openhands \
-p 3000:3000 \
--add-host host.docker.internal:host-gateway \
--name openhands-app \
docker.all-hands.dev/all-hands-ai/openhands:0.49
docker.all-hands.dev/all-hands-ai/openhands:0.48
```
> **Note**: If you used OpenHands before version 0.44, you may want to run `mv ~/.openhands-state ~/.openhands` to migrate your conversation history to the new location.
+1 -2
View File
@@ -101,14 +101,13 @@ The OpenHands evaluation harness supports a wide variety of benchmarks across [s
- SWE-Bench: [`evaluation/benchmarks/swe_bench`](./benchmarks/swe_bench)
- HumanEvalFix: [`evaluation/benchmarks/humanevalfix`](./benchmarks/humanevalfix)
- BIRD: [`evaluation/benchmarks/bird`](./benchmarks/bird)
- BioCoder: [`evaluation/benchmarks/biocoder`](./benchmarks/biocoder)
- BioCoder: [`evaluation/benchmarks/ml_bench`](./benchmarks/ml_bench)
- ML-Bench: [`evaluation/benchmarks/ml_bench`](./benchmarks/ml_bench)
- APIBench: [`evaluation/benchmarks/gorilla`](./benchmarks/gorilla/)
- ToolQA: [`evaluation/benchmarks/toolqa`](./benchmarks/toolqa/)
- AiderBench: [`evaluation/benchmarks/aider_bench`](./benchmarks/aider_bench/)
- Commit0: [`evaluation/benchmarks/commit0_bench`](./benchmarks/commit0_bench/)
- DiscoveryBench: [`evaluation/benchmarks/discoverybench`](./benchmarks/discoverybench/)
- TerminalBench: [`evaluation/benchmarks/terminal_bench`](./benchmarks/terminal_bench)
### Web Browsing
@@ -1,45 +0,0 @@
# **Localization Evaluation for SWE-Bench**
This folder implements localization evaluation at both file and function levels to complementing the assessment of agent inference on [SWE-Bench](https://www.swebench.com/).
## **1. Environment Setup**
- Python env: [Install python environment](../../../README.md#development-environment)
- LLM config: [Configure LLM config](../../../README.md#configure-openhands-and-your-llm)
## **2. Inference & Evaluation**
- Inference and evaluation follow the original `run_infer.sh` and `run_eval.sh` implementation
- You may refer to instructions at [README.md](../README.md) for running inference and evaluation on SWE-Bench
## **3. Localization Evaluation**
- Localization evaluation computes two-level localization accuracy, while also considers task success as an additional metric for overall evaluation:
- **File Localization Accuracy:** Accuracy of correctly localizing the target file
- **Function Localization Accuracy:** Accuracy of correctly localizing the target function
- **Resolve Rate** (will be auto-skipped if missing): Success rate of whether tasks are successfully resolved
- **File Localization Efficiency:** Average number of iterations taken to successfully localize the target file
- **Function Localization Efficiency:** Average number of iterations taken to successfully localize the target file
- **Task success efficiency:** Average number of iterations taken to resolve the task
- **Resource efficiency:** the API expenditure of the agent running inference on SWE-Bench instances
- Run localization evaluation
- Format:
```bash
./evaluation/benchmarks/swe_bench/scripts/eval_localization.sh [infer-dir] [split] [dataset] [max-infer-turn] [align-with-max]
```
- `infer-dir`: inference directory containing inference outputs
- `split`: SWE-Bench dataset split to use
- `dataset`: SWE-Bench dataset name
- `max-infer-turn`: the maximum number of iterations the agent took to run inference
- `align-with-max`: whether to align failure indices (e.g., incorrect localization, unresolved tasks) with `max_iter`
- Example:
```bash
# Example
./evaluation/benchmarks/swe_bench/scripts/eval_localization.sh \
--infer-dir ./evaluation/evaluation_outputs/outputs/princeton-nlp__SWE-bench_Verified-test/CodeActAgent/gpt_4o_100_N \
--split test \
--dataset princeton-nlp/SWE-bench_Verified \
--max-infer-turn 100 \
--align-with-max true
```
- Localization evaluation results will be automatically saved to `[infer-dir]/loc_eval`
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,227 +0,0 @@
#!/usr/bin/env bash
set -eo pipefail
source "evaluation/utils/version_control.sh"
# Function to display usage information
usage() {
echo "Usage: $0 [OPTIONS]"
echo "Options:"
echo " --infer-dir DIR Directory containing model inference outputs"
echo " --split SPLIT SWE-Bench dataset split selection"
echo " --dataset DATASET Dataset name"
echo " --max-infer-turn NUM Max number of turns for coding agent"
echo " --align-with-max BOOL Align failed instance indices with max iteration (true/false)"
echo " -h, --help Display this help message"
echo ""
echo "Example:"
echo " $0 --infer-dir ./inference_outputs --split test --align-with-max false"
}
# Check if no arguments were provided
if [ $# -eq 0 ]; then
usage
exit 1
fi
# Parse command line arguments
while [[ $# -gt 0 ]]; do
case $1 in
--infer-dir)
INFER_DIR="$2"
shift 2
;;
--split)
SPLIT="$2"
shift 2
;;
--dataset)
DATASET="$2"
shift 2
;;
--max-infer-turn)
MAX_TURN="$2"
shift 2
;;
--align-with-max)
ALIGN_WITH_MAX="$2"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown option: $1"
usage
exit 1
;;
esac
done
# Check for required arguments (only INFER_DIR is required)
if [ -z "$INFER_DIR" ]; then
echo "Error: Missing required arguments (--infer-dir is required)"
usage
exit 1
fi
# Set defaults for optional arguments if not provided
if [ -z "$SPLIT" ]; then
SPLIT="test"
echo "Split not specified, using default: $SPLIT"
fi
if [ -z "$DATASET" ]; then
DATASET="princeton-nlp/SWE-bench_Verified"
echo "Dataset not specified, using default: $DATASET"
fi
if [ -z "$MAX_TURN" ]; then
MAX_TURN=20
echo "Max inference turn not specified, using default: $MAX_TURN"
fi
if [ -z "$ALIGN_WITH_MAX" ]; then
ALIGN_WITH_MAX="true"
echo "Align with max not specified, using default: $ALIGN_WITH_MAX"
fi
# Validate align-with-max value
if [ "$ALIGN_WITH_MAX" != "true" ] && [ "$ALIGN_WITH_MAX" != "false" ]; then
print_error "Invalid value for --align-with-max: $ALIGN_WITH_MAX. Must be 'true' or 'false'"
exit 1
fi
# Color codes for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Function to print colored output
print_status() {
echo -e "${GREEN}[INFO]${NC} $1"
}
print_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
print_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
print_header() {
echo -e "${BLUE}[TASK]${NC} $1"
}
# Check if Python is available
print_header "Checking Python installation..."
if ! command -v python3 &> /dev/null; then
if ! command -v python &> /dev/null; then
print_error "Python is not installed or not in PATH"
exit 1
else
PYTHON_CMD="python"
print_status "Using python command"
fi
else
PYTHON_CMD="python3"
print_status "Using python3 command"
fi
# Check if the Python script exists
SCRIPT_NAME="./evaluation/benchmarks/swe_bench/loc_eval/loc_evaluator.py"
if [ ! -f "$SCRIPT_NAME" ]; then
print_error "Python script '$SCRIPT_NAME' not found in current directory"
print_warning "Make sure the Python script is in the same directory as this bash script"
exit 1
fi
# Check if required directories exist
print_header "Validating directories..."
if [ ! -d "$INFER_DIR" ]; then
print_error "Inference directory not found: $INFER_DIR"
exit 1
fi
# Evaluation outputs
EVAL_DIR="$INFER_DIR/eval_outputs"
# Display configuration
print_header "Starting Localization Evaluation with the following configuration:"
echo " Inference Directory: $INFER_DIR"
if [ -d "$EVAL_DIR" ]; then
echo " Evaluation Directory: $EVAL_DIR"
else
echo " Evaluation Directory: None (evaluation outputs doesn't exist)"
fi
echo " Output Directory: $INFER_DIR/loc_eval"
echo " Split: $SPLIT"
echo " Dataset: $DATASET"
echo " Max Turns: $MAX_TURN"
echo " Align with Max: $ALIGN_WITH_MAX"
echo " Python Command: $PYTHON_CMD"
echo ""
# Check Python dependencies (optional check)
print_header "Checking Python dependencies..."
$PYTHON_CMD -c "
import sys
required_modules = ['pandas', 'json', 'os', 'argparse', 'collections']
missing_modules = []
for module in required_modules:
try:
__import__(module)
except ImportError:
missing_modules.append(module)
if missing_modules:
print(f'Missing required modules: {missing_modules}')
sys.exit(1)
else:
print('All basic dependencies are available')
" || {
print_error "Some Python dependencies are missing"
print_warning "Please install required packages: pip install pandas"
exit 1
}
# Create log directory if doesn't exists
mkdir -p "$INFER_DIR/loc_eval"
# Set up logging
LOG_FILE="$INFER_DIR/loc_eval/loc_evaluation_$(date +%Y%m%d_%H%M%S).log"
print_status "Logging output to: $LOG_FILE"
# Build the command
CMD_ARGS="\"$SCRIPT_NAME\" \
--infer-dir \"$INFER_DIR\" \
--split \"$SPLIT\" \
--dataset \"$DATASET\" \
--max-infer-turn \"$MAX_TURN\" \
--align-with-max \"$ALIGN_WITH_MAX\""
# Run the Python script
print_header "Running localization evaluation..."
eval "$PYTHON_CMD $CMD_ARGS" 2>&1 | tee "$LOG_FILE"
# Check if the script ran successfully
if [ ${PIPESTATUS[0]} -eq 0 ]; then
print_status "Localization evaluation completed successfully!"
print_status "Results saved to: $INFER_DIR/loc_eval"
print_status "Log file: $LOG_FILE"
# Display summary if results exist
if [ -f "$INFER_DIR/loc_eval/loc_eval_results/loc_acc/overall_eval.json" ]; then
print_header "Evaluation Summary:"
cat "$INFER_DIR/loc_eval/loc_eval_results/loc_acc/overall_eval.json"
echo
fi
else
print_error "Localization evaluation failed!"
print_warning "Check the log file for details: $LOG_FILE"
exit 1
fi
@@ -1,31 +0,0 @@
# Terminal-Bench Evaluation on OpenHands
Terminal-Bench has its own evaluation harness that is very different from OpenHands'. We
implemented [OpenHands agent](https://github.com/laude-institute/terminal-bench/tree/main/terminal_bench/agents/installed_agents/openhands) using OpenHands local runtime
inside terminal-bench framework. Hereby we introduce how to use the terminal-bench
harness to evaluate OpenHands.
## Installation
Terminal-bench ships a CLI tool to manage tasks and run evaluation.
Please follow official [Installation Doc](https://www.tbench.ai/docs/installation). You could also clone terminal-bench [source code](https://github.com/laude-institute/terminal-bench) and use `uv run tb` CLI.
## Evaluation
Please see [Terminal-Bench Leaderboard](https://www.tbench.ai/leaderboard) for the latest
instruction on benchmarking guidance. The dataset might evolve.
Sample command:
```bash
export LLM_BASE_URL=<optional base url>
export LLM_API_KEY=<llm key>
tb run \
--dataset-name terminal-bench-core \
--dataset-version 0.1.1 \
--agent openhands \
--model <model> \
--cleanup
```
You could run `tb --help` or `tb run --help` to learn more about their CLI.
@@ -44,64 +44,4 @@ describe("AuthModal", () => {
expect(window.location.href).toBe(mockUrl);
});
it("should render Terms of Service and Privacy Policy text with correct links", () => {
render(<AuthModal githubAuthUrl="mock-url" appMode="saas" />);
// Find the terms of service section using data-testid
const termsSection = screen.getByTestId("auth-modal-terms-of-service");
expect(termsSection).toBeInTheDocument();
// Check that all text content is present in the paragraph
expect(termsSection).toHaveTextContent(
"AUTH$BY_SIGNING_UP_YOU_AGREE_TO_OUR",
);
expect(termsSection).toHaveTextContent("COMMON$TERMS_OF_SERVICE");
expect(termsSection).toHaveTextContent("COMMON$AND");
expect(termsSection).toHaveTextContent("COMMON$PRIVACY_POLICY");
// Check Terms of Service link
const tosLink = screen.getByRole("link", {
name: "COMMON$TERMS_OF_SERVICE",
});
expect(tosLink).toBeInTheDocument();
expect(tosLink).toHaveAttribute("href", "https://www.all-hands.dev/tos");
expect(tosLink).toHaveAttribute("target", "_blank");
expect(tosLink).toHaveClass("underline", "hover:text-primary");
// Check Privacy Policy link
const privacyLink = screen.getByRole("link", {
name: "COMMON$PRIVACY_POLICY",
});
expect(privacyLink).toBeInTheDocument();
expect(privacyLink).toHaveAttribute(
"href",
"https://www.all-hands.dev/privacy",
);
expect(privacyLink).toHaveAttribute("target", "_blank");
expect(privacyLink).toHaveClass("underline", "hover:text-primary");
// Verify that both links are within the terms section
expect(termsSection).toContainElement(tosLink);
expect(termsSection).toContainElement(privacyLink);
});
it("should open Terms of Service link in new tab", () => {
render(<AuthModal githubAuthUrl="mock-url" appMode="saas" />);
const tosLink = screen.getByRole("link", {
name: "COMMON$TERMS_OF_SERVICE",
});
expect(tosLink).toHaveAttribute("target", "_blank");
});
it("should open Privacy Policy link in new tab", () => {
render(<AuthModal githubAuthUrl="mock-url" appMode="saas" />);
const privacyLink = screen.getByRole("link", {
name: "COMMON$PRIVACY_POLICY",
});
expect(privacyLink).toHaveAttribute("target", "_blank");
});
});
@@ -529,287 +529,4 @@ describe("ConversationPanel", () => {
expect(screen.queryByTestId("stop-button")).not.toBeInTheDocument();
});
it("should show edit button in context menu", async () => {
const user = userEvent.setup();
renderConversationPanel();
const cards = await screen.findAllByTestId("conversation-card");
expect(cards).toHaveLength(3);
// Click ellipsis to open context menu
const ellipsisButton = within(cards[0]).getByTestId("ellipsis-button");
await user.click(ellipsisButton);
// Edit button should be visible
const editButton = screen.getByTestId("edit-button");
expect(editButton).toBeInTheDocument();
expect(editButton).toHaveTextContent("BUTTON$EDIT_TITLE");
});
it("should enter edit mode when edit button is clicked", async () => {
const user = userEvent.setup();
renderConversationPanel();
const cards = await screen.findAllByTestId("conversation-card");
// Click ellipsis to open context menu
const ellipsisButton = within(cards[0]).getByTestId("ellipsis-button");
await user.click(ellipsisButton);
// Click edit button
const editButton = screen.getByTestId("edit-button");
await user.click(editButton);
// Should find input field instead of title text
const titleInput = within(cards[0]).getByTestId("conversation-card-title");
expect(titleInput).toBeInTheDocument();
expect(titleInput.tagName).toBe("INPUT");
expect(titleInput).toHaveValue("Conversation 1");
expect(titleInput).toHaveFocus();
});
it("should successfully update conversation title", async () => {
const user = userEvent.setup();
// Mock the updateConversation API call
const updateConversationSpy = vi.spyOn(OpenHands, "updateConversation");
updateConversationSpy.mockResolvedValue(true);
// Mock the toast function
const mockToast = vi.fn();
vi.mock("#/utils/custom-toast-handlers", () => ({
displaySuccessToast: mockToast,
}));
renderConversationPanel();
const cards = await screen.findAllByTestId("conversation-card");
// Enter edit mode
const ellipsisButton = within(cards[0]).getByTestId("ellipsis-button");
await user.click(ellipsisButton);
const editButton = screen.getByTestId("edit-button");
await user.click(editButton);
// Edit the title
const titleInput = within(cards[0]).getByTestId("conversation-card-title");
await user.clear(titleInput);
await user.type(titleInput, "Updated Title");
// Blur the input to save
await user.tab();
// Verify API call was made with correct parameters
expect(updateConversationSpy).toHaveBeenCalledWith("1", {
title: "Updated Title",
});
});
it("should save title when Enter key is pressed", async () => {
const user = userEvent.setup();
const updateConversationSpy = vi.spyOn(OpenHands, "updateConversation");
updateConversationSpy.mockResolvedValue(true);
renderConversationPanel();
const cards = await screen.findAllByTestId("conversation-card");
// Enter edit mode
const ellipsisButton = within(cards[0]).getByTestId("ellipsis-button");
await user.click(ellipsisButton);
const editButton = screen.getByTestId("edit-button");
await user.click(editButton);
// Edit the title and press Enter
const titleInput = within(cards[0]).getByTestId("conversation-card-title");
await user.clear(titleInput);
await user.type(titleInput, "Title Updated via Enter");
await user.keyboard("{Enter}");
// Verify API call was made
expect(updateConversationSpy).toHaveBeenCalledWith("1", {
title: "Title Updated via Enter",
});
});
it("should trim whitespace from title", async () => {
const user = userEvent.setup();
const updateConversationSpy = vi.spyOn(OpenHands, "updateConversation");
updateConversationSpy.mockResolvedValue(true);
renderConversationPanel();
const cards = await screen.findAllByTestId("conversation-card");
// Enter edit mode
const ellipsisButton = within(cards[0]).getByTestId("ellipsis-button");
await user.click(ellipsisButton);
const editButton = screen.getByTestId("edit-button");
await user.click(editButton);
// Edit the title with extra whitespace
const titleInput = within(cards[0]).getByTestId("conversation-card-title");
await user.clear(titleInput);
await user.type(titleInput, " Trimmed Title ");
await user.tab();
// Verify API call was made with trimmed title
expect(updateConversationSpy).toHaveBeenCalledWith("1", {
title: "Trimmed Title",
});
// Verify input shows trimmed value
expect(titleInput).toHaveValue("Trimmed Title");
});
it("should revert to original title when empty", async () => {
const user = userEvent.setup();
const updateConversationSpy = vi.spyOn(OpenHands, "updateConversation");
updateConversationSpy.mockResolvedValue(true);
renderConversationPanel();
const cards = await screen.findAllByTestId("conversation-card");
// Enter edit mode
const ellipsisButton = within(cards[0]).getByTestId("ellipsis-button");
await user.click(ellipsisButton);
const editButton = screen.getByTestId("edit-button");
await user.click(editButton);
// Clear the title completely
const titleInput = within(cards[0]).getByTestId("conversation-card-title");
await user.clear(titleInput);
await user.tab();
// Verify API was not called
expect(updateConversationSpy).not.toHaveBeenCalled();
// Verify input reverted to original value
expect(titleInput).toHaveValue("Conversation 1");
});
it("should handle API error when updating title", async () => {
const user = userEvent.setup();
const updateConversationSpy = vi.spyOn(OpenHands, "updateConversation");
updateConversationSpy.mockRejectedValue(new Error("API Error"));
vi.mock("#/utils/custom-toast-handlers", () => ({
displayErrorToast: vi.fn(),
}));
renderConversationPanel();
const cards = await screen.findAllByTestId("conversation-card");
// Enter edit mode
const ellipsisButton = within(cards[0]).getByTestId("ellipsis-button");
await user.click(ellipsisButton);
const editButton = screen.getByTestId("edit-button");
await user.click(editButton);
// Edit the title
const titleInput = within(cards[0]).getByTestId("conversation-card-title");
await user.clear(titleInput);
await user.type(titleInput, "Failed Update");
await user.tab();
// Verify API call was made
expect(updateConversationSpy).toHaveBeenCalledWith("1", {
title: "Failed Update",
});
// Wait for error handling
await waitFor(() => {
expect(updateConversationSpy).toHaveBeenCalled();
});
});
it("should close context menu when edit button is clicked", async () => {
const user = userEvent.setup();
renderConversationPanel();
const cards = await screen.findAllByTestId("conversation-card");
// Click ellipsis to open context menu
const ellipsisButton = within(cards[0]).getByTestId("ellipsis-button");
await user.click(ellipsisButton);
// Verify context menu is open
const contextMenu = screen.getByTestId("context-menu");
expect(contextMenu).toBeInTheDocument();
// Click edit button
const editButton = screen.getByTestId("edit-button");
await user.click(editButton);
// Verify context menu is closed
expect(screen.queryByTestId("context-menu")).not.toBeInTheDocument();
});
it("should not call API when title is unchanged", async () => {
const user = userEvent.setup();
const updateConversationSpy = vi.spyOn(OpenHands, "updateConversation");
updateConversationSpy.mockResolvedValue(true);
renderConversationPanel();
const cards = await screen.findAllByTestId("conversation-card");
// Enter edit mode
const ellipsisButton = within(cards[0]).getByTestId("ellipsis-button");
await user.click(ellipsisButton);
const editButton = screen.getByTestId("edit-button");
await user.click(editButton);
// Don't change the title, just blur
const titleInput = within(cards[0]).getByTestId("conversation-card-title");
await user.tab();
// Verify API was called with the same title (since handleConversationTitleChange will always be called)
expect(updateConversationSpy).toHaveBeenCalledWith("1", {
title: "Conversation 1",
});
});
it("should handle special characters in title", async () => {
const user = userEvent.setup();
const updateConversationSpy = vi.spyOn(OpenHands, "updateConversation");
updateConversationSpy.mockResolvedValue(true);
renderConversationPanel();
const cards = await screen.findAllByTestId("conversation-card");
// Enter edit mode
const ellipsisButton = within(cards[0]).getByTestId("ellipsis-button");
await user.click(ellipsisButton);
const editButton = screen.getByTestId("edit-button");
await user.click(editButton);
// Edit the title with special characters
const titleInput = within(cards[0]).getByTestId("conversation-card-title");
await user.clear(titleInput);
await user.type(titleInput, "Special @#$%^&*()_+ Characters");
await user.tab();
// Verify API call was made with special characters
expect(updateConversationSpy).toHaveBeenCalledWith("1", {
title: "Special @#$%^&*()_+ Characters",
});
});
});
@@ -82,5 +82,17 @@ describe("extractModelAndProvider", () => {
model: "claude-opus-4-20250514",
separator: "/",
});
expect(extractModelAndProvider("claude-3-haiku-20240307")).toEqual({
provider: "anthropic",
model: "claude-3-haiku-20240307",
separator: "/",
});
expect(extractModelAndProvider("claude-2.1")).toEqual({
provider: "anthropic",
model: "claude-2.1",
separator: "/",
});
});
});
@@ -52,16 +52,14 @@ test("organizeModelsAndProviders", () => {
separator: "/",
models: [
"claude-3-5-sonnet-20241022",
],
},
other: {
separator: "",
models: [
"together-ai-21.1b-41b",
"claude-3-haiku-20240307",
"claude-2",
"claude-2.1",
],
},
other: {
separator: "",
models: ["together-ai-21.1b-41b"],
},
});
});
+759 -617
View File
File diff suppressed because it is too large Load Diff
+8 -8
View File
@@ -1,13 +1,13 @@
{
"name": "openhands-frontend",
"version": "0.49.0",
"version": "0.48.0",
"private": true,
"type": "module",
"engines": {
"node": ">=22.0.0"
},
"dependencies": {
"@heroui/react": "^2.8.0-beta.15",
"@heroui/react": "^2.8.0-beta.13",
"@microlink/react-json-view": "^1.26.2",
"@monaco-editor/react": "^4.7.0-rc.0",
"@react-router/node": "^7.6.3",
@@ -18,14 +18,14 @@
"@stripe/stripe-js": "^7.4.0",
"@tailwindcss/postcss": "^4.1.11",
"@tailwindcss/vite": "^4.1.11",
"@tanstack/react-query": "^5.83.0",
"@tanstack/react-query": "^5.81.4",
"@vitejs/plugin-react": "^4.6.0",
"@xterm/addon-fit": "^0.10.0",
"@xterm/xterm": "^5.4.0",
"axios": "^1.10.0",
"clsx": "^2.1.1",
"eslint-config-airbnb-typescript": "^18.0.0",
"framer-motion": "^12.23.5",
"framer-motion": "^12.23.0",
"i18next": "^25.3.2",
"i18next-browser-languagedetector": "^8.2.0",
"i18next-http-backend": "^3.0.2",
@@ -49,7 +49,7 @@
"sirv-cli": "^3.0.1",
"socket.io-client": "^4.8.1",
"tailwind-merge": "^3.3.1",
"vite": "^7.0.4",
"vite": "^7.0.3",
"web-vitals": "^5.0.3",
"ws": "^8.18.2"
},
@@ -81,9 +81,9 @@
"devDependencies": {
"@babel/parser": "^7.28.0",
"@babel/traverse": "^7.28.0",
"@babel/types": "^7.28.1",
"@babel/types": "^7.27.0",
"@mswjs/socket.io-binding": "^0.2.0",
"@playwright/test": "^1.54.1",
"@playwright/test": "^1.53.2",
"@react-router/dev": "^7.6.3",
"@tailwindcss/typography": "^0.5.16",
"@tanstack/eslint-plugin-query": "^5.81.2",
@@ -91,7 +91,7 @@
"@testing-library/jest-dom": "^6.6.1",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
"@types/node": "^24.0.13",
"@types/node": "^24.0.12",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"@types/react-highlight": "^0.12.8",
-12
View File
@@ -477,18 +477,6 @@ class OpenHands {
return data.prompt;
}
static async updateConversation(
conversationId: string,
updates: { title: string },
): Promise<boolean> {
const { data } = await openHands.patch<boolean>(
`/api/conversations/${conversationId}`,
updates,
);
return data;
}
}
export default OpenHands;
@@ -85,19 +85,21 @@ export function ChatMessage({
/>
</div>
<div className="text-sm break-words">
<Markdown
components={{
code,
ul,
ol,
a: anchor,
p: paragraph,
}}
remarkPlugins={[remarkGfm]}
>
{message}
</Markdown>
<div className="text-sm break-words flex">
<div>
<Markdown
components={{
code,
ul,
ol,
a: anchor,
p: paragraph,
}}
remarkPlugins={[remarkGfm]}
>
{message}
</Markdown>
</div>
</div>
{children}
</article>
@@ -164,7 +164,7 @@ export function EventMessage({
const message = parseMessageFromEvent(event);
return (
<>
<div className="flex flex-col self-end">
<ChatMessage type={event.source} message={message} actions={actions}>
{event.args.image_urls && event.args.image_urls.length > 0 && (
<ImageCarousel size="small" images={event.args.image_urls} />
@@ -184,7 +184,7 @@ export function EventMessage({
{isAssistantMessage(event) &&
event.action === "message" &&
renderLikertScale()}
</>
</div>
);
}
@@ -12,8 +12,6 @@ import { LoadingSpinner } from "#/components/shared/loading-spinner";
import { ExitConversationModal } from "./exit-conversation-modal";
import { useClickOutsideElement } from "#/hooks/use-click-outside-element";
import { Provider } from "#/types/settings";
import { useUpdateConversation } from "#/hooks/mutation/use-update-conversation";
import { displaySuccessToast } from "#/utils/custom-toast-handlers";
interface ConversationPanelProps {
onClose: () => void;
@@ -41,7 +39,6 @@ export function ConversationPanel({ onClose }: ConversationPanelProps) {
const { mutate: deleteConversation } = useDeleteConversation();
const { mutate: stopConversation } = useStopConversation();
const { mutate: updateConversation } = useUpdateConversation();
const handleDeleteProject = (conversationId: string) => {
setConfirmDeleteModalVisible(true);
@@ -53,20 +50,6 @@ export function ConversationPanel({ onClose }: ConversationPanelProps) {
setSelectedConversationId(conversationId);
};
const handleConversationTitleChange = async (
conversationId: string,
newTitle: string,
) => {
updateConversation(
{ conversationId, newTitle },
{
onSuccess: () => {
displaySuccessToast(t(I18nKey.CONVERSATION$TITLE_UPDATED));
},
},
);
};
const handleConfirmDelete = () => {
if (selectedConversationId) {
deleteConversation(
@@ -131,9 +114,6 @@ export function ConversationPanel({ onClose }: ConversationPanelProps) {
isActive={isActive}
onDelete={() => handleDeleteProject(project.conversation_id)}
onStop={() => handleStopConversation(project.conversation_id)}
onChangeTitle={(title) =>
handleConversationTitleChange(project.conversation_id, title)
}
title={project.title}
selectedRepository={{
selected_repository: project.selected_repository,
@@ -1,98 +0,0 @@
import { DiGit } from "react-icons/di";
import { FaServer, FaExternalLinkAlt } from "react-icons/fa";
import { useSelector } from "react-redux";
import { useTranslation } from "react-i18next";
import { VscCode } from "react-icons/vsc";
import { Container } from "#/components/layout/container";
import { I18nKey } from "#/i18n/declaration";
import { RootState } from "#/store";
import { RUNTIME_INACTIVE_STATES } from "#/types/agent-state";
import { ServedAppLabel } from "#/components/layout/served-app-label";
import { TabContent } from "#/components/layout/tab-content";
import { transformVSCodeUrl } from "#/utils/vscode-url-helper";
import { useConversationId } from "#/hooks/use-conversation-id";
import GlobeIcon from "#/icons/globe.svg?react";
import JupyterIcon from "#/icons/jupyter.svg?react";
import OpenHands from "#/api/open-hands";
import TerminalIcon from "#/icons/terminal.svg?react";
export function ConversationTabs() {
const { curAgentState } = useSelector((state: RootState) => state.agent);
const { conversationId } = useConversationId();
const { t } = useTranslation();
const basePath = `/conversations/${conversationId}`;
return (
<Container
className="h-full w-full"
labels={[
{
label: "Changes",
to: "",
icon: <DiGit className="w-6 h-6" />,
},
{
label: (
<div className="flex items-center gap-1">
{t(I18nKey.VSCODE$TITLE)}
</div>
),
to: "vscode",
icon: <VscCode className="w-5 h-5" />,
rightContent: !RUNTIME_INACTIVE_STATES.includes(curAgentState) ? (
<FaExternalLinkAlt
className="w-3 h-3 text-neutral-400 cursor-pointer"
onClick={async (e) => {
e.preventDefault();
e.stopPropagation();
if (conversationId) {
try {
const data = await OpenHands.getVSCodeUrl(conversationId);
if (data.vscode_url) {
const transformedUrl = transformVSCodeUrl(
data.vscode_url,
);
if (transformedUrl) {
window.open(transformedUrl, "_blank");
}
}
} catch (err) {
// Silently handle the error
}
}
}}
/>
) : null,
},
{
label: t(I18nKey.WORKSPACE$TERMINAL_TAB_LABEL),
to: "terminal",
icon: <TerminalIcon />,
},
{ label: "Jupyter", to: "jupyter", icon: <JupyterIcon /> },
{
label: <ServedAppLabel />,
to: "served",
icon: <FaServer />,
},
{
label: (
<div className="flex items-center gap-1">
{t(I18nKey.BROWSER$TITLE)}
</div>
),
to: "browser",
icon: <GlobeIcon />,
},
]}
>
{/* Use both Outlet and TabContent */}
<div className="h-full w-full">
<TabContent conversationPath={basePath} />
</div>
</Container>
);
}
@@ -1,6 +1,5 @@
import React, { useState, useEffect, useContext } from "react";
import { useTranslation } from "react-i18next";
import { FaStar } from "react-icons/fa";
import { cn } from "#/utils/utils";
import { I18nKey } from "#/i18n/declaration";
import { useSubmitConversationFeedback } from "#/hooks/mutation/use-submit-conversation-feedback";
@@ -208,7 +207,7 @@ export function LikertScale({
className={cn("text-xl transition-all", getButtonClass(rating))}
aria-label={`Rate ${rating} stars`}
>
<FaStar />
{t(I18nKey.FEEDBACK$STAR_RATING)}
</button>
))}
{/* Show selected reason inline with stars when submitted (only for ratings <= 3) */}
@@ -1,215 +1,19 @@
import React, { useState } from "react";
import { useTranslation, Trans } from "react-i18next";
import { FaTrash, FaEye, FaEyeSlash, FaCopy } from "react-icons/fa6";
import { FaTrash } from "react-icons/fa6";
import { I18nKey } from "#/i18n/declaration";
import { BrandButton } from "#/components/features/settings/brand-button";
import { LoadingSpinner } from "#/components/shared/loading-spinner";
import { ApiKey, CreateApiKeyResponse } from "#/api/api-keys";
import {
displayErrorToast,
displaySuccessToast,
} from "#/utils/custom-toast-handlers";
import { displayErrorToast } from "#/utils/custom-toast-handlers";
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";
interface LlmApiKeyManagerProps {
llmApiKey: { key: string | null } | undefined;
isLoadingLlmKey: boolean;
refreshLlmApiKey: ReturnType<typeof useRefreshLlmApiKey>;
}
function LlmApiKeyManager({
llmApiKey,
isLoadingLlmKey,
refreshLlmApiKey,
}: LlmApiKeyManagerProps) {
const { t } = useTranslation();
const [showLlmApiKey, setShowLlmApiKey] = useState(false);
const handleRefreshLlmApiKey = () => {
refreshLlmApiKey.mutate(undefined, {
onSuccess: () => {
displaySuccessToast(
t(I18nKey.SETTINGS$API_KEY_REFRESHED, {
defaultValue: "API key refreshed successfully",
}),
);
},
onError: () => {
displayErrorToast(t(I18nKey.ERROR$GENERIC));
},
});
};
if (isLoadingLlmKey || !llmApiKey) {
return null;
}
return (
<div className="border-b border-gray-200 pb-6 mb-6 flex flex-col gap-6">
<h3 className="text-xl font-medium text-white">
{t(I18nKey.SETTINGS$LLM_API_KEY)}
</h3>
<div className="flex items-center justify-between">
<BrandButton
type="button"
variant="primary"
onClick={handleRefreshLlmApiKey}
isDisabled={refreshLlmApiKey.isPending}
>
{refreshLlmApiKey.isPending ? (
<LoadingSpinner size="small" />
) : (
t(I18nKey.SETTINGS$REFRESH_LLM_API_KEY)
)}
</BrandButton>
</div>
<div>
<p className="text-sm text-gray-300 mb-2">
{t(I18nKey.SETTINGS$LLM_API_KEY_DESCRIPTION)}
</p>
<div className="flex items-center gap-2">
<div className="flex-1 bg-base-tertiary rounded-md py-2 flex items-center">
<div className="flex-1">
{llmApiKey.key ? (
<div className="flex items-center">
{showLlmApiKey ? (
<span className="text-white font-mono">
{llmApiKey.key}
</span>
) : (
<span className="text-white">{"•".repeat(20)}</span>
)}
</div>
) : (
<span className="text-white">
{t(I18nKey.API$NO_KEY_AVAILABLE)}
</span>
)}
</div>
<div className="flex items-center">
{llmApiKey.key && (
<button
type="button"
className="text-white hover:text-gray-300 mr-2"
aria-label={showLlmApiKey ? "Hide API key" : "Show API key"}
title={showLlmApiKey ? "Hide API key" : "Show API key"}
onClick={() => setShowLlmApiKey(!showLlmApiKey)}
>
{showLlmApiKey ? (
<FaEyeSlash size={20} />
) : (
<FaEye size={20} />
)}
</button>
)}
<button
type="button"
className="text-white hover:text-gray-300 mr-2"
aria-label="Copy API key"
title="Copy API key"
onClick={() => {
if (llmApiKey.key) {
navigator.clipboard.writeText(llmApiKey.key);
displaySuccessToast(t(I18nKey.SETTINGS$API_KEY_COPIED));
}
}}
>
<FaCopy size={20} />
</button>
</div>
</div>
</div>
</div>
</div>
);
}
interface ApiKeysTableProps {
apiKeys: ApiKey[];
isLoading: boolean;
onDeleteKey: (key: ApiKey) => void;
}
function ApiKeysTable({ apiKeys, isLoading, onDeleteKey }: ApiKeysTableProps) {
const { t } = useTranslation();
const formatDate = (dateString: string | null) => {
if (!dateString) return "Never";
return new Date(dateString).toLocaleString();
};
if (isLoading) {
return (
<div className="flex justify-center p-4">
<LoadingSpinner size="large" />
</div>
);
}
if (!Array.isArray(apiKeys) || apiKeys.length === 0) {
return null;
}
return (
<div className="border border-tertiary rounded-md overflow-hidden">
<table className="w-full">
<thead className="bg-base-tertiary">
<tr>
<th className="text-left p-3 text-sm font-medium">
{t(I18nKey.SETTINGS$NAME)}
</th>
<th className="text-left p-3 text-sm font-medium">
{t(I18nKey.SETTINGS$CREATED_AT)}
</th>
<th className="text-left p-3 text-sm font-medium">
{t(I18nKey.SETTINGS$LAST_USED)}
</th>
<th className="text-right p-3 text-sm font-medium">
{t(I18nKey.SETTINGS$ACTIONS)}
</th>
</tr>
</thead>
<tbody>
{apiKeys.map((key) => (
<tr key={key.id} className="border-t border-tertiary">
<td
className="p-3 text-sm truncate max-w-[160px]"
title={key.name}
>
{key.name}
</td>
<td className="p-3 text-sm">{formatDate(key.created_at)}</td>
<td className="p-3 text-sm">{formatDate(key.last_used_at)}</td>
<td className="p-3 text-right">
<button
type="button"
onClick={() => onDeleteKey(key)}
aria-label={`Delete ${key.name}`}
className="cursor-pointer"
>
<FaTrash size={16} />
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
export function ApiKeysManager() {
const { t } = useTranslation();
const { data: apiKeys = [], isLoading, error } = useApiKeys();
const { data: llmApiKey, isLoading: isLoadingLlmKey } = useLlmApiKey();
const refreshLlmApiKey = useRefreshLlmApiKey();
const [createModalOpen, setCreateModalOpen] = useState(false);
const [deleteModalOpen, setDeleteModalOpen] = useState(false);
const [keyToDelete, setKeyToDelete] = useState<ApiKey | null>(null);
@@ -242,24 +46,14 @@ export function ApiKeysManager() {
setNewlyCreatedKey(null);
};
const handleDeleteKey = (key: ApiKey) => {
setKeyToDelete(key);
setDeleteModalOpen(true);
const formatDate = (dateString: string | null) => {
if (!dateString) return "Never";
return new Date(dateString).toLocaleString();
};
return (
<>
<div className="flex flex-col gap-6">
<LlmApiKeyManager
llmApiKey={llmApiKey}
isLoadingLlmKey={isLoadingLlmKey}
refreshLlmApiKey={refreshLlmApiKey}
/>
<h3 className="text-xl font-medium text-white">
{t(I18nKey.SETTINGS$OPENHANDS_API_KEYS)}
</h3>
<div className="flex items-center justify-between">
<BrandButton
type="button"
@@ -288,11 +82,64 @@ export function ApiKeysManager() {
/>
</p>
<ApiKeysTable
apiKeys={apiKeys}
isLoading={isLoading}
onDeleteKey={handleDeleteKey}
/>
{isLoading && (
<div className="flex justify-center p-4">
<LoadingSpinner size="large" />
</div>
)}
{!isLoading && Array.isArray(apiKeys) && apiKeys.length > 0 && (
<div className="border border-tertiary rounded-md overflow-hidden">
<table className="w-full">
<thead className="bg-base-tertiary">
<tr>
<th className="text-left p-3 text-sm font-medium">
{t(I18nKey.SETTINGS$NAME)}
</th>
<th className="text-left p-3 text-sm font-medium">
{t(I18nKey.SETTINGS$CREATED_AT)}
</th>
<th className="text-left p-3 text-sm font-medium">
{t(I18nKey.SETTINGS$LAST_USED)}
</th>
<th className="text-right p-3 text-sm font-medium">
{t(I18nKey.SETTINGS$ACTIONS)}
</th>
</tr>
</thead>
<tbody>
{apiKeys.map((key) => (
<tr key={key.id} className="border-t border-tertiary">
<td
className="p-3 text-sm truncate max-w-[160px]"
title={key.name}
>
{key.name}
</td>
<td className="p-3 text-sm">
{formatDate(key.created_at)}
</td>
<td className="p-3 text-sm">
{formatDate(key.last_used_at)}
</td>
<td className="p-3 text-right">
<button
type="button"
onClick={() => {
setKeyToDelete(key);
setDeleteModalOpen(true);
}}
aria-label={`Delete ${key.name}`}
className="cursor-pointer"
>
<FaTrash size={16} />
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
{/* Create API Key Modal */}
@@ -3,16 +3,9 @@ interface HelpLinkProps {
text: string;
linkText: string;
href: string;
suffix?: string;
}
export function HelpLink({
testId,
text,
linkText,
href,
suffix,
}: HelpLinkProps) {
export function HelpLink({ testId, text, linkText, href }: HelpLinkProps) {
return (
<p data-testid={testId} className="text-xs">
{text}{" "}
@@ -24,7 +17,6 @@ export function HelpLink({
>
{linkText}
</a>
{suffix && ` ${suffix}`}
</p>
);
}
@@ -38,6 +38,7 @@ export function SettingsSwitch({
type="checkbox"
onChange={(e) => handleToggle(e.target.checked)}
checked={controlledIsToggled ?? isToggled}
defaultChecked={defaultIsToggled}
/>
<StyledSwitchComponent isToggled={controlledIsToggled ?? isToggled} />
@@ -91,31 +91,6 @@ export function AuthModal({ githubAuthUrl, appMode }: AuthModalProps) {
{t(I18nKey.BITBUCKET$CONNECT_TO_BITBUCKET)}
</BrandButton>
</div>
<p
className="mt-4 text-xs text-center text-muted-foreground"
data-testid="auth-modal-terms-of-service"
>
{t(I18nKey.AUTH$BY_SIGNING_UP_YOU_AGREE_TO_OUR)}{" "}
<a
href="https://www.all-hands.dev/tos"
target="_blank"
className="underline hover:text-primary"
rel="noopener noreferrer"
>
{t(I18nKey.COMMON$TERMS_OF_SERVICE)}
</a>{" "}
{t(I18nKey.COMMON$AND)}{" "}
<a
href="https://www.all-hands.dev/privacy"
target="_blank"
className="underline hover:text-primary"
rel="noopener noreferrer"
>
{t(I18nKey.COMMON$PRIVACY_POLICY)}
</a>
.
</p>
</ModalBody>
</ModalBackdrop>
);
+15 -87
View File
@@ -1,9 +1,6 @@
import clsx from "clsx";
import React, { useEffect, useRef, useState } from "react";
import React from "react";
import { NavTab } from "./nav-tab";
import { ScrollLeftButton } from "./scroll-left-button";
import { ScrollRightButton } from "./scroll-right-button";
import { useTrackElementWidth } from "#/hooks/use-track-element-width";
interface ContainerProps {
label?: React.ReactNode;
@@ -25,96 +22,27 @@ export function Container({
children,
className,
}: ContainerProps) {
const [containerWidth, setContainerWidth] = useState(0);
const [canScrollLeft, setCanScrollLeft] = useState(false);
const [canScrollRight, setCanScrollRight] = useState(false);
const containerRef = useRef<HTMLDivElement | null>(null);
const scrollContainerRef = useRef<HTMLDivElement>(null);
// Track container width using ResizeObserver
useTrackElementWidth({
elementRef: containerRef,
callback: setContainerWidth,
});
// Check scroll position and update button states
const updateScrollButtons = () => {
if (scrollContainerRef.current) {
const { scrollLeft, scrollWidth, clientWidth } =
scrollContainerRef.current;
setCanScrollLeft(scrollLeft > 0);
setCanScrollRight(scrollLeft < scrollWidth - clientWidth);
}
};
// Update scroll buttons when tabs change or container width changes
useEffect(() => {
updateScrollButtons();
}, [labels, containerWidth]);
// Scroll functions
const scrollLeft = () => {
if (scrollContainerRef.current) {
scrollContainerRef.current.scrollBy({ left: -200, behavior: "smooth" });
}
};
const scrollRight = () => {
if (scrollContainerRef.current) {
scrollContainerRef.current.scrollBy({ left: 200, behavior: "smooth" });
}
};
const showScrollButtons = containerWidth < 598 && labels && labels.length > 0;
return (
<div
ref={containerRef}
className={clsx(
"bg-base-secondary border border-neutral-600 rounded-xl flex flex-col h-full w-full",
"bg-base-secondary border border-neutral-600 rounded-xl flex flex-col h-full",
className,
)}
>
{labels && (
<div className="relative flex items-center h-[36px] w-full">
{/* Left scroll button */}
{showScrollButtons && (
<ScrollLeftButton
scrollLeft={scrollLeft}
canScrollLeft={canScrollLeft}
/>
)}
{/* Scrollable tabs container */}
<div
ref={scrollContainerRef}
className={clsx(
"flex text-xs overflow-x-auto scrollbar-hide w-full",
showScrollButtons && "mx-8",
)}
onScroll={updateScrollButtons}
>
{labels.map(
({ label: l, to, icon, isBeta, isLoading, rightContent }) => (
<NavTab
key={to}
to={to}
label={l}
icon={icon}
isBeta={isBeta}
isLoading={isLoading}
rightContent={rightContent}
/>
),
)}
</div>
{/* Right scroll button */}
{showScrollButtons && (
<ScrollRightButton
scrollRight={scrollRight}
canScrollRight={canScrollRight}
/>
<div className="flex text-xs h-[36px]">
{labels.map(
({ label: l, to, icon, isBeta, isLoading, rightContent }) => (
<NavTab
key={to}
to={to}
label={l}
icon={icon}
isBeta={isBeta}
isLoading={isLoading}
rightContent={rightContent}
/>
),
)}
</div>
)}
+3 -3
View File
@@ -33,12 +33,12 @@ export function NavTab({
>
{({ isActive }) => (
<div className="flex items-center justify-between w-full">
<div className="flex items-center gap-1 min-w-0">
<div className="flex items-center gap-2">
<div className={cn(isActive && "text-logo")}>{icon}</div>
<span className="truncate">{label}</span>
{label}
{isBeta && <BetaBadge />}
</div>
<div className="flex items-center gap-2 flex-shrink-0">
<div className="flex items-center gap-2">
{rightContent}
{isLoading && <LoadingSpinner size="small" />}
</div>
@@ -1,27 +0,0 @@
import clsx from "clsx";
import { ChevronLeft } from "../../assets/chevron-left";
interface ScrollLeftButtonProps {
scrollLeft: () => void;
canScrollLeft: boolean;
}
export function ScrollLeftButton({
scrollLeft,
canScrollLeft,
}: ScrollLeftButtonProps) {
return (
<button
type="button"
onClick={scrollLeft}
disabled={!canScrollLeft}
className={clsx(
"cursor-pointer absolute left-0 z-10 bg-base-secondary border-r border-neutral-600 h-full px-2 flex items-center justify-center",
"hover:bg-neutral-700 disabled:opacity-50 disabled:cursor-not-allowed",
"rounded-tl-xl",
)}
>
<ChevronLeft width={16} height={16} active={canScrollLeft} />
</button>
);
}
@@ -1,27 +0,0 @@
import clsx from "clsx";
import { ChevronRight } from "../../assets/chevron-right";
interface ScrollRightButtonProps {
scrollRight: () => void;
canScrollRight: boolean;
}
export function ScrollRightButton({
scrollRight,
canScrollRight,
}: ScrollRightButtonProps) {
return (
<button
type="button"
onClick={scrollRight}
disabled={!canScrollRight}
className={clsx(
"cursor-pointer absolute right-0 z-10 bg-base-secondary border-l border-neutral-600 h-full px-2 flex items-center justify-center",
"hover:bg-neutral-700 disabled:opacity-50 disabled:cursor-not-allowed",
"rounded-tr-xl",
)}
>
<ChevronRight width={16} height={16} active={canScrollRight} />
</button>
);
}
@@ -97,30 +97,26 @@ export function ModelSelector({
}}
>
<AutocompleteSection title={t(I18nKey.MODEL_SELECTOR$VERIFIED)}>
{VERIFIED_PROVIDERS.filter((provider) => models[provider]).map(
(provider) => (
{Object.keys(models)
.filter((provider) => VERIFIED_PROVIDERS.includes(provider))
.map((provider) => (
<AutocompleteItem
data-testid={`provider-item-${provider}`}
key={provider}
>
{mapProvider(provider)}
</AutocompleteItem>
),
)}
))}
</AutocompleteSection>
<AutocompleteSection title={t(I18nKey.MODEL_SELECTOR$OTHERS)}>
{Object.keys(models)
.filter((provider) => !VERIFIED_PROVIDERS.includes(provider))
.map((provider) => (
<AutocompleteItem key={provider}>
{mapProvider(provider)}
</AutocompleteItem>
))}
</AutocompleteSection>
{Object.keys(models).some(
(provider) => !VERIFIED_PROVIDERS.includes(provider),
) ? (
<AutocompleteSection title={t(I18nKey.MODEL_SELECTOR$OTHERS)}>
{Object.keys(models)
.filter((provider) => !VERIFIED_PROVIDERS.includes(provider))
.map((provider) => (
<AutocompleteItem key={provider}>
{mapProvider(provider)}
</AutocompleteItem>
))}
</AutocompleteSection>
) : null}
</Autocomplete>
</fieldset>
@@ -151,28 +147,24 @@ export function ModelSelector({
}}
>
<AutocompleteSection title={t(I18nKey.MODEL_SELECTOR$VERIFIED)}>
{VERIFIED_MODELS.filter((model) =>
models[selectedProvider || ""]?.models?.includes(model),
).map((model) => (
<AutocompleteItem key={model}>{model}</AutocompleteItem>
))}
{models[selectedProvider || ""]?.models
.filter((model) => VERIFIED_MODELS.includes(model))
.map((model) => (
<AutocompleteItem key={model}>{model}</AutocompleteItem>
))}
</AutocompleteSection>
<AutocompleteSection title={t(I18nKey.MODEL_SELECTOR$OTHERS)}>
{models[selectedProvider || ""]?.models
.filter((model) => !VERIFIED_MODELS.includes(model))
.map((model) => (
<AutocompleteItem
data-testid={`model-item-${model}`}
key={model}
>
{model}
</AutocompleteItem>
))}
</AutocompleteSection>
{models[selectedProvider || ""]?.models?.some(
(model) => !VERIFIED_MODELS.includes(model),
) ? (
<AutocompleteSection title={t(I18nKey.MODEL_SELECTOR$OTHERS)}>
{models[selectedProvider || ""]?.models
.filter((model) => !VERIFIED_MODELS.includes(model))
.map((model) => (
<AutocompleteItem
data-testid={`model-item-${model}`}
key={model}
>
{model}
</AutocompleteItem>
))}
</AutocompleteSection>
) : null}
</Autocomplete>
</fieldset>
</div>
@@ -1,51 +0,0 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import OpenHands from "#/api/open-hands";
export const useUpdateConversation = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (variables: { conversationId: string; newTitle: string }) =>
OpenHands.updateConversation(variables.conversationId, {
title: variables.newTitle,
}),
onMutate: async (variables) => {
await queryClient.cancelQueries({ queryKey: ["user", "conversations"] });
const previousConversations = queryClient.getQueryData([
"user",
"conversations",
]);
queryClient.setQueryData(
["user", "conversations"],
(old: { conversation_id: string; title: string }[] | undefined) =>
old?.map((conv) =>
conv.conversation_id === variables.conversationId
? { ...conv, title: variables.newTitle }
: conv,
),
);
return { previousConversations };
},
onError: (err, variables, context) => {
if (context?.previousConversations) {
queryClient.setQueryData(
["user", "conversations"],
context.previousConversations,
);
}
},
onSettled: (data, error, variables) => {
// Invalidate and refetch the conversation list to show the updated title
queryClient.invalidateQueries({
queryKey: ["user", "conversations"],
});
// Also invalidate the specific conversation query
queryClient.invalidateQueries({
queryKey: ["user", "conversation", variables.conversationId],
});
},
});
};
@@ -1,42 +0,0 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { openHands } from "#/api/open-hands-axios";
import { useConfig } from "./use-config";
export const LLM_API_KEY_QUERY_KEY = "llm-api-key";
export interface LlmApiKeyResponse {
key: string | null;
}
export function useLlmApiKey() {
const { data: config } = useConfig();
return useQuery({
queryKey: [LLM_API_KEY_QUERY_KEY],
enabled: config?.APP_MODE === "saas",
queryFn: async () => {
const { data } =
await openHands.get<LlmApiKeyResponse>("/api/keys/llm/byor");
return data;
},
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 15, // 15 minutes
});
}
export function useRefreshLlmApiKey() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async () => {
const { data } = await openHands.post<LlmApiKeyResponse>(
"/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] });
},
});
}
@@ -1,27 +0,0 @@
import { useEffect } from "react";
interface UseTrackElementWidthProps {
elementRef: React.RefObject<HTMLElement | null>;
callback: (width: number) => void;
}
export const useTrackElementWidth = ({
elementRef,
callback,
}: UseTrackElementWidthProps) => {
useEffect(() => {
const resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
callback(entry.contentRect.width);
}
});
if (elementRef.current) {
resizeObserver.observe(elementRef.current);
}
return () => {
resizeObserver.disconnect();
};
}, []);
};
+1 -14
View File
@@ -332,7 +332,6 @@ export enum I18nKey {
CONVERSATION$AGO = "CONVERSATION$AGO",
GITHUB$VSCODE_LINK_DESCRIPTION = "GITHUB$VSCODE_LINK_DESCRIPTION",
CONVERSATION$EXIT_WARNING = "CONVERSATION$EXIT_WARNING",
CONVERSATION$TITLE_UPDATED = "CONVERSATION$TITLE_UPDATED",
LANDING$OR = "LANDING$OR",
SUGGESTIONS$TEST_COVERAGE = "SUGGESTIONS$TEST_COVERAGE",
SUGGESTIONS$AUTO_MERGE = "SUGGESTIONS$AUTO_MERGE",
@@ -369,9 +368,6 @@ export enum I18nKey {
SETTINGS$LANGUAGE_TOOLTIP = "SETTINGS$LANGUAGE_TOOLTIP",
SETTINGS$DISABLED_RUNNING = "SETTINGS$DISABLED_RUNNING",
SETTINGS$API_KEY_PLACEHOLDER = "SETTINGS$API_KEY_PLACEHOLDER",
SETTINGS$LLM_API_KEY = "SETTINGS$LLM_API_KEY",
SETTINGS$LLM_API_KEY_DESCRIPTION = "SETTINGS$LLM_API_KEY_DESCRIPTION",
SETTINGS$REFRESH_LLM_API_KEY = "SETTINGS$REFRESH_LLM_API_KEY",
SETTINGS$CONFIRMATION_MODE = "SETTINGS$CONFIRMATION_MODE",
SETTINGS$CONFIRMATION_MODE_TOOLTIP = "SETTINGS$CONFIRMATION_MODE_TOOLTIP",
SETTINGS$AGENT_SELECT_ENABLED = "SETTINGS$AGENT_SELECT_ENABLED",
@@ -384,9 +380,6 @@ export enum I18nKey {
SETTINGS$RESET = "SETTINGS$RESET",
SETTINGS$API_KEYS = "SETTINGS$API_KEYS",
SETTINGS$API_KEYS_DESCRIPTION = "SETTINGS$API_KEYS_DESCRIPTION",
SETTINGS$OPENHANDS_API_KEY_HELP = "SETTINGS$OPENHANDS_API_KEY_HELP",
SETTINGS$OPENHANDS_API_KEY_HELP_TEXT = "SETTINGS$OPENHANDS_API_KEY_HELP_TEXT",
SETTINGS$OPENHANDS_API_KEY_HELP_SUFFIX = "SETTINGS$OPENHANDS_API_KEY_HELP_SUFFIX",
SETTINGS$CREATE_API_KEY = "SETTINGS$CREATE_API_KEY",
SETTINGS$CREATE_API_KEY_DESCRIPTION = "SETTINGS$CREATE_API_KEY_DESCRIPTION",
SETTINGS$DELETE_API_KEY = "SETTINGS$DELETE_API_KEY",
@@ -402,7 +395,6 @@ export enum I18nKey {
SETTINGS$API_KEY_DELETED = "SETTINGS$API_KEY_DELETED",
SETTINGS$API_KEY_WARNING = "SETTINGS$API_KEY_WARNING",
SETTINGS$API_KEY_COPIED = "SETTINGS$API_KEY_COPIED",
SETTINGS$API_KEY_REFRESHED = "SETTINGS$API_KEY_REFRESHED",
SETTINGS$API_KEY_NAME_PLACEHOLDER = "SETTINGS$API_KEY_NAME_PLACEHOLDER",
BUTTON$CREATE = "BUTTON$CREATE",
BUTTON$DELETE = "BUTTON$DELETE",
@@ -672,18 +664,13 @@ export enum I18nKey {
API$TAVILY_KEY_EXAMPLE = "API$TAVILY_KEY_EXAMPLE",
API$TVLY_KEY_EXAMPLE = "API$TVLY_KEY_EXAMPLE",
SECRETS$CONNECT_GIT_PROVIDER = "SECRETS$CONNECT_GIT_PROVIDER",
SETTINGS$OPENHANDS_API_KEYS = "SETTINGS$OPENHANDS_API_KEYS",
CONVERSATION$BUDGET_USAGE_FORMAT = "CONVERSATION$BUDGET_USAGE_FORMAT",
CONVERSATION$CACHE_HIT = "CONVERSATION$CACHE_HIT",
CONVERSATION$CACHE_WRITE = "CONVERSATION$CACHE_WRITE",
FEEDBACK$STAR_RATING = "FEEDBACK$STAR_RATING",
BUTTON$CONFIRM = "BUTTON$CONFIRM",
FORM$VALUE = "FORM$VALUE",
FORM$DESCRIPTION = "FORM$DESCRIPTION",
COMMON$OPTIONAL = "COMMON$OPTIONAL",
BROWSER$SERVER_MESSAGE = "BROWSER$SERVER_MESSAGE",
API$NO_KEY_AVAILABLE = "API$NO_KEY_AVAILABLE",
AUTH$BY_SIGNING_UP_YOU_AGREE_TO_OUR = "AUTH$BY_SIGNING_UP_YOU_AGREE_TO_OUR",
COMMON$TERMS_OF_SERVICE = "COMMON$TERMS_OF_SERVICE",
COMMON$AND = "COMMON$AND",
COMMON$PRIVACY_POLICY = "COMMON$PRIVACY_POLICY",
}
+79 -286
View File
@@ -15,6 +15,7 @@
"de": "Kein Repository gefunden, um Microagent zu starten",
"uk": "Не знайдено репозиторій для запуску мікроагента"
},
"MICROAGENT$ADD_TO_MICROAGENT": {
"en": "Add to Microagent",
"ja": "マイクロエージェントに追加",
@@ -4383,7 +4384,7 @@
"ja": "設定を更新しました",
"uk": "Налаштування оновлено"
},
"CHAT_INTERFACE$AUGMENTED_PROMPT_FILES_TITLE": {
"CHAT_INTERFACE$AUGMENTED_PROMPT_FILES_TITLE":{
"en": "NEW FILES ADDED",
"de": "NEUE DATEIEN HINZUGEFÜGT",
"zh-CN": "已添加新文件",
@@ -4397,8 +4398,8 @@
"fr": "NOUVEAUX FICHIERS AJOUTÉS",
"tr": "YENİ DOSYALAR EKLENDİ",
"ja": "新しいファイルが追加されました",
"uk": "ДОДАНО НОВІ ФАЙЛИ"
},
"uk": "ДОДАНО НОВІ ФАЙЛИ"
},
"CHAT_INTERFACE$DISCONNECTED": {
"en": "Disconnected",
"ja": "切断されました",
@@ -5311,22 +5312,6 @@
"de": "Gespräch verlassen",
"uk": "Вийти з розмови"
},
"CONVERSATION$TITLE_UPDATED": {
"en": "Conversation title updated successfully",
"ja": "会話のタイトルが正常に更新されました",
"zh-CN": "对话标题更新成功",
"zh-TW": "對話標題更新成功",
"ko-KR": "대화 제목이 성공적으로 업데이트되었습니다",
"de": "Gesprächstitel erfolgreich aktualisiert",
"no": "Samtaletittel oppdatert",
"it": "Titolo della conversazione aggiornato con successo",
"pt": "Título da conversa atualizado com sucesso",
"es": "Título de la conversación actualizado exitosamente",
"ar": "تم تحديث عنوان المحادثة بنجاح",
"fr": "Titre de la conversation mis à jour avec succès",
"tr": "Konuşma başlığı başarıyla güncellendi",
"uk": "Назву розмови успішно оновлено"
},
"LANDING$OR": {
"en": "Or",
"ja": "または",
@@ -5903,54 +5888,6 @@
"ja": "APIキーを入力",
"uk": "Введіть свій ключ API."
},
"SETTINGS$LLM_API_KEY": {
"en": "LLM API Key",
"zh-CN": "LLM API 密钥",
"zh-TW": "LLM API 金鑰",
"de": "LLM API Schlüssel",
"ko-KR": "LLM API 키",
"no": "LLM API-nøkkel",
"it": "Chiave API LLM",
"pt": "Chave API LLM",
"es": "Clave API LLM",
"ar": "مفتاح API للنماذج اللغوية الكبيرة",
"fr": "Clé API LLM",
"tr": "LLM API Anahtarı",
"ja": "LLM APIキー",
"uk": "Ключ API LLM"
},
"SETTINGS$LLM_API_KEY_DESCRIPTION": {
"en": "You can use this API Key as the LLM API Key for OpenHands open-source and CLI. It will incur cost on your OpenHands Cloud account. Do NOT share this key elsewhere.",
"zh-CN": "您可以将此 API 密钥用作 OpenHands 开源和 CLI 的 LLM API 密钥。它将在您的 OpenHands Cloud 账户上产生费用。请勿在其他地方共享此密钥。",
"zh-TW": "您可以將此 API 金鑰用作 OpenHands 開源和 CLI 的 LLM API 金鑰。它將在您的 OpenHands Cloud 帳戶上產生費用。請勿在其他地方共享此金鑰。",
"de": "Sie können diesen API-Schlüssel als LLM-API-Schlüssel für OpenHands Open-Source und CLI verwenden. Es fallen Kosten für Ihr OpenHands Cloud-Konto an. Teilen Sie diesen Schlüssel NICHT anderswo.",
"ko-KR": "이 API 키를 OpenHands 오픈소스 및 CLI용 LLM API 키로 사용할 수 있습니다. OpenHands Cloud 계정에 비용이 청구됩니다. 이 키를 다른 곳에서 공유하지 마세요.",
"no": "Du kan bruke denne API-nøkkelen som LLM API-nøkkel for OpenHands åpen kildekode og CLI. Det vil påløpe kostnader på din OpenHands Cloud-konto. IKKE del denne nøkkelen andre steder.",
"it": "Puoi utilizzare questa chiave API come chiave API LLM per OpenHands open-source e CLI. Comporterà costi sul tuo account OpenHands Cloud. NON condividere questa chiave altrove.",
"pt": "Você pode usar esta Chave API como a Chave API LLM para OpenHands de código aberto e CLI. Isso incorrerá em custos na sua conta OpenHands Cloud. NÃO compartilhe esta chave em outros lugares.",
"es": "Puede usar esta Clave API como la Clave API LLM para OpenHands de código abierto y CLI. Incurrirá en costos en su cuenta de OpenHands Cloud. NO comparta esta clave en ningún otro lugar.",
"ar": "يمكنك استخدام مفتاح API هذا كمفتاح API للنماذج اللغوية الكبيرة لـ OpenHands مفتوح المصدر وواجهة سطر الأوامر. سيتكبد تكلفة على حساب OpenHands Cloud الخاص بك. لا تشارك هذا المفتاح في أي مكان آخر.",
"fr": "Vous pouvez utiliser cette clé API comme clé API LLM pour OpenHands open-source et CLI. Cela entraînera des coûts sur votre compte OpenHands Cloud. NE partagez PAS cette clé ailleurs.",
"tr": "Bu API Anahtarını, OpenHands açık kaynak ve CLI için LLM API Anahtarı olarak kullanabilirsiniz. OpenHands Cloud hesabınızda maliyet oluşturacaktır. Bu anahtarı başka yerlerde paylaşmayın.",
"ja": "このAPIキーをOpenHandsオープンソースおよびCLIのLLM APIキーとして使用できます。OpenHands Cloudアカウントに費用が発生します。このキーを他の場所で共有しないでください。",
"uk": "Ви можете використовувати цей ключ API як ключ API LLM для OpenHands з відкритим кодом та CLI. Це призведе до витрат на вашому обліковому записі OpenHands Cloud. НЕ діліться цим ключем деінде."
},
"SETTINGS$REFRESH_LLM_API_KEY": {
"en": "Refresh API Key",
"zh-CN": "刷新 API 密钥",
"zh-TW": "刷新 API 金鑰",
"de": "API-Schlüssel aktualisieren",
"ko-KR": "API 키 새로고침",
"no": "Oppdater API-nøkkel",
"it": "Aggiorna chiave API",
"pt": "Atualizar chave API",
"es": "Actualizar clave API",
"ar": "تحديث مفتاح API",
"fr": "Actualiser la clé API",
"tr": "API Anahtarını Yenile",
"ja": "APIキーを更新",
"uk": "Оновити ключ API"
},
"SETTINGS$CONFIRMATION_MODE": {
"en": "Enable Confirmation Mode",
"de": "Bestätigungsmodus aktivieren",
@@ -6015,7 +5952,7 @@
"ja": "セキュリティアナライザー",
"uk": "Увімкнути аналізатор безпеки"
},
"SETTINGS$SECURITY_ANALYZER_PLACEHOLDER": {
"SETTINGS$SECURITY_ANALYZER_PLACEHOLDER":{
"en": "Select a security analyzer…",
"de": "Wählen Sie einen Sicherheitsanalysator aus…",
"zh-CN": "选择一个安全分析器…",
@@ -6143,54 +6080,6 @@
"de": "API-Schlüssel ermöglichen es Ihnen, sich programmatisch bei der OpenHands-API zu authentifizieren. Halten Sie Ihre API-Schlüssel sicher; jeder mit Ihrem API-Schlüssel kann auf Ihr Konto zugreifen. Weitere Informationen zur Verwendung der API finden Sie in unserer <a>API-Dokumentation</a>.",
"uk": "Ключі API дозволяють вам програмно автентифікуватися за допомогою API OpenHands. Зберігайте свої ключі API в безпеці; будь-хто, хто має ваш ключ API, може отримати доступ до вашого облікового запису. Для отримання додаткової інформації про використання API, перегляньте нашу <a>документацію API</a>."
},
"SETTINGS$OPENHANDS_API_KEY_HELP": {
"en": "You can find your OpenHands API Key in the <a>API Keys</a> tab of OpenHands Cloud.",
"ja": "OpenHands APIキーはOpenHands Cloudの<a>APIキー</a>タブで確認できます。",
"zh-CN": "您可以在OpenHands Cloud的<a>API密钥</a>标签页中找到您的OpenHands API密钥。",
"zh-TW": "您可以在OpenHands Cloud的<a>API密鑰</a>標籤頁中找到您的OpenHands API密鑰。",
"ko-KR": "OpenHands API 키는 OpenHands Cloud의 <a>API 키</a> 탭에서 찾을 수 있습니다.",
"no": "Du kan finne din OpenHands API-nøkkel i <a>API-nøkler</a>-fanen i OpenHands Cloud.",
"it": "Puoi trovare la tua chiave API OpenHands nella scheda <a>Chiavi API</a> di OpenHands Cloud.",
"pt": "Você pode encontrar sua chave de API OpenHands na guia <a>Chaves de API</a> do OpenHands Cloud.",
"es": "Puede encontrar su clave API de OpenHands en la pestaña <a>Claves API</a> de OpenHands Cloud.",
"ar": "يمكنك العثور على مفتاح API الخاص بـ OpenHands في علامة التبويب <a>مفاتيح API</a> في OpenHands Cloud.",
"fr": "Vous pouvez trouver votre clé API OpenHands dans l'onglet <a>Clés API</a> d'OpenHands Cloud.",
"tr": "OpenHands API Anahtarınızı OpenHands Cloud'un <a>API Anahtarları</a> sekmesinde bulabilirsiniz.",
"de": "Sie finden Ihren OpenHands API-Schlüssel im Tab <a>API-Schlüssel</a> von OpenHands Cloud.",
"uk": "Ви можете знайти свій ключ API OpenHands у вкладці <a>Ключі API</a> OpenHands Cloud."
},
"SETTINGS$OPENHANDS_API_KEY_HELP_TEXT": {
"en": "You can find your OpenHands API Key in the",
"ja": "OpenHands APIキーは",
"zh-CN": "您可以在",
"zh-TW": "您可以在",
"ko-KR": "OpenHands API 키는",
"no": "Du kan finne din OpenHands API-nøkkel i",
"it": "Puoi trovare la tua chiave API OpenHands nella",
"pt": "Você pode encontrar sua chave de API OpenHands na",
"es": "Puede encontrar su clave API de OpenHands en la",
"ar": "يمكنك العثور على مفتاح API الخاص بـ OpenHands في",
"fr": "Vous pouvez trouver votre clé API OpenHands dans",
"tr": "OpenHands API Anahtarınızı",
"de": "Sie finden Ihren OpenHands API-Schlüssel im",
"uk": "Ви можете знайти свій ключ API OpenHands у"
},
"SETTINGS$OPENHANDS_API_KEY_HELP_SUFFIX": {
"en": "tab of OpenHands Cloud.",
"ja": "タブで確認できます。",
"zh-CN": "标签页中找到您的OpenHands API密钥。",
"zh-TW": "標籤頁中找到您的OpenHands API密鑰。",
"ko-KR": "탭에서 찾을 수 있습니다.",
"no": "-fanen i OpenHands Cloud.",
"it": "scheda di OpenHands Cloud.",
"pt": "guia do OpenHands Cloud.",
"es": "pestaña de OpenHands Cloud.",
"ar": "علامة التبويب في OpenHands Cloud.",
"fr": "l'onglet d'OpenHands Cloud.",
"tr": "OpenHands Cloud'un sekmesinde bulabilirsiniz.",
"de": "Tab von OpenHands Cloud.",
"uk": "вкладці OpenHands Cloud."
},
"SETTINGS$CREATE_API_KEY": {
"en": "Create API Key",
"uk": "Створити API ключ",
@@ -6431,22 +6320,6 @@
"es": "Clave API copiada al portapapeles",
"tr": "API anahtarı panoya kopyalandı"
},
"SETTINGS$API_KEY_REFRESHED": {
"en": "API key refreshed successfully",
"uk": "Ключ API успішно оновлено",
"ja": "APIキーが正常に更新されました",
"zh-CN": "API密钥已成功刷新",
"zh-TW": "API金鑰已成功刷新",
"ko-KR": "API 키가 성공적으로 새로고침되었습니다",
"no": "API-nøkkel oppdatert",
"ar": "تم تحديث مفتاح API بنجاح",
"de": "API-Schlüssel erfolgreich aktualisiert",
"fr": "Clé API actualisée avec succès",
"it": "Chiave API aggiornata con successo",
"pt": "Chave API atualizada com sucesso",
"es": "Clave API actualizada con éxito",
"tr": "API anahtarı başarıyla yenilendi"
},
"SETTINGS$API_KEY_NAME_PLACEHOLDER": {
"en": "My API Key",
"uk": "Мій ключ API",
@@ -10751,22 +10624,6 @@
"de": "Git-Anbieter verbinden, um Geheimnisse zu verwalten",
"uk": "Підключити провайдера Git для управління секретами"
},
"SETTINGS$OPENHANDS_API_KEYS": {
"en": "OpenHands API Keys",
"ja": "OpenHands APIキー",
"zh-CN": "OpenHands API密钥",
"zh-TW": "OpenHands API密鑰",
"ko-KR": "OpenHands API 키",
"no": "OpenHands API-nøkler",
"it": "Chiavi API OpenHands",
"pt": "Chaves de API OpenHands",
"es": "Claves API de OpenHands",
"ar": "مفاتيح API لـ OpenHands",
"fr": "Clés API OpenHands",
"tr": "OpenHands API Anahtarları",
"de": "OpenHands API-Schlüssel",
"uk": "API-ключі OpenHands"
},
"CONVERSATION$BUDGET_USAGE_FORMAT": {
"en": "${currentCost} / ${maxBudget} ({usagePercentage}% {used})",
"ja": "${currentCost} / ${maxBudget} ({usagePercentage}% {used})",
@@ -10784,36 +10641,52 @@
"uk": "${currentCost} / ${maxBudget} ({usagePercentage}% {used})"
},
"CONVERSATION$CACHE_HIT": {
"en": "Cache Hit",
"ja": "キャッシュヒット",
"zh-CN": "缓存命中",
"zh-TW": "緩存命中",
"ko-KR": "캐시 히트",
"no": "Cache Treff",
"it": "Cache Hit",
"pt": "Cache Hit",
"es": "Cache Hit",
"ar": "إصابة الذاكرة المؤقتة",
"fr": "Cache Hit",
"tr": "Önbellek İsabeti",
"de": "Cache-Treffer",
"uk": "Кеш-хіт"
"en": "Cache Hit:",
"ja": "キャッシュヒット:",
"zh-CN": "缓存命中:",
"zh-TW": "快取命中:",
"ko-KR": "캐시 히트:",
"no": "Cache-treff:",
"it": "Cache Hit:",
"pt": "Cache Hit:",
"es": "Cache Hit:",
"ar": "إصابة التخزين المؤقت:",
"fr": "Cache Hit:",
"tr": "Önbellek İsabeti:",
"de": "Cache-Treffer:",
"uk": "Попадання в кеш:"
},
"CONVERSATION$CACHE_WRITE": {
"en": "Cache Write",
"ja": "キャッシュ書き込み",
"zh-CN": "缓存写入",
"zh-TW": "緩存寫入",
"ko-KR": "캐시 쓰기",
"no": "Cache Skriv",
"it": "Scrittura Cache",
"pt": "Escrita em Cache",
"es": "Escritura en Caché",
"ar": "كتابة الذاكرة المؤقتة",
"fr": "Écriture Cache",
"tr": "Önbellek Yazma",
"de": "Cache-Schreiben",
"uk": "Запис у кеш"
"en": "Cache Write:",
"ja": "キャッシュ書き込み:",
"zh-CN": "缓存写入:",
"zh-TW": "快取寫入:",
"ko-KR": "캐시 쓰기:",
"no": "Cache-skriving:",
"it": "Cache Write:",
"pt": "Cache Write:",
"es": "Cache Write:",
"ar": "كتابة التخزين المؤقت:",
"fr": "Cache Write:",
"tr": "Önbellek Yazma:",
"de": "Cache-Schreibung:",
"uk": "Запис в кеш:"
},
"FEEDBACK$STAR_RATING": {
"en": "★",
"ja": "★",
"zh-CN": "★",
"zh-TW": "★",
"ko-KR": "★",
"no": "★",
"it": "★",
"pt": "★",
"es": "★",
"ar": "★",
"fr": "★",
"tr": "★",
"de": "★",
"uk": "★"
},
"BUTTON$CONFIRM": {
"en": "Confirm",
@@ -10841,7 +10714,7 @@
"it": "Valore",
"pt": "Valor",
"es": "Valor",
"ar": "قيمة",
"ar": "القيمة",
"fr": "Valeur",
"tr": "Değer",
"de": "Wert",
@@ -10857,122 +10730,42 @@
"it": "Descrizione",
"pt": "Descrição",
"es": "Descripción",
"ar": "وصف",
"ar": "الوصف",
"fr": "Description",
"tr": "Açıklama",
"de": "Beschreibung",
"uk": "Опис"
},
"COMMON$OPTIONAL": {
"en": "Optional",
"ja": "任意",
"zh-CN": "可选",
"zh-TW": "可選",
"ko-KR": "선택 사항",
"no": "Valgfritt",
"it": "Opzionale",
"pt": "Opcional",
"es": "Opcional",
"ar": "اختياري",
"fr": "Optionnel",
"tr": "İsteğe Bağlı",
"de": "Optional",
"uk": "Необов'язково"
"en": "(Optional)",
"ja": "(オプション)",
"zh-CN": "(可选)",
"zh-TW": "(可選)",
"ko-KR": "(선택사항)",
"no": "(Valgfritt)",
"it": "(Opzionale)",
"pt": "(Opcional)",
"es": "(Opcional)",
"ar": "(اختياري)",
"fr": "(Optionnel)",
"tr": "(İsteğe bağlı)",
"de": "(Optional)",
"uk": "(Необов'язково)"
},
"BROWSER$SERVER_MESSAGE": {
"en": "Server Message",
"ja": "サーバーメッセージ",
"zh-CN": "服务器消息",
"zh-TW": "伺服器訊息",
"ko-KR": "서버 메시지",
"no": "Servermelding",
"it": "Messaggio del Server",
"pt": "Mensagem do Servidor",
"es": "Mensaje del Servidor",
"ar": "رسالة الخادم",
"fr": "Message du Serveur",
"tr": "Sunucu Mesajı",
"de": "Server-Nachricht",
"uk": "Повідомлення сервера"
},
"API$NO_KEY_AVAILABLE": {
"en": "No API key available",
"ja": "APIキーが利用できません",
"zh-CN": "没有可用的API密钥",
"zh-TW": "沒有可用的API密鑰",
"ko-KR": "사용 가능한 API 키 없음",
"no": "Ingen API-nøkkel tilgjengelig",
"it": "Nessuna chiave API disponibile",
"pt": "Nenhuma chave API disponível",
"es": "No hay clave API disponible",
"ar": "لا يوجد مفتاح API متاح",
"fr": "Aucune clé API disponible",
"tr": "Kullanılabilir API anahtarı yok",
"de": "Kein API-Schlüssel verfügbar",
"uk": "Немає доступного API-ключа"
},
"AUTH$BY_SIGNING_UP_YOU_AGREE_TO_OUR": {
"en": "By signing up, you agree to our",
"ja": "サインアップすることで、あなたは当社の",
"zh-CN": "注册即表示您同意我们的",
"zh-TW": "註冊即表示您同意我們的",
"ko-KR": "가입하면 당사의",
"no": "Ved å registrere deg godtar du våre",
"it": "Registrandoti, accetti i nostri",
"pt": "Ao se inscrever, você concorda com nossos",
"es": "Al registrarte, aceptas nuestros",
"ar": "من خلال التسجيل، فإنك توافق على",
"fr": "En vous inscrivant, vous acceptez nos",
"tr": "Kaydolarak, hizmet şartlarımızı kabul etmiş olursunuz",
"de": "Mit der Anmeldung stimmen Sie unseren",
"uk": "Реєструючись, ви погоджуєтеся з нашими"
},
"COMMON$TERMS_OF_SERVICE": {
"en": "Terms of Service",
"ja": "利用規約",
"zh-CN": "服务条款",
"zh-TW": "服務條款",
"ko-KR": "서비스 약관",
"no": "Vilkår for bruk",
"it": "Termini di servizio",
"pt": "Termos de Serviço",
"es": "Términos de servicio",
"ar": "شروط الخدمة",
"fr": "Conditions d'utilisation",
"tr": "Hizmet Şartları",
"de": "Nutzungsbedingungen",
"uk": "Умови надання послуг"
},
"COMMON$AND": {
"en": "and",
"ja": "と",
"zh-CN": "和",
"zh-TW": "和",
"ko-KR": "및",
"no": "og",
"it": "e",
"pt": "e",
"es": "y",
"ar": "و",
"fr": "et",
"tr": "ve",
"de": "und",
"uk": "та"
},
"COMMON$PRIVACY_POLICY": {
"en": "Privacy Policy",
"ja": "プライバシーポリシー",
"zh-CN": "隐私政策",
"zh-TW": "隱私政策",
"ko-KR": "개인정보 처리방침",
"no": "Personvernerklæring",
"it": "Informativa sulla privacy",
"pt": "Política de Privacidade",
"es": "Política de privacidad",
"ar": "سياسة الخصوصية",
"fr": "Politique de confidentialité",
"tr": "Gizlilik Politikası",
"de": "Datenschutzrichtlinie",
"uk": "Політика конфіденційності"
"en": "If you tell OpenHands to start a web server, the app will appear here.",
"ja": "OpenHandsにWebサーバーの起動を指示すると、アプリがここに表示されます。",
"zh-CN": "如果您告诉OpenHands启动Web服务器,应用程序将在此处显示。",
"zh-TW": "如果您告訴OpenHands啟動Web伺服器,應用程式將在此處顯示。",
"ko-KR": "OpenHands에게 웹 서버를 시작하라고 말하면 앱이 여기에 나타납니다.",
"no": "Hvis du ber OpenHands om å starte en webserver, vil appen vises her.",
"it": "Se dici a OpenHands di avviare un server web, l'app apparirà qui.",
"pt": "Se você disser ao OpenHands para iniciar um servidor web, o aplicativo aparecerá aqui.",
"es": "Si le dices a OpenHands que inicie un servidor web, la aplicación aparecerá aquí.",
"ar": "إذا أخبرت OpenHands ببدء خادم ويب، فستظهر التطبيق هنا.",
"fr": "Si vous demandez à OpenHands de démarrer un serveur web, l'application apparaîtra ici.",
"tr": "OpenHands'e bir web sunucusu başlatmasını söylerseniz, uygulama burada görünecektir.",
"de": "Wenn Sie OpenHands anweisen, einen Webserver zu starten, wird die App hier angezeigt.",
"uk": "Якщо ви скажете OpenHands запустити веб-сервер, додаток з'явиться тут."
}
}
-3
View File
@@ -45,9 +45,6 @@ export default function AcceptTOS() {
navigate(finalRedirectUrl);
}
},
onError: () => {
window.location.href = "/";
},
});
const handleAcceptTOS = () => {
+95 -13
View File
@@ -1,42 +1,55 @@
import { useDisclosure } from "@heroui/react";
import React from "react";
import { useNavigate } from "react-router";
import { useDispatch } from "react-redux";
import { useDispatch, useSelector } from "react-redux";
import { FaServer, FaExternalLinkAlt } from "react-icons/fa";
import { useTranslation } from "react-i18next";
import { DiGit } from "react-icons/di";
import { VscCode } from "react-icons/vsc";
import { I18nKey } from "#/i18n/declaration";
import { RUNTIME_INACTIVE_STATES } from "#/types/agent-state";
import { useConversationId } from "#/hooks/use-conversation-id";
import { Controls } from "#/components/features/controls/controls";
import { clearTerminal } from "#/state/command-slice";
import { useEffectOnce } from "#/hooks/use-effect-once";
import GlobeIcon from "#/icons/globe.svg?react";
import JupyterIcon from "#/icons/jupyter.svg?react";
import TerminalIcon from "#/icons/terminal.svg?react";
import { clearJupyter } from "#/state/jupyter-slice";
import { ChatInterface } from "../components/features/chat/chat-interface";
import { WsClientProvider } from "#/context/ws-client-provider";
import { EventHandler } from "../wrapper/event-handler";
import { useConversationConfig } from "#/hooks/query/use-conversation-config";
import { Container } from "#/components/layout/container";
import {
Orientation,
ResizablePanel,
} from "#/components/layout/resizable-panel";
import Security from "#/components/shared/modals/security/security";
import { useActiveConversation } from "#/hooks/query/use-active-conversation";
import { ServedAppLabel } from "#/components/layout/served-app-label";
import { useSettings } from "#/hooks/query/use-settings";
import { RootState } from "#/store";
import { displayErrorToast } from "#/utils/custom-toast-handlers";
import { useDocumentTitleFromState } from "#/hooks/use-document-title-from-state";
import { transformVSCodeUrl } from "#/utils/vscode-url-helper";
import OpenHands from "#/api/open-hands";
import { TabContent } from "#/components/layout/tab-content";
import { useIsAuthed } from "#/hooks/query/use-is-authed";
import { ConversationSubscriptionsProvider } from "#/context/conversation-subscriptions-provider";
import { useUserProviders } from "#/hooks/use-user-providers";
import { ConversationTabs } from "#/components/features/conversation/conversation-tabs";
function AppContent() {
useConversationConfig();
const { t } = useTranslation();
const { data: settings } = useSettings();
const { conversationId } = useConversationId();
const { data: conversation, isFetched, refetch } = useActiveConversation();
const { data: isAuthed } = useIsAuthed();
const { providers } = useUserProviders();
const { curAgentState } = useSelector((state: RootState) => state.agent);
const dispatch = useDispatch();
const navigate = useNavigate();
@@ -87,15 +100,12 @@ function AppContent() {
} = useDisclosure();
function renderMain() {
if (width <= 1024) {
const basePath = `/conversations/${conversationId}`;
if (width <= 640) {
return (
<div className="flex flex-col gap-3 overflow-auto w-full">
<div className="rounded-xl overflow-hidden border border-neutral-600 w-full bg-base-secondary min-h-[494px]">
<ChatInterface />
</div>
<div className="h-full w-full min-h-[494px]">
<ConversationTabs />
</div>
<div className="rounded-xl overflow-hidden border border-neutral-600 w-full bg-base-secondary">
<ChatInterface />
</div>
);
}
@@ -107,7 +117,79 @@ function AppContent() {
firstClassName="rounded-xl overflow-hidden border border-neutral-600 bg-base-secondary"
secondClassName="flex flex-col overflow-hidden"
firstChild={<ChatInterface />}
secondChild={<ConversationTabs />}
secondChild={
<Container
className="h-full w-full"
labels={[
{
label: "Changes",
to: "",
icon: <DiGit className="w-6 h-6" />,
},
{
label: (
<div className="flex items-center gap-1">
{t(I18nKey.VSCODE$TITLE)}
</div>
),
to: "vscode",
icon: <VscCode className="w-5 h-5" />,
rightContent: !RUNTIME_INACTIVE_STATES.includes(
curAgentState,
) ? (
<FaExternalLinkAlt
className="w-3 h-3 text-neutral-400 cursor-pointer"
onClick={async (e) => {
e.preventDefault();
e.stopPropagation();
if (conversationId) {
try {
const data =
await OpenHands.getVSCodeUrl(conversationId);
if (data.vscode_url) {
const transformedUrl = transformVSCodeUrl(
data.vscode_url,
);
if (transformedUrl) {
window.open(transformedUrl, "_blank");
}
}
} catch (err) {
// Silently handle the error
}
}
}}
/>
) : null,
},
{
label: t(I18nKey.WORKSPACE$TERMINAL_TAB_LABEL),
to: "terminal",
icon: <TerminalIcon />,
},
{ label: "Jupyter", to: "jupyter", icon: <JupyterIcon /> },
{
label: <ServedAppLabel />,
to: "served",
icon: <FaServer />,
},
{
label: (
<div className="flex items-center gap-1">
{t(I18nKey.BROWSER$TITLE)}
</div>
),
to: "browser",
icon: <GlobeIcon />,
},
]}
>
{/* Use both Outlet and TabContent */}
<div className="h-full w-full">
<TabContent conversationPath={basePath} />
</div>
</Container>
}
/>
);
}
+11 -48
View File
@@ -24,7 +24,6 @@ import { LlmSettingsInputsSkeleton } from "#/components/features/settings/llm-se
import { KeyStatusIcon } from "#/components/features/settings/key-status-icon";
import { DEFAULT_SETTINGS } from "#/services/settings";
import { getProviderId } from "#/utils/map-provider";
import { DEFAULT_OPENHANDS_MODEL } from "#/utils/verified-models";
function LlmSettingsScreen() {
const { t } = useTranslation();
@@ -50,11 +49,6 @@ function LlmSettingsScreen() {
securityAnalyzer: false,
});
// Track the currently selected model to show help text
const [currentSelectedModel, setCurrentSelectedModel] = React.useState<
string | null
>(null);
const modelsAndProviders = organizeModelsAndProviders(
resources?.models || [],
);
@@ -80,13 +74,6 @@ function LlmSettingsScreen() {
else setView("basic");
}, [settings, resources]);
// Initialize currentSelectedModel with the current settings
React.useEffect(() => {
if (settings?.LLM_MODEL) {
setCurrentSelectedModel(settings.LLM_MODEL);
}
}, [settings?.LLM_MODEL]);
const handleSuccessfulMutation = () => {
displaySuccessToast(t(I18nKey.SETTINGS$SAVED_WARNING));
setDirtyInputs({
@@ -197,9 +184,6 @@ function LlmSettingsScreen() {
...prev,
model: modelIsDirty,
}));
// Track the currently selected model for help text display
setCurrentSelectedModel(model);
};
const handleApiKeyIsDirty = (apiKey: string) => {
@@ -224,9 +208,6 @@ function LlmSettingsScreen() {
...prev,
model: modelIsDirty,
}));
// Track the currently selected model for help text display
setCurrentSelectedModel(model);
};
const handleBaseUrlIsDirty = (baseUrl: string) => {
@@ -298,23 +279,13 @@ function LlmSettingsScreen() {
className="flex flex-col gap-6"
>
{!isLoading && !isFetching && (
<>
<ModelSelector
models={modelsAndProviders}
currentModel={settings.LLM_MODEL || DEFAULT_OPENHANDS_MODEL}
onChange={handleModelIsDirty}
/>
{(settings.LLM_MODEL?.startsWith("openhands/") ||
currentSelectedModel?.startsWith("openhands/")) && (
<HelpLink
testId="openhands-api-key-help"
text={t(I18nKey.SETTINGS$OPENHANDS_API_KEY_HELP_TEXT)}
linkText={t(I18nKey.SETTINGS$NAV_API_KEYS)}
href="https://app.all-hands.dev/settings/api-keys"
suffix={t(I18nKey.SETTINGS$OPENHANDS_API_KEY_HELP_SUFFIX)}
/>
)}
</>
<ModelSelector
models={modelsAndProviders}
currentModel={
settings.LLM_MODEL || "anthropic/claude-sonnet-4-20250514"
}
onChange={handleModelIsDirty}
/>
)}
<SettingsInput
@@ -373,22 +344,14 @@ function LlmSettingsScreen() {
testId="llm-custom-model-input"
name="llm-custom-model-input"
label={t(I18nKey.SETTINGS$CUSTOM_MODEL)}
defaultValue={settings.LLM_MODEL || DEFAULT_OPENHANDS_MODEL}
placeholder={DEFAULT_OPENHANDS_MODEL}
defaultValue={
settings.LLM_MODEL || "anthropic/claude-sonnet-4-20250514"
}
placeholder="anthropic/claude-sonnet-4-20250514"
type="text"
className="w-full max-w-[680px]"
onChange={handleCustomModelIsDirty}
/>
{(settings.LLM_MODEL?.startsWith("openhands/") ||
currentSelectedModel?.startsWith("openhands/")) && (
<HelpLink
testId="openhands-api-key-help-2"
text={t(I18nKey.SETTINGS$OPENHANDS_API_KEY_HELP_TEXT)}
linkText={t(I18nKey.SETTINGS$NAV_API_KEYS)}
href="https://app.all-hands.dev/settings/api-keys"
suffix={t(I18nKey.SETTINGS$OPENHANDS_API_KEY_HELP_SUFFIX)}
/>
)}
<SettingsInput
testId="base-url-input"
@@ -1,9 +1,7 @@
import { isNumber } from "./is-number";
import {
VERIFIED_ANTHROPIC_MODELS,
VERIFIED_MISTRAL_MODELS,
VERIFIED_OPENAI_MODELS,
VERIFIED_OPENHANDS_MODELS,
} from "./verified-models";
/**
@@ -49,12 +47,6 @@ export const extractModelAndProvider = (model: string) => {
if (VERIFIED_ANTHROPIC_MODELS.includes(split[0])) {
return { provider: "anthropic", model: split[0], separator: "/" };
}
if (VERIFIED_MISTRAL_MODELS.includes(split[0])) {
return { provider: "mistral", model: split[0], separator: "/" };
}
if (VERIFIED_OPENHANDS_MODELS.includes(split[0])) {
return { provider: "openhands", model: split[0], separator: "/" };
}
// return as model only
return { provider: "", model, separator: "" };
}
-1
View File
@@ -23,7 +23,6 @@ export const MAP_PROVIDER = {
replicate: "Replicate",
voyage: "Voyage AI",
openrouter: "OpenRouter",
openhands: "OpenHands",
};
export const mapProvider = (provider: string) =>
+8 -34
View File
@@ -1,10 +1,5 @@
// Here are the list of verified models and providers that we know work well with OpenHands.
export const VERIFIED_PROVIDERS = [
"openhands",
"anthropic",
"openai",
"mistral",
];
export const VERIFIED_PROVIDERS = ["openai", "azure", "anthropic", "deepseek"];
export const VERIFIED_MODELS = [
"o3-mini-2025-01-31",
"o3-2025-04-16",
@@ -13,12 +8,7 @@ export const VERIFIED_MODELS = [
"claude-3-7-sonnet-20250219",
"claude-sonnet-4-20250514",
"claude-opus-4-20250514",
"gemini-2.5-pro",
"o4-mini",
"deepseek-chat",
"devstral-small-2505",
"devstral-small-2507",
"devstral-medium-2507",
];
// LiteLLM does not return OpenAI models with the provider, so we list them here to set them ourselves for consistency
@@ -26,8 +16,10 @@ export const VERIFIED_MODELS = [
export const VERIFIED_OPENAI_MODELS = [
"gpt-4o",
"gpt-4o-mini",
"gpt-4-32k",
"gpt-4.1",
"gpt-4.1-2025-04-14",
"o1-mini",
"o3",
"o3-2025-04-16",
"o4-mini",
@@ -38,33 +30,15 @@ export const VERIFIED_OPENAI_MODELS = [
// LiteLLM does not return the compatible Anthropic models with the provider, so we list them here to set them ourselves
// (e.g., they return `claude-3-5-sonnet-20241022` instead of `anthropic/claude-3-5-sonnet-20241022`)
export const VERIFIED_ANTHROPIC_MODELS = [
"claude-2",
"claude-2.1",
"claude-3-5-sonnet-20240620",
"claude-3-5-sonnet-20241022",
"claude-3-5-haiku-20241022",
"claude-3-haiku-20240307",
"claude-3-opus-20240229",
"claude-3-sonnet-20240229",
"claude-3-7-sonnet-20250219",
"claude-sonnet-4-20250514",
"claude-opus-4-20250514",
];
// LiteLLM does not return the compatible Mistral models with the provider, so we list them here to set them ourselves
// (e.g., they return `devstral-small-2505` instead of `mistral/devstral-small-2505`)
export const VERIFIED_MISTRAL_MODELS = [
"devstral-small-2507",
"devstral-medium-2507",
"devstral-small-2505",
];
// LiteLLM does not return the compatible OpenHands models with the provider, so we list them here to set them ourselves
// (e.g., they return `claude-sonnet-4-20250514` instead of `openhands/claude-sonnet-4-20250514`)
export const VERIFIED_OPENHANDS_MODELS = [
"claude-sonnet-4-20250514",
"claude-opus-4-20250514",
"gemini-2.5-pro",
"o4-mini",
"devstral-small-2507",
"devstral-medium-2507",
"devstral-small-2505",
];
// Default model for OpenHands provider
export const DEFAULT_OPENHANDS_MODEL = "openhands/claude-sonnet-4-20250514";
-54
View File
@@ -1,54 +0,0 @@
# Publishing Process
1. **Version Check**: The workflow first checks if the version in `package.json` has changed compared to the previous commit
2. **Build**: If version changed, it sets up Bun, installs dependencies, and builds the package
3. **Duplicate Check**: Verifies the version doesn't already exist on npm
4. **Publish**: Publishes the package to npm using the `NPM_TOKEN` secret
# Publishing a New Version
1. **Update the version** in `openhands-ui/package.json`:
```bash
cd openhands-ui
# For patch release (1.0.0 → 1.0.1)
npm version patch
# For minor release (1.0.0 → 1.1.0)
npm version minor
# For major release (1.0.0 → 2.0.0)
npm version major
# For pre-release (1.0.0 → 1.0.1-beta.0)
npm version prerelease --preid=beta
```
2. **Commit and push** the version change:
```bash
git add package.json
git commit -m "chore(ui): bump version to X.X.X"
```
3. **Create a PR** with your changes and the version bump
4. **Merge the PR** - the package will be automatically published
## Manual Publishing (Fallback)
If the automated workflow fails, you can manually publish:
```bash
cd openhands-ui
bun install
bun run build
npm publish
```
## Version Strategy
- **Patch** (X.X.1): Bug fixes, small improvements
- **Minor** (X.1.X): New features, non-breaking changes
- **Major** (1.X.X): Breaking changes
- **Pre-release** (X.X.X-beta.X): Beta versions for testing
+5 -87
View File
@@ -1,97 +1,15 @@
# @openhands/ui
# openhands-ui
A modern React component library built with TypeScript and Tailwind CSS.
## Installation
Choose your preferred package manager:
To install dependencies:
```bash
# npm
npm install @openhands/ui
# yarn
yarn add @openhands/ui
# pnpm
pnpm add @openhands/ui
# bun
bun add @openhands/ui
```
## Quick Start
```tsx
import { Button, Typography } from "@openhands/ui";
import "@openhands/ui/styles";
function App() {
return (
<div>
<Typography.H1>Hello World</Typography.H1>
<Button variant="primary">Get Started</Button>
</div>
);
}
```
## Components
| Component | Description |
| ----------------- | ----------------------------------------- |
| `Button` | Interactive button with multiple variants |
| `Checkbox` | Checkbox input with label support |
| `Chip` | Display tags or labels |
| `Divider` | Visual separator |
| `Icon` | Icon wrapper component |
| `Input` | Text input field |
| `InteractiveChip` | Clickable chip component |
| `RadioGroup` | Radio button group |
| `RadioOption` | Individual radio option |
| `Scrollable` | Scrollable container |
| `Toggle` | Toggle switch |
| `Tooltip` | Tooltip overlay |
| `Typography` | Text components (H1-H6, Text, Code) |
## Development
Use your preferred package manager to install dependencies and run the development server. We recommend using [Bun](https://bun.sh) for a fast development experience.
**Note**: If you plan to make dependency changes and submit a PR, you must use Bun during development.
```bash
# Install dependencies
bun install
# Start Storybook
bun run dev
# Build package
bun run build
```
### Testing Locally Without Publishing
To test the package in another project without publishing to npm:
To run storybook:
```bash
# Build the package:
bun run build
# Create a local package:
# This generates a `.tgz` file in the current directory.
bun pm pack
# Install in your target project:
# Replace `path/to/openhands-ui-x.x.x.tgz` with the actual path to the generated `.tgz` file.
npm install path/to/openhands-ui-x.x.x.tgz
bun run --bun sb
```
## Publishing
This package is automatically published to npm **when a version bump is merged to the main branch**. See [PUBLISHING.md](./PUBLISHING.md) for detailed information about the publishing process.
## License
MIT
This project was created using `bun init` in bun v1.2.16. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.
+75 -204
View File
@@ -7,7 +7,9 @@
"@floating-ui/react": "^0.27.12",
"clsx": "^2.1.1",
"focus-trap-react": "^11.0.4",
"react": "^19.1.0",
"react-bootstrap-icons": "^1.11.6",
"react-dom": "^19.1.0",
"tailwind-merge": "^3.3.1",
"tailwind-scrollbar": "^4.0.2",
"tailwindcss": "^4.1.10",
@@ -26,13 +28,10 @@
"@vitest/coverage-v8": "^3.2.4",
"playwright": "^1.53.1",
"storybook": "^9.0.12",
"typescript": "^5.8.3",
"vite-plugin-dts": "^4.5.4",
"vitest": "^3.2.4",
},
"peerDependencies": {
"react": ">=18.0.0",
"react-dom": ">=18.0.0",
"typescript": "^5.8.3",
},
},
},
@@ -85,57 +84,55 @@
"@chromatic-com/storybook": ["@chromatic-com/storybook@4.0.1", "", { "dependencies": { "@neoconfetti/react": "^1.0.0", "chromatic": "^12.0.0", "filesize": "^10.0.12", "jsonfile": "^6.1.0", "strip-ansi": "^7.1.0" }, "peerDependencies": { "storybook": "^0.0.0-0 || ^9.0.0 || ^9.1.0-0" } }, "sha512-GQXe5lyZl3yLewLJQyFXEpOp2h+mfN2bPrzYaOFNCJjO4Js9deKbRHTOSaiP2FRwZqDLdQwy2+SEGeXPZ94yYw=="],
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.6", "", { "os": "aix", "cpu": "ppc64" }, "sha512-ShbM/3XxwuxjFiuVBHA+d3j5dyac0aEVVq1oluIDf71hUw0aRF59dV/efUsIwFnR6m8JNM2FjZOzmaZ8yG61kw=="],
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA=="],
"@esbuild/android-arm": ["@esbuild/android-arm@0.25.6", "", { "os": "android", "cpu": "arm" }, "sha512-S8ToEOVfg++AU/bHwdksHNnyLyVM+eMVAOf6yRKFitnwnbwwPNqKr3srzFRe7nzV69RQKb5DgchIX5pt3L53xg=="],
"@esbuild/android-arm": ["@esbuild/android-arm@0.25.5", "", { "os": "android", "cpu": "arm" }, "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA=="],
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.6", "", { "os": "android", "cpu": "arm64" }, "sha512-hd5zdUarsK6strW+3Wxi5qWws+rJhCCbMiC9QZyzoxfk5uHRIE8T287giQxzVpEvCwuJ9Qjg6bEjcRJcgfLqoA=="],
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.5", "", { "os": "android", "cpu": "arm64" }, "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg=="],
"@esbuild/android-x64": ["@esbuild/android-x64@0.25.6", "", { "os": "android", "cpu": "x64" }, "sha512-0Z7KpHSr3VBIO9A/1wcT3NTy7EB4oNC4upJ5ye3R7taCc2GUdeynSLArnon5G8scPwaU866d3H4BCrE5xLW25A=="],
"@esbuild/android-x64": ["@esbuild/android-x64@0.25.5", "", { "os": "android", "cpu": "x64" }, "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw=="],
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-FFCssz3XBavjxcFxKsGy2DYK5VSvJqa6y5HXljKzhRZ87LvEi13brPrf/wdyl/BbpbMKJNOr1Sd0jtW4Ge1pAA=="],
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ=="],
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-GfXs5kry/TkGM2vKqK2oyiLFygJRqKVhawu3+DOCk7OxLy/6jYkWXhlHwOoTb0WqGnWGAS7sooxbZowy+pK9Yg=="],
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ=="],
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.6", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-aoLF2c3OvDn2XDTRvn8hN6DRzVVpDlj2B/F66clWd/FHLiHaG3aVZjxQX2DYphA5y/evbdGvC6Us13tvyt4pWg=="],
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw=="],
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.6", "", { "os": "freebsd", "cpu": "x64" }, "sha512-2SkqTjTSo2dYi/jzFbU9Plt1vk0+nNg8YC8rOXXea+iA3hfNJWebKYPs3xnOUf9+ZWhKAaxnQNUf2X9LOpeiMQ=="],
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw=="],
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.6", "", { "os": "linux", "cpu": "arm" }, "sha512-SZHQlzvqv4Du5PrKE2faN0qlbsaW/3QQfUUc6yO2EjFcA83xnwm91UbEEVx4ApZ9Z5oG8Bxz4qPE+HFwtVcfyw=="],
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.5", "", { "os": "linux", "cpu": "arm" }, "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw=="],
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-b967hU0gqKd9Drsh/UuAm21Khpoh6mPBSgz8mKRq4P5mVK8bpA+hQzmm/ZwGVULSNBzKdZPQBRT3+WuVavcWsQ=="],
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg=="],
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.6", "", { "os": "linux", "cpu": "ia32" }, "sha512-aHWdQ2AAltRkLPOsKdi3xv0mZ8fUGPdlKEjIEhxCPm5yKEThcUjHpWB1idN74lfXGnZ5SULQSgtr5Qos5B0bPw=="],
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA=="],
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.6", "", { "os": "linux", "cpu": "none" }, "sha512-VgKCsHdXRSQ7E1+QXGdRPlQ/e08bN6WMQb27/TMfV+vPjjTImuT9PmLXupRlC90S1JeNNW5lzkAEO/McKeJ2yg=="],
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.5", "", { "os": "linux", "cpu": "none" }, "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg=="],
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.6", "", { "os": "linux", "cpu": "none" }, "sha512-WViNlpivRKT9/py3kCmkHnn44GkGXVdXfdc4drNmRl15zVQ2+D2uFwdlGh6IuK5AAnGTo2qPB1Djppj+t78rzw=="],
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.5", "", { "os": "linux", "cpu": "none" }, "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg=="],
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.6", "", { "os": "linux", "cpu": "ppc64" }, "sha512-wyYKZ9NTdmAMb5730I38lBqVu6cKl4ZfYXIs31Baf8aoOtB4xSGi3THmDYt4BTFHk7/EcVixkOV2uZfwU3Q2Jw=="],
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ=="],
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.6", "", { "os": "linux", "cpu": "none" }, "sha512-KZh7bAGGcrinEj4qzilJ4hqTY3Dg2U82c8bv+e1xqNqZCrCyc+TL9AUEn5WGKDzm3CfC5RODE/qc96OcbIe33w=="],
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.5", "", { "os": "linux", "cpu": "none" }, "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA=="],
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.6", "", { "os": "linux", "cpu": "s390x" }, "sha512-9N1LsTwAuE9oj6lHMyyAM+ucxGiVnEqUdp4v7IaMmrwb06ZTEVCIs3oPPplVsnjPfyjmxwHxHMF8b6vzUVAUGw=="],
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ=="],
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.6", "", { "os": "linux", "cpu": "x64" }, "sha512-A6bJB41b4lKFWRKNrWoP2LHsjVzNiaurf7wyj/XtFNTsnPuxwEBWHLty+ZE0dWBKuSK1fvKgrKaNjBS7qbFKig=="],
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.5", "", { "os": "linux", "cpu": "x64" }, "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw=="],
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.6", "", { "os": "none", "cpu": "arm64" }, "sha512-IjA+DcwoVpjEvyxZddDqBY+uJ2Snc6duLpjmkXm/v4xuS3H+3FkLZlDm9ZsAbF9rsfP3zeA0/ArNDORZgrxR/Q=="],
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.5", "", { "os": "none", "cpu": "arm64" }, "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw=="],
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.6", "", { "os": "none", "cpu": "x64" }, "sha512-dUXuZr5WenIDlMHdMkvDc1FAu4xdWixTCRgP7RQLBOkkGgwuuzaGSYcOpW4jFxzpzL1ejb8yF620UxAqnBrR9g=="],
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.5", "", { "os": "none", "cpu": "x64" }, "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ=="],
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.6", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-l8ZCvXP0tbTJ3iaqdNf3pjaOSd5ex/e6/omLIQCVBLmHTlfXW3zAxQ4fnDmPLOB1x9xrcSi/xtCWFwCZRIaEwg=="],
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.5", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw=="],
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.6", "", { "os": "openbsd", "cpu": "x64" }, "sha512-hKrmDa0aOFOr71KQ/19JC7az1P0GWtCN1t2ahYAf4O007DHZt/dW8ym5+CUdJhQ/qkZmI1HAF8KkJbEFtCL7gw=="],
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg=="],
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.6", "", { "os": "none", "cpu": "arm64" }, "sha512-+SqBcAWoB1fYKmpWoQP4pGtx+pUUC//RNYhFdbcSA16617cchuryuhOCRpPsjCblKukAckWsV+aQ3UKT/RMPcA=="],
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA=="],
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.6", "", { "os": "sunos", "cpu": "x64" }, "sha512-dyCGxv1/Br7MiSC42qinGL8KkG4kX0pEsdb0+TKhmJZgCUDBGmyo1/ArCjNGiOLiIAgdbWgmWgib4HoCi5t7kA=="],
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw=="],
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-42QOgcZeZOvXfsCBJF5Afw73t4veOId//XD3i+/9gSkhSV6Gk3VPlWncctI+JcOyERv85FUo7RxuxGy+z8A43Q=="],
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ=="],
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.6", "", { "os": "win32", "cpu": "ia32" }, "sha512-4AWhgXmDuYN7rJI6ORB+uU9DHLq/erBbuMoAuB4VWJTu5KtCgcKYPynF0YI1VkBNuEfjNlLrFr9KZPJzrtLkrQ=="],
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.6", "", { "os": "win32", "cpu": "x64" }, "sha512-NgJPHHbEpLQgDH2MjQu90pzW/5vvXIZ7KOnPyNBm92A6WgZ/7b6fJyUBjoumLqeOQQGqY2QjQxRo97ah4Sj0cA=="],
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.5", "", { "os": "win32", "cpu": "x64" }, "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g=="],
"@floating-ui/core": ["@floating-ui/core@1.7.2", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-wNB5ooIKHQc+Kui96jE/n69rHFWAVoxn5CAzL1Xdd8FG03cgY3MLO+GF9U3W737fYDSgPWA6MReKhBQBop6Pcw=="],
@@ -165,14 +162,6 @@
"@mdx-js/react": ["@mdx-js/react@3.1.0", "", { "dependencies": { "@types/mdx": "^2.0.0" }, "peerDependencies": { "@types/react": ">=16", "react": ">=16" } }, "sha512-QjHtSaoameoalGnKDT3FoIl4+9RwyTmo9ZJGBdLOks/YOiWHoRDI3PUwEzOE7kEmGcV3AFcp9K6dYu9rEuKLAQ=="],
"@microsoft/api-extractor": ["@microsoft/api-extractor@7.52.8", "", { "dependencies": { "@microsoft/api-extractor-model": "7.30.6", "@microsoft/tsdoc": "~0.15.1", "@microsoft/tsdoc-config": "~0.17.1", "@rushstack/node-core-library": "5.13.1", "@rushstack/rig-package": "0.5.3", "@rushstack/terminal": "0.15.3", "@rushstack/ts-command-line": "5.0.1", "lodash": "~4.17.15", "minimatch": "~3.0.3", "resolve": "~1.22.1", "semver": "~7.5.4", "source-map": "~0.6.1", "typescript": "5.8.2" }, "bin": { "api-extractor": "bin/api-extractor" } }, "sha512-cszYIcjiNscDoMB1CIKZ3My61+JOhpERGlGr54i6bocvGLrcL/wo9o+RNXMBrb7XgLtKaizZWUpqRduQuHQLdg=="],
"@microsoft/api-extractor-model": ["@microsoft/api-extractor-model@7.30.6", "", { "dependencies": { "@microsoft/tsdoc": "~0.15.1", "@microsoft/tsdoc-config": "~0.17.1", "@rushstack/node-core-library": "5.13.1" } }, "sha512-znmFn69wf/AIrwHya3fxX6uB5etSIn6vg4Q4RB/tb5VDDs1rqREc+AvMC/p19MUN13CZ7+V/8pkYPTj7q8tftg=="],
"@microsoft/tsdoc": ["@microsoft/tsdoc@0.15.1", "", {}, "sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw=="],
"@microsoft/tsdoc-config": ["@microsoft/tsdoc-config@0.17.1", "", { "dependencies": { "@microsoft/tsdoc": "0.15.1", "ajv": "~8.12.0", "jju": "~1.4.0", "resolve": "~1.22.2" } }, "sha512-UtjIFe0C6oYgTnad4q1QP4qXwLhe6tIpNTRStJ2RZEPIkqQPREAwE5spzVxsdn9UaEMUqhh0AqSx3X4nWAKXWw=="],
"@neoconfetti/react": ["@neoconfetti/react@1.0.0", "", {}, "sha512-klcSooChXXOzIm+SE5IISIAn3bYzYfPjbX7D7HoqZL84oAfgREeSg5vSIaSFH+DaGzzvImTyWe1OyrJ67vik4A=="],
"@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
@@ -183,75 +172,67 @@
"@rollup/pluginutils": ["@rollup/pluginutils@5.2.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-qWJ2ZTbmumwiLFomfzTyt5Kng4hwPi9rwCYN4SHb6eaRU1KNO4ccxINHr/VhH4GgPlt1XfSTLX2LBTme8ne4Zw=="],
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.44.2", "", { "os": "android", "cpu": "arm" }, "sha512-g0dF8P1e2QYPOj1gu7s/3LVP6kze9A7m6x0BZ9iTdXK8N5c2V7cpBKHV3/9A4Zd8xxavdhK0t4PnqjkqVmUc9Q=="],
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.44.1", "", { "os": "android", "cpu": "arm" }, "sha512-JAcBr1+fgqx20m7Fwe1DxPUl/hPkee6jA6Pl7n1v2EFiktAHenTaXl5aIFjUIEsfn9w3HE4gK1lEgNGMzBDs1w=="],
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.44.2", "", { "os": "android", "cpu": "arm64" }, "sha512-Yt5MKrOosSbSaAK5Y4J+vSiID57sOvpBNBR6K7xAaQvk3MkcNVV0f9fE20T+41WYN8hDn6SGFlFrKudtx4EoxA=="],
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.44.1", "", { "os": "android", "cpu": "arm64" }, "sha512-RurZetXqTu4p+G0ChbnkwBuAtwAbIwJkycw1n6GvlGlBuS4u5qlr5opix8cBAYFJgaY05TWtM+LaoFggUmbZEQ=="],
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.44.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-EsnFot9ZieM35YNA26nhbLTJBHD0jTwWpPwmRVDzjylQT6gkar+zenfb8mHxWpRrbn+WytRRjE0WKsfaxBkVUA=="],
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.44.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-fM/xPesi7g2M7chk37LOnmnSTHLG/v2ggWqKj3CCA1rMA4mm5KVBT1fNoswbo1JhPuNNZrVwpTvlCVggv8A2zg=="],
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.44.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-dv/t1t1RkCvJdWWxQ2lWOO+b7cMsVw5YFaS04oHpZRWehI1h0fV1gF4wgGCTyQHHjJDfbNpwOi6PXEafRBBezw=="],
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.44.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-gDnWk57urJrkrHQ2WVx9TSVTH7lSlU7E3AFqiko+bgjlh78aJ88/3nycMax52VIVjIm3ObXnDL2H00e/xzoipw=="],
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.44.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-W4tt4BLorKND4qeHElxDoim0+BsprFTwb+vriVQnFFtT/P6v/xO5I99xvYnVzKWrK6j7Hb0yp3x7V5LUbaeOMg=="],
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.44.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-wnFQmJ/zPThM5zEGcnDcCJeYJgtSLjh1d//WuHzhf6zT3Md1BvvhJnWoy+HECKu2bMxaIcfWiu3bJgx6z4g2XA=="],
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.44.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tdT1PHopokkuBVyHjvYehnIe20fxibxFCEhQP/96MDSOcyjM/shlTkZZLOufV3qO6/FQOSiJTBebhVc12JyPTA=="],
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.44.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-uBmIxoJ4493YATvU2c0upGz87f99e3wop7TJgOA/bXMFd2SvKCI7xkxY/5k50bv7J6dw1SXT4MQBQSLn8Bb/Uw=="],
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.44.2", "", { "os": "linux", "cpu": "arm" }, "sha512-+xmiDGGaSfIIOXMzkhJ++Oa0Gwvl9oXUeIiwarsdRXSe27HUIvjbSIpPxvnNsRebsNdUo7uAiQVgBD1hVriwSQ=="],
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.44.1", "", { "os": "linux", "cpu": "arm" }, "sha512-n0edDmSHlXFhrlmTK7XBuwKlG5MbS7yleS1cQ9nn4kIeW+dJH+ExqNgQ0RrFRew8Y+0V/x6C5IjsHrJmiHtkxQ=="],
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.44.2", "", { "os": "linux", "cpu": "arm" }, "sha512-bDHvhzOfORk3wt8yxIra8N4k/N0MnKInCW5OGZaeDYa/hMrdPaJzo7CSkjKZqX4JFUWjUGm88lI6QJLCM7lDrA=="],
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.44.1", "", { "os": "linux", "cpu": "arm" }, "sha512-8WVUPy3FtAsKSpyk21kV52HCxB+me6YkbkFHATzC2Yd3yuqHwy2lbFL4alJOLXKljoRw08Zk8/xEj89cLQ/4Nw=="],
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.44.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-NMsDEsDiYghTbeZWEGnNi4F0hSbGnsuOG+VnNvxkKg0IGDvFh7UVpM/14mnMwxRxUf9AdAVJgHPvKXf6FpMB7A=="],
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.44.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-yuktAOaeOgorWDeFJggjuCkMGeITfqvPgkIXhDqsfKX8J3jGyxdDZgBV/2kj/2DyPaLiX6bPdjJDTu9RB8lUPQ=="],
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.44.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-lb5bxXnxXglVq+7imxykIp5xMq+idehfl+wOgiiix0191av84OqbjUED+PRC5OA8eFJYj5xAGcpAZ0pF2MnW+A=="],
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.44.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-W+GBM4ifET1Plw8pdVaecwUgxmiH23CfAUj32u8knq0JPFyK4weRy6H7ooxYFD19YxBulL0Ktsflg5XS7+7u9g=="],
"@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.44.2", "", { "os": "linux", "cpu": "none" }, "sha512-Yl5Rdpf9pIc4GW1PmkUGHdMtbx0fBLE1//SxDmuf3X0dUC57+zMepow2LK0V21661cjXdTn8hO2tXDdAWAqE5g=="],
"@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.44.1", "", { "os": "linux", "cpu": "none" }, "sha512-1zqnUEMWp9WrGVuVak6jWTl4fEtrVKfZY7CvcBmUUpxAJ7WcSowPSAWIKa/0o5mBL/Ij50SIf9tuirGx63Ovew=="],
"@rollup/rollup-linux-powerpc64le-gnu": ["@rollup/rollup-linux-powerpc64le-gnu@4.44.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-03vUDH+w55s680YYryyr78jsO1RWU9ocRMaeV2vMniJJW/6HhoTBwyyiiTPVHNWLnhsnwcQ0oH3S9JSBEKuyqw=="],
"@rollup/rollup-linux-powerpc64le-gnu": ["@rollup/rollup-linux-powerpc64le-gnu@4.44.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Rl3JKaRu0LHIx7ExBAAnf0JcOQetQffaw34T8vLlg9b1IhzcBgaIdnvEbbsZq9uZp3uAH+JkHd20Nwn0h9zPjA=="],
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.44.2", "", { "os": "linux", "cpu": "none" }, "sha512-iYtAqBg5eEMG4dEfVlkqo05xMOk6y/JXIToRca2bAWuqjrJYJlx/I7+Z+4hSrsWU8GdJDFPL4ktV3dy4yBSrzg=="],
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.44.1", "", { "os": "linux", "cpu": "none" }, "sha512-j5akelU3snyL6K3N/iX7otLBIl347fGwmd95U5gS/7z6T4ftK288jKq3A5lcFKcx7wwzb5rgNvAg3ZbV4BqUSw=="],
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.44.2", "", { "os": "linux", "cpu": "none" }, "sha512-e6vEbgaaqz2yEHqtkPXa28fFuBGmUJ0N2dOJK8YUfijejInt9gfCSA7YDdJ4nYlv67JfP3+PSWFX4IVw/xRIPg=="],
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.44.1", "", { "os": "linux", "cpu": "none" }, "sha512-ppn5llVGgrZw7yxbIm8TTvtj1EoPgYUAbfw0uDjIOzzoqlZlZrLJ/KuiE7uf5EpTpCTrNt1EdtzF0naMm0wGYg=="],
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.44.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-evFOtkmVdY3udE+0QKrV5wBx7bKI0iHz5yEVx5WqDJkxp9YQefy4Mpx3RajIVcM6o7jxTvVd/qpC1IXUhGc1Mw=="],
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.44.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-Hu6hEdix0oxtUma99jSP7xbvjkUM/ycke/AQQ4EC5g7jNRLLIwjcNwaUy95ZKBJJwg1ZowsclNnjYqzN4zwkAw=="],
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.44.2", "", { "os": "linux", "cpu": "x64" }, "sha512-/bXb0bEsWMyEkIsUL2Yt5nFB5naLAwyOWMEviQfQY1x3l5WsLKgvZf66TM7UTfED6erckUVUJQ/jJ1FSpm3pRQ=="],
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.44.1", "", { "os": "linux", "cpu": "x64" }, "sha512-EtnsrmZGomz9WxK1bR5079zee3+7a+AdFlghyd6VbAjgRJDbTANJ9dcPIPAi76uG05micpEL+gPGmAKYTschQw=="],
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.44.2", "", { "os": "linux", "cpu": "x64" }, "sha512-3D3OB1vSSBXmkGEZR27uiMRNiwN08/RVAcBKwhUYPaiZ8bcvdeEwWPvbnXvvXHY+A/7xluzcN+kaiOFNiOZwWg=="],
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.44.1", "", { "os": "linux", "cpu": "x64" }, "sha512-iAS4p+J1az6Usn0f8xhgL4PaU878KEtutP4hqw52I4IO6AGoyOkHCxcc4bqufv1tQLdDWFx8lR9YlwxKuv3/3g=="],
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.44.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-VfU0fsMK+rwdK8mwODqYeM2hDrF2WiHaSmCBrS7gColkQft95/8tphyzv2EupVxn3iE0FI78wzffoULH1G+dkw=="],
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.44.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-NtSJVKcXwcqozOl+FwI41OH3OApDyLk3kqTJgx8+gp6On9ZEt5mYhIsKNPGuaZr3p9T6NWPKGU/03Vw4CNU9qg=="],
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.44.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-+qMUrkbUurpE6DVRjiJCNGZBGo9xM4Y0FXU5cjgudWqIBWbcLkjE3XprJUsOFgC6xjBClwVa9k6O3A7K3vxb5Q=="],
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.44.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-JYA3qvCOLXSsnTR3oiyGws1Dm0YTuxAAeaYGVlGpUsHqloPcFjPg+X0Fj2qODGLNwQOAcCiQmHub/V007kiH5A=="],
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.44.2", "", { "os": "win32", "cpu": "x64" }, "sha512-3+QZROYfJ25PDcxFF66UEk8jGWigHJeecZILvkPkyQN7oc5BvFo4YEXFkOs154j3FTMp9mn9Ky8RCOwastduEA=="],
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.44.1", "", { "os": "win32", "cpu": "x64" }, "sha512-J8o22LuF0kTe7m+8PvW9wk3/bRq5+mRo5Dqo6+vXb7otCm3TPhYOJqOaQtGU9YMWQSL3krMnoOxMr0+9E6F3Ug=="],
"@rushstack/node-core-library": ["@rushstack/node-core-library@5.13.1", "", { "dependencies": { "ajv": "~8.13.0", "ajv-draft-04": "~1.0.0", "ajv-formats": "~3.0.1", "fs-extra": "~11.3.0", "import-lazy": "~4.0.0", "jju": "~1.4.0", "resolve": "~1.22.1", "semver": "~7.5.4" }, "peerDependencies": { "@types/node": "*" }, "optionalPeers": ["@types/node"] }, "sha512-5yXhzPFGEkVc9Fu92wsNJ9jlvdwz4RNb2bMso+/+TH0nMm1jDDDsOIf4l8GAkPxGuwPw5DH24RliWVfSPhlW/Q=="],
"@storybook/addon-a11y": ["@storybook/addon-a11y@9.0.15", "", { "dependencies": { "@storybook/global": "^5.0.0", "axe-core": "^4.2.0" }, "peerDependencies": { "storybook": "^9.0.15" } }, "sha512-/oborGUeN7KT6jyTMhGRET9tXvZ080OCB/Hw6txSfsVxgZ4Z1QTJcOreejHGeYyxHN1ugEJ26K95agk4M13WZg=="],
"@rushstack/rig-package": ["@rushstack/rig-package@0.5.3", "", { "dependencies": { "resolve": "~1.22.1", "strip-json-comments": "~3.1.1" } }, "sha512-olzSSjYrvCNxUFZowevC3uz8gvKr3WTpHQ7BkpjtRpA3wK+T0ybep/SRUMfr195gBzJm5gaXw0ZMgjIyHqJUow=="],
"@storybook/addon-docs": ["@storybook/addon-docs@9.0.15", "", { "dependencies": { "@mdx-js/react": "^3.0.0", "@storybook/csf-plugin": "9.0.15", "@storybook/icons": "^1.2.12", "@storybook/react-dom-shim": "9.0.15", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "ts-dedent": "^2.0.0" }, "peerDependencies": { "storybook": "^9.0.15" } }, "sha512-HOb45DkF23T1tRzakb9q33qnBRso15S/GM28ippPZWi5ZXR9RAyKVgOSMA/ViEpK4ezASxN+Tee+H7m4ksEFZw=="],
"@rushstack/terminal": ["@rushstack/terminal@0.15.3", "", { "dependencies": { "@rushstack/node-core-library": "5.13.1", "supports-color": "~8.1.1" }, "peerDependencies": { "@types/node": "*" }, "optionalPeers": ["@types/node"] }, "sha512-DGJ0B2Vm69468kZCJkPj3AH5nN+nR9SPmC0rFHtzsS4lBQ7/dgOwtwVxYP7W9JPDMuRBkJ4KHmWKr036eJsj9g=="],
"@storybook/addon-onboarding": ["@storybook/addon-onboarding@9.0.15", "", { "peerDependencies": { "storybook": "^9.0.15" } }, "sha512-g2FqO0aS6vvjMZdY+0xjV1C7YGcDE0GkuPAv1JqejNYGyX2Z8nuLHy2zqhLIBpfoap4S9PZO+obqEKVeo70Q0Q=="],
"@rushstack/ts-command-line": ["@rushstack/ts-command-line@5.0.1", "", { "dependencies": { "@rushstack/terminal": "0.15.3", "@types/argparse": "1.0.38", "argparse": "~1.0.9", "string-argv": "~0.3.1" } }, "sha512-bsbUucn41UXrQK7wgM8CNM/jagBytEyJqXw/umtI8d68vFm1Jwxh1OtLrlW7uGZgjCWiiPH6ooUNa1aVsuVr3Q=="],
"@storybook/addon-vitest": ["@storybook/addon-vitest@9.0.15", "", { "dependencies": { "@storybook/global": "^5.0.0", "@storybook/icons": "^1.4.0", "prompts": "^2.4.0", "ts-dedent": "^2.2.0" }, "peerDependencies": { "@vitest/browser": "^3.0.0", "@vitest/runner": "^3.0.0", "storybook": "^9.0.15", "vitest": "^3.0.0" }, "optionalPeers": ["@vitest/browser", "@vitest/runner", "vitest"] }, "sha512-4TynzdZgJMsvneT5lZGp+WrUoFtp8+LRL3y35EepJa3GMBc+9WgsKQrso+xnDQh1gLvVNe46n3klZvunVr4AFA=="],
"@storybook/addon-a11y": ["@storybook/addon-a11y@9.0.16", "", { "dependencies": { "@storybook/global": "^5.0.0", "axe-core": "^4.2.0" }, "peerDependencies": { "storybook": "^9.0.16" } }, "sha512-pi9ipxhs9bA2yCHDGp2+yWy6E2LywDFTqWcFh3aw/LRxnlRTf52QiVJkWpJbNFEXgk4QrKVrAruf9LLiXpTcOA=="],
"@storybook/builder-vite": ["@storybook/builder-vite@9.0.15", "", { "dependencies": { "@storybook/csf-plugin": "9.0.15", "ts-dedent": "^2.0.0" }, "peerDependencies": { "storybook": "^9.0.15", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-ogPec1V+e3MgTY5DBlq/6hBBui0Y4TmolYQh0eL3cATHrwZlwkTTDWQfsOnMALd5w+4Jq8n0gk0cQgR5rh1FHw=="],
"@storybook/addon-docs": ["@storybook/addon-docs@9.0.16", "", { "dependencies": { "@mdx-js/react": "^3.0.0", "@storybook/csf-plugin": "9.0.16", "@storybook/icons": "^1.2.12", "@storybook/react-dom-shim": "9.0.16", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "ts-dedent": "^2.0.0" }, "peerDependencies": { "storybook": "^9.0.16" } }, "sha512-/ZXaxMC/JqL0cnVuyPHXdJhNvgCrKvxcnM3ACdgBLsEIGcIqegPF+Ahkb2f9sjU36sR7ihT81cL/7cUvQwzd4Q=="],
"@storybook/addon-onboarding": ["@storybook/addon-onboarding@9.0.16", "", { "peerDependencies": { "storybook": "^9.0.16" } }, "sha512-69BPJ9fGNGpDAcGvNJ58V5uQOmpHkQMLcgp/ON3NepoCHiSReUzojB6wV8Ag13PUZmvWXVnE14SWKBZp93xTFQ=="],
"@storybook/addon-vitest": ["@storybook/addon-vitest@9.0.16", "", { "dependencies": { "@storybook/global": "^5.0.0", "@storybook/icons": "^1.4.0", "prompts": "^2.4.0", "ts-dedent": "^2.2.0" }, "peerDependencies": { "@vitest/browser": "^3.0.0", "@vitest/runner": "^3.0.0", "storybook": "^9.0.16", "vitest": "^3.0.0" }, "optionalPeers": ["@vitest/browser", "@vitest/runner", "vitest"] }, "sha512-amIJLeAcREF/imEmAhZmsPc3kvtEDVdk7O6uvh8/ql8UYNN5Tnc+ud6CsfIZO82ru+PupoYKrt6SC8EwpQ8YMQ=="],
"@storybook/builder-vite": ["@storybook/builder-vite@9.0.16", "", { "dependencies": { "@storybook/csf-plugin": "9.0.16", "ts-dedent": "^2.0.0" }, "peerDependencies": { "storybook": "^9.0.16", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-zXockUexeRy3ABG7DFLEvJqJe4mGL0JkI7FMrpwiKaHCQNaD87vR0xkRVeh0a3B8GKUNxoYtpYKvdzc9DobQHQ=="],
"@storybook/csf-plugin": ["@storybook/csf-plugin@9.0.16", "", { "dependencies": { "unplugin": "^1.3.1" }, "peerDependencies": { "storybook": "^9.0.16" } }, "sha512-MSmfPwI0j1mMAc+R3DVkVBQf2KLzaVn2SLdEwweesx63Nh9j3zu9CqKEa0zOuDX1lR2M0DZU0lV6K4sc2EYI4A=="],
"@storybook/csf-plugin": ["@storybook/csf-plugin@9.0.15", "", { "dependencies": { "unplugin": "^1.3.1" }, "peerDependencies": { "storybook": "^9.0.15" } }, "sha512-KszyGjrocMiNbkmpBGARF1ugLYMVaw1J8Z31kmwTHsMgMZwAKcOsofJ0fPgFno0yV59DUVkWxVBdPs9V0hhvxA=="],
"@storybook/global": ["@storybook/global@5.0.0", "", {}, "sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ=="],
"@storybook/icons": ["@storybook/icons@1.4.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta" } }, "sha512-Td73IeJxOyalzvjQL+JXx72jlIYHgs+REaHiREOqfpo3A2AYYG71AUbcv+lg7mEDIweKVCxsMQ0UKo634c8XeA=="],
"@storybook/react": ["@storybook/react@9.0.16", "", { "dependencies": { "@storybook/global": "^5.0.0", "@storybook/react-dom-shim": "9.0.16" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", "storybook": "^9.0.16", "typescript": ">= 4.9.x" }, "optionalPeers": ["typescript"] }, "sha512-1jk9fBe8vEoZrba9cK19ZDdZgYMXUNl3Egjj5RsTMYMc1L2mtIu9o56VyK/1V4Q52N9IyawHvmIIuxc5pCZHkQ=="],
"@storybook/react": ["@storybook/react@9.0.15", "", { "dependencies": { "@storybook/global": "^5.0.0", "@storybook/react-dom-shim": "9.0.15" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", "storybook": "^9.0.15", "typescript": ">= 4.9.x" }, "optionalPeers": ["typescript"] }, "sha512-hewpSH8Ij4Bg7S9Tfw7ecfGPv7YDycRxsfpsDX7Mw3JhLuCdqjpmmTL2RgoNojg7TAW3FPdixcgQi/b4PH50ag=="],
"@storybook/react-dom-shim": ["@storybook/react-dom-shim@9.0.16", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", "storybook": "^9.0.16" } }, "sha512-5aIK+31R41mRUvDB4vmBv8hwh3IVHIk/Zbs6kkWF2a+swOsB2+a06aLX21lma4/0T/AuFVXHWat0+inQ4nrXRg=="],
"@storybook/react-dom-shim": ["@storybook/react-dom-shim@9.0.15", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", "storybook": "^9.0.15" } }, "sha512-X5VlYKoZSIMU9HEshIwtNzp41nPt4kiJtJ2c5HzFa5F6M8rEHM5n059CGcCZQqff3FnZtK/y6v/kCVZO+8oETA=="],
"@storybook/react-vite": ["@storybook/react-vite@9.0.16", "", { "dependencies": { "@joshwooding/vite-plugin-react-docgen-typescript": "0.6.1", "@rollup/pluginutils": "^5.0.2", "@storybook/builder-vite": "9.0.16", "@storybook/react": "9.0.16", "find-up": "^7.0.0", "magic-string": "^0.30.0", "react-docgen": "^8.0.0", "resolve": "^1.22.8", "tsconfig-paths": "^4.2.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", "storybook": "^9.0.16", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-a+UsoymyvPH4bJJVI+asj02N8U2wlkGyzhUqF6LUM9gXzixRMxoRHkchCKLdqLhE+//STrwC0YFF3GG6Y5oMEg=="],
"@storybook/react-vite": ["@storybook/react-vite@9.0.15", "", { "dependencies": { "@joshwooding/vite-plugin-react-docgen-typescript": "0.6.1", "@rollup/pluginutils": "^5.0.2", "@storybook/builder-vite": "9.0.15", "@storybook/react": "9.0.15", "find-up": "^7.0.0", "magic-string": "^0.30.0", "react-docgen": "^8.0.0", "resolve": "^1.22.8", "tsconfig-paths": "^4.2.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", "storybook": "^9.0.15", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-OOAywn5x2Ged3LD84+TMwpjZUelFg7Wb8eHkgHE2SzM20XiZrhoKvreqxlzbfey3weBl+bKNhsiWF9BluT8YHg=="],
"@tailwindcss/node": ["@tailwindcss/node@4.1.11", "", { "dependencies": { "@ampproject/remapping": "^2.3.0", "enhanced-resolve": "^5.18.1", "jiti": "^2.4.2", "lightningcss": "1.30.1", "magic-string": "^0.30.17", "source-map-js": "^1.2.1", "tailwindcss": "4.1.11" } }, "sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q=="],
@@ -289,8 +270,6 @@
"@testing-library/user-event": ["@testing-library/user-event@14.6.1", "", { "peerDependencies": { "@testing-library/dom": ">=7.21.4" } }, "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw=="],
"@types/argparse": ["@types/argparse@1.0.38", "", {}, "sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA=="],
"@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="],
"@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
@@ -301,7 +280,7 @@
"@types/babel__traverse": ["@types/babel__traverse@7.20.7", "", { "dependencies": { "@babel/types": "^7.20.7" } }, "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng=="],
"@types/bun": ["@types/bun@1.2.18", "", { "dependencies": { "bun-types": "1.2.18" } }, "sha512-Xf6RaWVheyemaThV0kUfaAUvCNokFr+bH8Jxp+tTZfx7dAPA8z9ePnP9S9+Vspzuxxx9JRAXhnyccRj3GyCMdQ=="],
"@types/bun": ["@types/bun@1.2.17", "", { "dependencies": { "bun-types": "1.2.17" } }, "sha512-l/BYs/JYt+cXA/0+wUhulYJB6a6p//GTPiJ7nV+QHa8iiId4HZmnu/3J/SowP5g0rTiERY2kfGKXEK5Ehltx4Q=="],
"@types/chai": ["@types/chai@5.2.2", "", { "dependencies": { "@types/deep-eql": "*" } }, "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg=="],
@@ -313,7 +292,7 @@
"@types/mdx": ["@types/mdx@2.0.13", "", {}, "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw=="],
"@types/node": ["@types/node@24.0.12", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-LtOrbvDf5ndC9Xi+4QZjVL0woFymF/xSTKZKPgrrl7H7XoeDvnD+E2IclKVDyaK9UM756W/3BXqSU+JEHopA9g=="],
"@types/node": ["@types/node@24.0.10", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA=="],
"@types/prismjs": ["@types/prismjs@1.26.5", "", {}, "sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ=="],
@@ -343,38 +322,12 @@
"@vitest/utils": ["@vitest/utils@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="],
"@volar/language-core": ["@volar/language-core@2.4.18", "", { "dependencies": { "@volar/source-map": "2.4.18" } }, "sha512-G3yYV85ekH4TV0EDS6DsS/dUJWrz675H9UgsxFz5pQbmas51a0Q2fF6Lb2q4RKgytuLZ4E0MBdT5PlVsJXNalw=="],
"@volar/source-map": ["@volar/source-map@2.4.18", "", {}, "sha512-zaj2V/zo/CHQ/xA75h60jBPgrz+Ou9s6aPl7dX0rT46/uill9aB/ZaDk92ROpJsa/9e2xftCeNAU9ZwVyB/egQ=="],
"@volar/typescript": ["@volar/typescript@2.4.18", "", { "dependencies": { "@volar/language-core": "2.4.18", "path-browserify": "^1.0.1", "vscode-uri": "^3.0.8" } }, "sha512-xcbsMG8m/yhvO1VIKnTtc+llZxw3YtWkZiV7/F1qNpTORdPExkZRcBxJ5d19MXLpkeiQ+DG5JURHh1SV0bcWRA=="],
"@vue/compiler-core": ["@vue/compiler-core@3.5.17", "", { "dependencies": { "@babel/parser": "^7.27.5", "@vue/shared": "3.5.17", "entities": "^4.5.0", "estree-walker": "^2.0.2", "source-map-js": "^1.2.1" } }, "sha512-Xe+AittLbAyV0pabcN7cP7/BenRBNcteM4aSDCtRvGw0d9OL+HG1u/XHLY/kt1q4fyMeZYXyIYrsHuPSiDPosA=="],
"@vue/compiler-dom": ["@vue/compiler-dom@3.5.17", "", { "dependencies": { "@vue/compiler-core": "3.5.17", "@vue/shared": "3.5.17" } }, "sha512-+2UgfLKoaNLhgfhV5Ihnk6wB4ljyW1/7wUIog2puUqajiC29Lp5R/IKDdkebh9jTbTogTbsgB+OY9cEWzG95JQ=="],
"@vue/compiler-vue2": ["@vue/compiler-vue2@2.7.16", "", { "dependencies": { "de-indent": "^1.0.2", "he": "^1.2.0" } }, "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A=="],
"@vue/language-core": ["@vue/language-core@2.2.0", "", { "dependencies": { "@volar/language-core": "~2.4.11", "@vue/compiler-dom": "^3.5.0", "@vue/compiler-vue2": "^2.7.16", "@vue/shared": "^3.5.0", "alien-signals": "^0.4.9", "minimatch": "^9.0.3", "muggle-string": "^0.4.1", "path-browserify": "^1.0.1" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-O1ZZFaaBGkKbsRfnVH1ifOK1/1BUkyK+3SQsfnh6PmMmD4qJcTU8godCeA96jjDRTL6zgnK7YzCHfaUlH2r0Mw=="],
"@vue/shared": ["@vue/shared@3.5.17", "", {}, "sha512-CabR+UN630VnsJO/jHWYBC1YVXyMq94KKp6iF5MQgZJs5I8cmjw6oVMO1oDbtBkENSHSSn/UadWlW/OAgdmKrg=="],
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
"ajv": ["ajv@8.12.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2", "uri-js": "^4.2.2" } }, "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA=="],
"ajv-draft-04": ["ajv-draft-04@1.0.0", "", { "peerDependencies": { "ajv": "^8.5.0" }, "optionalPeers": ["ajv"] }, "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw=="],
"ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
"alien-signals": ["alien-signals@0.4.14", "", {}, "sha512-itUAVzhczTmP2U5yX67xVpsbbOiquusbWVyA9N+sy6+r6YVbFkahXvNCeEPWEOMhwDYwbVbGHFkVL03N9I5g+Q=="],
"ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="],
"aria-query": ["aria-query@5.3.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A=="],
"assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="],
@@ -393,13 +346,13 @@
"browserslist": ["browserslist@4.25.1", "", { "dependencies": { "caniuse-lite": "^1.0.30001726", "electron-to-chromium": "^1.5.173", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw=="],
"bun-types": ["bun-types@1.2.18", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-04+Eha5NP7Z0A9YgDAzMk5PHR16ZuLVa83b26kH5+cp1qZW4F6FmAURngE7INf4tKOvCE69vYvDEwoNl1tGiWw=="],
"bun-types": ["bun-types@1.2.17", "", { "dependencies": { "@types/node": "*" } }, "sha512-ElC7ItwT3SCQwYZDYoAH+q6KT4Fxjl8DtZ6qDulUFBmXA8YB4xo+l54J9ZJN+k2pphfn9vk7kfubeSd5QfTVJQ=="],
"cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="],
"caniuse-lite": ["caniuse-lite@1.0.30001727", "", {}, "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q=="],
"caniuse-lite": ["caniuse-lite@1.0.30001726", "", {}, "sha512-VQAUIUzBiZ/UnlM28fSp2CRF3ivUn1BWEvxMcVTNwpw91Py1pGbPIyIKtd+tzct9C3ouceCVdGAXxZOpZAsgdw=="],
"chai": ["chai@5.2.1", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-5nFxhUrX0PqtyogoYOA8IPswy5sZFTOsBFl/9bNsmDLgsxYTzSZQJDPppDnZPTQbzSEm0hqGjWPzRemQCYbD6A=="],
"chai": ["chai@5.2.0", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw=="],
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
@@ -415,12 +368,6 @@
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
"compare-versions": ["compare-versions@6.1.1", "", {}, "sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg=="],
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
"confbox": ["confbox@0.2.2", "", {}, "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ=="],
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
@@ -429,8 +376,6 @@
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
"de-indent": ["de-indent@1.0.2", "", {}, "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg=="],
"debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
"deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="],
@@ -447,17 +392,15 @@
"eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="],
"electron-to-chromium": ["electron-to-chromium@1.5.181", "", {}, "sha512-+ISMj8OIQ+0qEeDj14Rt8WwcTOiqHyAB+5bnK1K7xNNLjBJ4hRCQfUkw8RWtcLbfBzDwc15ZnKH0c7SNOfwiyA=="],
"electron-to-chromium": ["electron-to-chromium@1.5.179", "", {}, "sha512-UWKi/EbBopgfFsc5k61wFpV7WrnnSlSzW/e2XcBmS6qKYTivZlLtoll5/rdqRTxGglGHkmkW0j0pFNJG10EUIQ=="],
"emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
"enhanced-resolve": ["enhanced-resolve@5.18.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ=="],
"entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
"es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="],
"esbuild": ["esbuild@0.25.6", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.6", "@esbuild/android-arm": "0.25.6", "@esbuild/android-arm64": "0.25.6", "@esbuild/android-x64": "0.25.6", "@esbuild/darwin-arm64": "0.25.6", "@esbuild/darwin-x64": "0.25.6", "@esbuild/freebsd-arm64": "0.25.6", "@esbuild/freebsd-x64": "0.25.6", "@esbuild/linux-arm": "0.25.6", "@esbuild/linux-arm64": "0.25.6", "@esbuild/linux-ia32": "0.25.6", "@esbuild/linux-loong64": "0.25.6", "@esbuild/linux-mips64el": "0.25.6", "@esbuild/linux-ppc64": "0.25.6", "@esbuild/linux-riscv64": "0.25.6", "@esbuild/linux-s390x": "0.25.6", "@esbuild/linux-x64": "0.25.6", "@esbuild/netbsd-arm64": "0.25.6", "@esbuild/netbsd-x64": "0.25.6", "@esbuild/openbsd-arm64": "0.25.6", "@esbuild/openbsd-x64": "0.25.6", "@esbuild/openharmony-arm64": "0.25.6", "@esbuild/sunos-x64": "0.25.6", "@esbuild/win32-arm64": "0.25.6", "@esbuild/win32-ia32": "0.25.6", "@esbuild/win32-x64": "0.25.6" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-GVuzuUwtdsghE3ocJ9Bs8PNoF13HNQ5TXbEi2AhvVb8xU1Iwt9Fos9FEamfoee+u/TOsn7GUWc04lz46n2bbTg=="],
"esbuild": ["esbuild@0.25.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.5", "@esbuild/android-arm": "0.25.5", "@esbuild/android-arm64": "0.25.5", "@esbuild/android-x64": "0.25.5", "@esbuild/darwin-arm64": "0.25.5", "@esbuild/darwin-x64": "0.25.5", "@esbuild/freebsd-arm64": "0.25.5", "@esbuild/freebsd-x64": "0.25.5", "@esbuild/linux-arm": "0.25.5", "@esbuild/linux-arm64": "0.25.5", "@esbuild/linux-ia32": "0.25.5", "@esbuild/linux-loong64": "0.25.5", "@esbuild/linux-mips64el": "0.25.5", "@esbuild/linux-ppc64": "0.25.5", "@esbuild/linux-riscv64": "0.25.5", "@esbuild/linux-s390x": "0.25.5", "@esbuild/linux-x64": "0.25.5", "@esbuild/netbsd-arm64": "0.25.5", "@esbuild/netbsd-x64": "0.25.5", "@esbuild/openbsd-arm64": "0.25.5", "@esbuild/openbsd-x64": "0.25.5", "@esbuild/sunos-x64": "0.25.5", "@esbuild/win32-arm64": "0.25.5", "@esbuild/win32-ia32": "0.25.5", "@esbuild/win32-x64": "0.25.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ=="],
"esbuild-register": ["esbuild-register@3.6.0", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "esbuild": ">=0.12 <1" } }, "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg=="],
@@ -469,11 +412,7 @@
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
"expect-type": ["expect-type@1.2.2", "", {}, "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA=="],
"exsolve": ["exsolve@1.0.7", "", {}, "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw=="],
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
"expect-type": ["expect-type@1.2.1", "", {}, "sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw=="],
"fdir": ["fdir@6.4.6", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w=="],
@@ -487,8 +426,6 @@
"foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="],
"fs-extra": ["fs-extra@11.3.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-Z4XaCL6dUDHfP/jT25jJKMmtxvuwbkrD1vNSMFlo9lNLY2c5FHYSQgHPRZUjAB26TpDEoW9HCOgplrdbaPV/ew=="],
"fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="],
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
@@ -503,12 +440,8 @@
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
"he": ["he@1.2.0", "", { "bin": { "he": "bin/he" } }, "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="],
"html-escaper": ["html-escaper@2.0.2", "", {}, "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg=="],
"import-lazy": ["import-lazy@4.0.0", "", {}, "sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw=="],
"indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="],
"is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="],
@@ -533,22 +466,16 @@
"jiti": ["jiti@2.4.2", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A=="],
"jju": ["jju@1.4.0", "", {}, "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA=="],
"js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="],
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
"json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
"jsonfile": ["jsonfile@6.1.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ=="],
"kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="],
"kolorist": ["kolorist@1.8.0", "", {}, "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ=="],
"lightningcss": ["lightningcss@1.30.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.30.1", "lightningcss-darwin-x64": "1.30.1", "lightningcss-freebsd-x64": "1.30.1", "lightningcss-linux-arm-gnueabihf": "1.30.1", "lightningcss-linux-arm64-gnu": "1.30.1", "lightningcss-linux-arm64-musl": "1.30.1", "lightningcss-linux-x64-gnu": "1.30.1", "lightningcss-linux-x64-musl": "1.30.1", "lightningcss-win32-arm64-msvc": "1.30.1", "lightningcss-win32-x64-msvc": "1.30.1" } }, "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg=="],
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ=="],
@@ -571,8 +498,6 @@
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.1", "", { "os": "win32", "cpu": "x64" }, "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg=="],
"local-pkg": ["local-pkg@1.1.1", "", { "dependencies": { "mlly": "^1.7.4", "pkg-types": "^2.0.1", "quansync": "^0.2.8" } }, "sha512-WunYko2W1NcdfAFpuLUoucsgULmgDBRkdxHxWQ7mK0cQqwPiy8E1enjuRBrhLtZkB5iScJ1XIPdhVEFK8aOLSg=="],
"locate-path": ["locate-path@7.2.0", "", { "dependencies": { "p-locate": "^6.0.0" } }, "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA=="],
"lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
@@ -603,14 +528,10 @@
"mkdirp": ["mkdirp@3.0.1", "", { "bin": { "mkdirp": "dist/cjs/src/bin.js" } }, "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg=="],
"mlly": ["mlly@1.7.4", "", { "dependencies": { "acorn": "^8.14.0", "pathe": "^2.0.1", "pkg-types": "^1.3.0", "ufo": "^1.5.4" } }, "sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw=="],
"mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"muggle-string": ["muggle-string@0.4.1", "", {}, "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ=="],
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"node-releases": ["node-releases@2.0.19", "", {}, "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw=="],
@@ -625,8 +546,6 @@
"package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="],
"path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="],
"path-exists": ["path-exists@5.0.0", "", {}, "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ=="],
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
@@ -643,11 +562,9 @@
"picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="],
"pkg-types": ["pkg-types@2.2.0", "", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, "sha512-2SM/GZGAEkPp3KWORxQZns4M+WSeXbC2HEvmOIJe3Cmiv6ieAJvdVhDldtHqM5J1Y7MrR1XhkBT/rMlhh9FdqQ=="],
"playwright": ["playwright@1.53.2", "", { "dependencies": { "playwright-core": "1.53.2" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-6K/qQxVFuVQhRQhFsVZ9fGeatxirtrpPgxzBYWyZLEXJzqYwuL4fuNmfOfD5et1tJE4GScKyPNeLhZeRwuTU3A=="],
"playwright": ["playwright@1.54.0", "", { "dependencies": { "playwright-core": "1.54.0" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-y9yzHmXRwEUOpghM7XGcA38GjWuTOUMaTIcm/5rHcYVjh5MSp9qQMRRMc/+p1cx+csoPnX4wkxAF61v5VKirxg=="],
"playwright-core": ["playwright-core@1.54.0", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-uiWpWaJh3R3etpJ0QrpligEMl62Dk1iSAB6NUXylvmQz+e3eipXHDHvOvydDAssb5Oqo0E818qdn0L9GcJSTyA=="],
"playwright-core": ["playwright-core@1.53.2", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-ox/OytMy+2w1jcYEYlOo1Hhp8hZkLCximMTUTMBXjGUA1KoFfiSZ+DU+3a739jsPY0yoKH2TFy9S2fsJas8yAw=="],
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
@@ -659,10 +576,6 @@
"prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="],
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
"quansync": ["quansync@0.2.10", "", {}, "sha512-t41VRkMYbkHyCYmOvx/6URnN80H7k4X0lLdBMGsz+maAwrJQYB1djpV6vHrQIBE0WBSGqhtEHrK9U3DWWH8v7A=="],
"react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="],
"react-bootstrap-icons": ["react-bootstrap-icons@1.11.6", "", { "dependencies": { "prop-types": "^15.7.2" }, "peerDependencies": { "react": ">=16.8.6" } }, "sha512-ycXiyeSyzbS1C4+MlPTYe0riB+UlZ7LV7YZQYqlERV2cxDiKtntI0huHmP/3VVvzPt4tGxqK0K+Y6g7We3U6tQ=="],
@@ -681,11 +594,9 @@
"redent": ["redent@3.0.0", "", { "dependencies": { "indent-string": "^4.0.0", "strip-indent": "^3.0.0" } }, "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg=="],
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
"resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="],
"rollup": ["rollup@4.44.2", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.44.2", "@rollup/rollup-android-arm64": "4.44.2", "@rollup/rollup-darwin-arm64": "4.44.2", "@rollup/rollup-darwin-x64": "4.44.2", "@rollup/rollup-freebsd-arm64": "4.44.2", "@rollup/rollup-freebsd-x64": "4.44.2", "@rollup/rollup-linux-arm-gnueabihf": "4.44.2", "@rollup/rollup-linux-arm-musleabihf": "4.44.2", "@rollup/rollup-linux-arm64-gnu": "4.44.2", "@rollup/rollup-linux-arm64-musl": "4.44.2", "@rollup/rollup-linux-loongarch64-gnu": "4.44.2", "@rollup/rollup-linux-powerpc64le-gnu": "4.44.2", "@rollup/rollup-linux-riscv64-gnu": "4.44.2", "@rollup/rollup-linux-riscv64-musl": "4.44.2", "@rollup/rollup-linux-s390x-gnu": "4.44.2", "@rollup/rollup-linux-x64-gnu": "4.44.2", "@rollup/rollup-linux-x64-musl": "4.44.2", "@rollup/rollup-win32-arm64-msvc": "4.44.2", "@rollup/rollup-win32-ia32-msvc": "4.44.2", "@rollup/rollup-win32-x64-msvc": "4.44.2", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-PVoapzTwSEcelaWGth3uR66u7ZRo6qhPHc0f2uRO9fX6XDVNrIiGYS0Pj9+R8yIIYSD/mCx2b16Ws9itljKSPg=="],
"rollup": ["rollup@4.44.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.44.1", "@rollup/rollup-android-arm64": "4.44.1", "@rollup/rollup-darwin-arm64": "4.44.1", "@rollup/rollup-darwin-x64": "4.44.1", "@rollup/rollup-freebsd-arm64": "4.44.1", "@rollup/rollup-freebsd-x64": "4.44.1", "@rollup/rollup-linux-arm-gnueabihf": "4.44.1", "@rollup/rollup-linux-arm-musleabihf": "4.44.1", "@rollup/rollup-linux-arm64-gnu": "4.44.1", "@rollup/rollup-linux-arm64-musl": "4.44.1", "@rollup/rollup-linux-loongarch64-gnu": "4.44.1", "@rollup/rollup-linux-powerpc64le-gnu": "4.44.1", "@rollup/rollup-linux-riscv64-gnu": "4.44.1", "@rollup/rollup-linux-riscv64-musl": "4.44.1", "@rollup/rollup-linux-s390x-gnu": "4.44.1", "@rollup/rollup-linux-x64-gnu": "4.44.1", "@rollup/rollup-linux-x64-musl": "4.44.1", "@rollup/rollup-win32-arm64-msvc": "4.44.1", "@rollup/rollup-win32-ia32-msvc": "4.44.1", "@rollup/rollup-win32-x64-msvc": "4.44.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-x8H8aPvD+xbl0Do8oez5f5o8eMS3trfCghc4HhLAnCkj7Vl0d1JWGs0UF/D886zLW2rOj2QymV/JcSSsw+XDNg=="],
"scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="],
@@ -707,15 +618,11 @@
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
"sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="],
"stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="],
"std-env": ["std-env@3.9.0", "", {}, "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw=="],
"storybook": ["storybook@9.0.16", "", { "dependencies": { "@storybook/global": "^5.0.0", "@testing-library/jest-dom": "^6.6.3", "@testing-library/user-event": "^14.6.1", "@vitest/expect": "3.2.4", "@vitest/spy": "3.2.4", "better-opn": "^3.0.2", "esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0", "esbuild-register": "^3.5.0", "recast": "^0.23.5", "semver": "^7.6.2", "ws": "^8.18.0" }, "peerDependencies": { "prettier": "^2 || ^3" }, "optionalPeers": ["prettier"], "bin": "./bin/index.cjs" }, "sha512-DzjzeggdzlXKKBK1L9iqNKqqNpyfeaL1hxxeAOmqgeMezwy5d5mCJmjNcZEmx+prsRmvj1OWm4ZZAg6iP/wABg=="],
"string-argv": ["string-argv@0.3.2", "", {}, "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q=="],
"storybook": ["storybook@9.0.15", "", { "dependencies": { "@storybook/global": "^5.0.0", "@testing-library/jest-dom": "^6.6.3", "@testing-library/user-event": "^14.6.1", "@vitest/expect": "3.2.4", "@vitest/spy": "3.2.4", "better-opn": "^3.0.2", "esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0", "esbuild-register": "^3.5.0", "recast": "^0.23.5", "semver": "^7.6.2", "ws": "^8.18.0" }, "peerDependencies": { "prettier": "^2 || ^3" }, "optionalPeers": ["prettier"], "bin": "./bin/index.cjs" }, "sha512-r9hwcSMM3dq7dkMveaWFTosrmyHCL2FRrV3JOwVnVWraF6GtCgp2k+r4hsYtyp1bY3zdmK9e4KYzXsGs5q1h/Q=="],
"string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
@@ -729,8 +636,6 @@
"strip-indent": ["strip-indent@4.0.0", "", { "dependencies": { "min-indent": "^1.0.1" } }, "sha512-mnVSV2l+Zv6BLpSD/8V87CW/y9EmmbYzGCIavsnsI6/nwn26DwffM/yztm30Z/I2DY9wdS3vXVCMnHDgZaVNoA=="],
"strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
"strip-literal": ["strip-literal@3.0.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA=="],
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
@@ -775,8 +680,6 @@
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
"ufo": ["ufo@1.6.1", "", {}, "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA=="],
"undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],
"unicorn-magic": ["unicorn-magic@0.1.0", "", {}, "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ=="],
@@ -787,18 +690,12 @@
"update-browserslist-db": ["update-browserslist-db@1.1.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw=="],
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
"vite": ["vite@7.0.4", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.6", "picomatch": "^4.0.2", "postcss": "^8.5.6", "rollup": "^4.40.0", "tinyglobby": "^0.2.14" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-SkaSguuS7nnmV7mfJ8l81JGBFV7Gvzp8IzgE8A8t23+AxuNX61Q5H1Tpz5efduSN7NHC8nQXD3sKQKZAu5mNEA=="],
"vite": ["vite@7.0.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.6", "picomatch": "^4.0.2", "postcss": "^8.5.6", "rollup": "^4.40.0", "tinyglobby": "^0.2.14" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-BiKOQoW5HGR30E6JDeNsati6HnSPMVEKbkIWbCiol+xKeu3g5owrjy7kbk/QEMuzCV87dSUTvycYKmlcfGKq3Q=="],
"vite-node": ["vite-node@3.2.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="],
"vite-plugin-dts": ["vite-plugin-dts@4.5.4", "", { "dependencies": { "@microsoft/api-extractor": "^7.50.1", "@rollup/pluginutils": "^5.1.4", "@volar/typescript": "^2.4.11", "@vue/language-core": "2.2.0", "compare-versions": "^6.1.1", "debug": "^4.4.0", "kolorist": "^1.8.0", "local-pkg": "^1.0.0", "magic-string": "^0.30.17" }, "peerDependencies": { "typescript": "*", "vite": "*" }, "optionalPeers": ["vite"] }, "sha512-d4sOM8M/8z7vRXHHq/ebbblfaxENjogAAekcfcDCCwAyvGqnPrc7f4NZbvItS+g4WTgerW0xDwSz5qz11JT3vg=="],
"vitest": ["vitest@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", "@vitest/mocker": "3.2.4", "@vitest/pretty-format": "^3.2.4", "@vitest/runner": "3.2.4", "@vitest/snapshot": "3.2.4", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "debug": "^4.4.1", "expect-type": "^1.2.1", "magic-string": "^0.30.17", "pathe": "^2.0.3", "picomatch": "^4.0.2", "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.14", "tinypool": "^1.1.1", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", "vite-node": "3.2.4", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@vitest/browser": "3.2.4", "@vitest/ui": "3.2.4", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A=="],
"vscode-uri": ["vscode-uri@3.1.0", "", {}, "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ=="],
"webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="],
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
@@ -821,23 +718,11 @@
"@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"@microsoft/api-extractor/minimatch": ["minimatch@3.0.8", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.4.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g=="],
"@microsoft/api-extractor/semver": ["semver@7.5.4", "", { "dependencies": { "lru-cache": "^6.0.0" }, "bin": { "semver": "bin/semver.js" } }, "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.4.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ=="],
"@microsoft/api-extractor/typescript": ["typescript@5.8.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ=="],
"@rushstack/node-core-library/ajv": ["ajv@8.13.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2", "uri-js": "^4.4.1" } }, "sha512-PRA911Blj99jR5RMeTunVbNXMF6Lp4vZXnk5GQjcnUWUTsrXtekg/pnmFFI2u/I36Y/2bITGS30GZCXei6uNkA=="],
"@rushstack/node-core-library/semver": ["semver@7.5.4", "", { "dependencies": { "lru-cache": "^6.0.0" }, "bin": { "semver": "bin/semver.js" } }, "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA=="],
"@rushstack/terminal/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.4.4", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.3", "tslib": "^2.4.0" }, "bundled": true }, "sha512-A9CnAbC6ARNMKcIcrQwq6HeHCjpcBZ5wSx4U01WXCqEKlrzB9F9315WDNHkrs2xbx7YjjSxbUYxuN6EQzpcY2g=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.4.4", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-hHyapA4A3gPaDCNfiqyZUStTMqIkKRshqPIuDOXv1hcBnD4U3l8cP0T1HMCfGRxQ6V64TGCcoswChANyOAwbQg=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.0.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-8K5IFFsQqF9wQNJptGbS6FNKgUTsSRYnTqNCG1vPP8jFdjSv18n2mQfJpkt2Oibo9iBEzcDnDxNwKTzC7svlJw=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.0.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA=="],
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.11", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.9.0" }, "bundled": true }, "sha512-9DPkXtvHydrcOsopiYpUgPHpmj0HWZKMUnL2dZqpvC42lsratuBG06V5ipyno0fUek5VlFsNQ+AcFATSrJXgMA=="],
@@ -845,22 +730,20 @@
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@testing-library/jest-dom/aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="],
"@testing-library/jest-dom/chalk": ["chalk@3.0.0", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg=="],
"@testing-library/jest-dom/dom-accessibility-api": ["dom-accessibility-api@0.6.3", "", {}, "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w=="],
"@vitest/mocker/estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
"ajv-formats/ajv": ["ajv@8.13.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2", "uri-js": "^4.4.1" } }, "sha512-PRA911Blj99jR5RMeTunVbNXMF6Lp4vZXnk5GQjcnUWUTsrXtekg/pnmFFI2u/I36Y/2bITGS30GZCXei6uNkA=="],
"ast-v8-to-istanbul/estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
"loose-envify/js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
"lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
"mlly/pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="],
"path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
"pretty-format/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
@@ -887,22 +770,10 @@
"wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"@microsoft/api-extractor/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
"@microsoft/api-extractor/semver/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="],
"@rushstack/node-core-library/semver/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="],
"mlly/pkg-types/confbox": ["confbox@0.1.8", "", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="],
"string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
"wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"@microsoft/api-extractor/semver/lru-cache/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
"@rushstack/node-core-library/semver/lru-cache/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
}
}
+1 -17
View File
@@ -1,17 +1 @@
// Components
export { Button } from "./components/button/Button";
export { Checkbox } from "./components/checkbox/Checkbox";
export { Chip } from "./components/chip/Chip";
export { Divider } from "./components/divider/Divider";
export { Icon } from "./components/icon/Icon";
export { Input } from "./components/input/Input";
export { InteractiveChip } from "./components/interactive-chip/InteractiveChip";
export { RadioGroup } from "./components/radio-group/RadioGroup";
export { RadioOption } from "./components/radio-group/RadioOption";
export { Scrollable } from "./components/scrollable/Scrollable";
export { Toggle } from "./components/toggle/Toggle";
export { Tooltip } from "./components/tooltip/Tooltip";
export { Typography } from "./components/typography/Typography";
// Styles
import "./index.css";
console.log("Hello via Bun!");
+7 -56
View File
@@ -1,56 +1,13 @@
{
"name": "@openhands/ui",
"publishConfig": {
"access": "public"
},
"license": "MIT",
"authors": [
{
"name": "Mislav Lukach",
"email": "mislavlukach@gmail.com"
},
{
"name": "Stephan Psaras",
"email": "stephan@all-hands.dev"
}
],
"version": "1.0.0-beta.4",
"version": "0.1.0",
"description": "OpenHands UI Components",
"keywords": [
"openhands",
"ui",
"components",
"react",
"typescript",
"tailwindcss"
"components"
],
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
},
"./styles": "./dist/index.css"
},
"files": [
"dist",
"README.md"
],
"sideEffects": [
"**/*.css"
],
"repository": {
"type": "git",
"url": "https://github.com/All-Hands-AI/OpenHands.git",
"directory": "openhands-ui"
},
"homepage": "https://www.all-hands.dev/",
"bugs": {
"url": "https://github.com/All-Hands-AI/OpenHands/issues"
},
"devDependencies": {
"@chromatic-com/storybook": "^4.0.0",
"@storybook/addon-a11y": "^9.0.12",
@@ -65,33 +22,27 @@
"@vitest/coverage-v8": "^3.2.4",
"playwright": "^1.53.1",
"storybook": "^9.0.12",
"typescript": "^5.8.3",
"vite-plugin-dts": "^4.5.4",
"vitest": "^3.2.4"
},
"peerDependencies": {
"react": ">=18.0.0",
"react-dom": ">=18.0.0"
"typescript": "^5.8.3"
},
"dependencies": {
"@floating-ui/react": "^0.27.12",
"clsx": "^2.1.1",
"react": "^19.1.0",
"focus-trap-react": "^11.0.4",
"react-bootstrap-icons": "^1.11.6",
"react-dom": "^19.1.0",
"tailwind-merge": "^3.3.1",
"tailwind-scrollbar": "^4.0.2",
"tailwindcss": "^4.1.10"
},
"scripts": {
"dev": "storybook dev -p 6006",
"build:sb": "storybook build",
"clean": "rm -rf dist",
"build": "vite build",
"prepublishOnly": "bun run clean && bun run build"
"build:sb": "storybook build"
},
"engines": {
"bun": ">=1.2.0",
"node": ">=22.0.0",
"npm": ">=10.0.0"
"bun": ">=1.2.0"
}
}
+1 -11
View File
@@ -25,15 +25,5 @@
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false
},
"include": ["index.ts", "components/**/*", "shared/**/*"],
"exclude": [
"node_modules",
"dist",
"**/*.stories.tsx",
"**/*.test.tsx",
"vitest.config.ts",
"vite.config.ts"
]
}
}
+1 -28
View File
@@ -1,34 +1,7 @@
import { defineConfig } from "vitest/config";
import react from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite";
import { resolve } from "path";
import dts from "vite-plugin-dts";
export default defineConfig({
plugins: [
react(),
tailwindcss(),
dts({
insertTypesEntry: true,
exclude: ["**/*.stories.tsx"],
}),
],
build: {
lib: {
entry: resolve(__dirname, "index.ts"),
name: "OpenHandsUI",
formats: ["es"],
fileName: "index",
},
rollupOptions: {
external: ["react", "react-dom"], // Don't bundle these
output: {
globals: {
react: "React",
"react-dom": "ReactDOM",
},
},
},
cssCodeSplit: false, // Bundle all CSS into a single index.css file
},
plugins: [react(), tailwindcss()],
});
@@ -1,87 +0,0 @@
You are OpenHands agent, a helpful AI assistant that can interact with a computer to solve tasks.
<ROLE>
Your primary role is to assist users by executing commands, modifying code, and solving technical problems effectively. You should be thorough, methodical, and prioritize quality over speed.
* If the user asks a question, like "why is X happening", don't try to fix the problem. Just give an answer to the question.
</ROLE>
<EFFICIENCY>
* Each action you take is somewhat expensive. Wherever possible, combine multiple actions into a single action, e.g. combine multiple bash commands into one, using sed and grep to edit/view multiple files at once.
* When exploring the codebase, use efficient tools like find, grep, and git commands with appropriate filters to minimize unnecessary operations.
</EFFICIENCY>
<FILE_SYSTEM_GUIDELINES>
* When a user provides a file path, do NOT assume it's relative to the current working directory. First explore the file system to locate the file before working on it.
* If asked to edit a file, edit the file directly, rather than creating a new file with a different filename.
* For global search-and-replace operations, consider using `sed` instead of opening file editors multiple times.
</FILE_SYSTEM_GUIDELINES>
<CODE_QUALITY>
* Write clean, efficient code with minimal comments. Avoid redundancy in comments: Do not repeat information that can be easily inferred from the code itself.
* When implementing solutions, focus on making the minimal changes needed to solve the problem.
* Before implementing any changes, first thoroughly understand the codebase through exploration.
* If you are adding a lot of code to a function or file, consider splitting the function or file into smaller pieces when appropriate.
</CODE_QUALITY>
<VERSION_CONTROL>
* When configuring git credentials, use "openhands" as the user.name and "openhands@all-hands.dev" as the user.email by default, unless explicitly instructed otherwise.
* Exercise caution with git operations. Do NOT make potentially dangerous changes (e.g., pushing to main, deleting repositories) unless explicitly asked to do so.
* When committing changes, use `git status` to see all modified files, and stage all files necessary for the commit. Use `git commit -a` whenever possible.
* Do NOT commit files that typically shouldn't go into version control (e.g., node_modules/, .env files, build directories, cache files, large binaries) unless explicitly instructed by the user.
* If unsure about committing certain files, check for the presence of .gitignore files or ask the user for clarification.
</VERSION_CONTROL>
<PULL_REQUESTS>
* **Important**: Do not push to the remote branch and/or start a pull request unless explicitly asked to do so.
* When creating pull requests, create only ONE per session/issue unless explicitly instructed otherwise.
* When working with an existing PR, update it with new commits rather than creating additional PRs for the same issue.
* When updating a PR, preserve the original PR title and purpose, updating description only when necessary.
</PULL_REQUESTS>
<PROBLEM_SOLVING_WORKFLOW>
1. EXPLORATION: Thoroughly explore relevant files and understand the context before proposing solutions
2. ANALYSIS: Consider multiple approaches and select the most promising one
3. TESTING:
* For bug fixes: Create tests to verify issues before implementing fixes
* For new features: Consider test-driven development when appropriate
* If the repository lacks testing infrastructure and implementing tests would require extensive setup, consult with the user before investing time in building testing infrastructure
* If the environment is not set up to run tests, consult with the user first before investing time to install all dependencies
4. IMPLEMENTATION: Make focused, minimal changes to address the problem
5. VERIFICATION: If the environment is set up to run tests, test your implementation thoroughly, including edge cases. If the environment is not set up to run tests, consult with the user first before investing time to run tests.
</PROBLEM_SOLVING_WORKFLOW>
<SECURITY>
* Only use GITHUB_TOKEN and other credentials in ways the user has explicitly requested and would expect.
* Use APIs to work with GitHub or other platforms, unless the user asks otherwise or your task requires browsing.
</SECURITY>
<ENVIRONMENT_SETUP>
* When user asks you to run an application, don't stop if the application is not installed. Instead, please install the application and run the command again.
* If you encounter missing dependencies:
1. First, look around in the repository for existing dependency files (requirements.txt, pyproject.toml, package.json, Gemfile, etc.)
2. If dependency files exist, use them to install all dependencies at once (e.g., `pip install -r requirements.txt`, `npm install`, etc.)
3. Only install individual packages directly if no dependency files are found or if only specific packages are needed
* Similarly, if you encounter missing dependencies for essential tools requested by the user, install them when possible.
</ENVIRONMENT_SETUP>
<TROUBLESHOOTING>
* If you've made repeated attempts to solve a problem but tests still fail or the user reports it's still broken:
1. Step back and reflect on 5-7 different possible sources of the problem
2. Assess the likelihood of each possible cause
3. Methodically address the most likely causes, starting with the highest probability
4. Document your reasoning process
* When you run into any major issue while executing a plan from the user, please don't try to directly work around it. Instead, propose a new plan and confirm with the user before proceeding.
</TROUBLESHOOTING>
<INTERACTION_RULES>
* When the user instructions are high-level or vague, explore the codebase before implementing solutions or interacting with users to figure out the best approach.
1. Read and follow project-specific documentation (rules.md, README, etc.) before making assumptions about workflows, conventions, or feature implementations.
2. Deliver complete, production-ready solutions rather than partial implementations; ensure all components work together before presenting results.
3. Check for existing solutions and test cases before creating new implementations; leverage established patterns rather than reinventing functionality.
* If you are not sure about the user's intent, ask for clarification before proceeding.
1. Always validate file existence and permissions before performing operations, and get back to users with clear error messages with specific paths when files are not found.
2. Support multilingual communication preferences and clarify requirements upfront to avoid repeated back-and-forth questioning.
3. Explain technical decisions clearly when making architectural choices, especially when creating new files or adding complexity to existing solutions.
4. Avoid resource waste by confirming requirements and approach before executing complex operations or generating extensive code.
</INTERACTION_RULES>
-133
View File
@@ -14,14 +14,8 @@ from openhands.cli.commands import (
handle_commands,
)
from openhands.cli.settings import modify_llm_settings_basic
from openhands.cli.shell_config import (
ShellConfigManager,
add_aliases_to_shell_config,
aliases_exist_in_shell_config,
)
from openhands.cli.tui import (
UsageMetrics,
cli_confirm,
display_agent_running_message,
display_banner,
display_event,
@@ -76,7 +70,6 @@ from openhands.memory.condenser.impl.llm_summarizing_condenser import (
LLMSummarizingCondenserConfig,
)
from openhands.microagent.microagent import BaseMicroagent
from openhands.runtime import get_runtime_cls
from openhands.runtime.base import Runtime
from openhands.storage.settings.file_settings_store import FileSettingsStore
@@ -367,115 +360,6 @@ async def run_setup_flow(config: OpenHandsConfig, settings_store: FileSettingsSt
await modify_llm_settings_basic(config, settings_store)
def run_alias_setup_flow(config: OpenHandsConfig) -> None:
"""Run the alias setup flow to configure shell aliases.
Prompts the user to set up aliases for 'openhands' and 'oh' commands.
Handles existing aliases by offering to keep or remove them.
"""
print_formatted_text('')
print_formatted_text(HTML('<gold>🚀 Welcome to OpenHands CLI!</gold>'))
print_formatted_text('')
# Check if aliases already exist
if aliases_exist_in_shell_config():
print_formatted_text(
HTML(
'<grey>We detected existing OpenHands aliases in your shell configuration.</grey>'
)
)
print_formatted_text('')
print_formatted_text(
HTML(
'<grey> • <b>openhands</b> → uvx --python 3.12 --from openhands-ai openhands</grey>'
)
)
print_formatted_text(
HTML(
'<grey> • <b>oh</b> → uvx --python 3.12 --from openhands-ai openhands</grey>'
)
)
print_formatted_text('')
print_formatted_text(
HTML('<ansigreen>✅ Aliases are already configured.</ansigreen>')
)
return # Exit early since aliases already exist
else:
# No existing aliases, show the normal setup flow
print_formatted_text(
HTML('<grey>Would you like to set up convenient shell aliases?</grey>')
)
print_formatted_text('')
print_formatted_text(
HTML(
'<grey>This will add the following aliases to your shell profile:</grey>'
)
)
print_formatted_text(
HTML(
'<grey> • <b>openhands</b> → uvx --python 3.12 --from openhands-ai openhands</grey>'
)
)
print_formatted_text(
HTML(
'<grey> • <b>oh</b> → uvx --python 3.12 --from openhands-ai openhands</grey>'
)
)
print_formatted_text('')
print_formatted_text(
HTML(
'<ansiyellow>⚠️ Note: This requires uv to be installed first.</ansiyellow>'
)
)
print_formatted_text(
HTML(
'<ansiyellow> Installation guide: https://docs.astral.sh/uv/getting-started/installation</ansiyellow>'
)
)
print_formatted_text('')
# Use cli_confirm to get user choice
choice = cli_confirm(
config,
'Set up shell aliases?',
['Yes, set up aliases', 'No, skip this step'],
)
if choice == 0: # User chose "Yes"
success = add_aliases_to_shell_config()
if success:
print_formatted_text('')
print_formatted_text(
HTML('<ansigreen>✅ Aliases added successfully!</ansigreen>')
)
# Get the appropriate reload command using the shell config manager
shell_manager = ShellConfigManager()
reload_cmd = shell_manager.get_reload_command()
print_formatted_text(
HTML(
f'<grey>Run <b>{reload_cmd}</b> (or restart your terminal) to use the new aliases.</grey>'
)
)
else:
print_formatted_text('')
print_formatted_text(
HTML(
'<ansired>❌ Failed to add aliases. You can set them up manually later.</ansired>'
)
)
else: # User chose "No"
print_formatted_text('')
print_formatted_text(
HTML(
'<grey>Skipped alias setup. You can run this setup again anytime.</grey>'
)
)
print_formatted_text('')
async def main_with_loop(loop: asyncio.AbstractEventLoop) -> None:
"""Runs the agent in CLI mode."""
args = parse_arguments()
@@ -567,17 +451,6 @@ async def main_with_loop(loop: asyncio.AbstractEventLoop) -> None:
# This ensures Jupyter plugin is disabled for CLI runtime
finalize_config(config)
# Check if we should show the alias setup flow
# Only show it if aliases don't exist in the shell configuration
# and we're in an interactive environment (not during tests or CI)
if not aliases_exist_in_shell_config() and sys.stdin.isatty():
# Clear the terminal if we haven't shown a banner yet
if not banner_shown:
clear()
run_alias_setup_flow(config)
banner_shown = True
# TODO: Set working directory from config or use current working directory?
current_dir = config.workspace_base
@@ -607,9 +480,6 @@ After reviewing the file, please ask the user what they would like to do with it
else:
task_str = read_task(args, config.cli_multiline_input)
# Setup the runtime
get_runtime_cls(config.runtime).setup(config)
# Run the first session
new_session_requested = await run_session(
loop,
@@ -627,9 +497,6 @@ After reviewing the file, please ask the user what they would like to do with it
loop, config, settings_store, current_dir, None
)
# Teardown the runtime
get_runtime_cls(config.runtime).teardown(config)
def main():
loop = asyncio.new_event_loop()
+34 -67
View File
@@ -17,7 +17,6 @@ from openhands.cli.utils import (
VERIFIED_ANTHROPIC_MODELS,
VERIFIED_MISTRAL_MODELS,
VERIFIED_OPENAI_MODELS,
VERIFIED_OPENHANDS_MODELS,
VERIFIED_PROVIDERS,
organize_models_and_providers,
)
@@ -241,11 +240,6 @@ async def modify_llm_settings_basic(
m for m in provider_models if m not in VERIFIED_MISTRAL_MODELS
]
provider_models = VERIFIED_MISTRAL_MODELS + provider_models
if provider == 'openhands':
provider_models = [
m for m in provider_models if m not in VERIFIED_OPENHANDS_MODELS
]
provider_models = VERIFIED_OPENHANDS_MODELS + provider_models
# Set default model to the best verified model for the provider
if provider == 'anthropic' and VERIFIED_ANTHROPIC_MODELS:
@@ -257,81 +251,54 @@ async def modify_llm_settings_basic(
elif provider == 'mistral' and VERIFIED_MISTRAL_MODELS:
# Use the first model in the VERIFIED_MISTRAL_MODELS list as it's the best/newest
default_model = VERIFIED_MISTRAL_MODELS[0]
elif provider == 'openhands' and VERIFIED_OPENHANDS_MODELS:
# Use the first model in the VERIFIED_OPENHANDS_MODELS list as it's the best/newest
default_model = VERIFIED_OPENHANDS_MODELS[0]
else:
# For other providers, use the first model in the list
default_model = (
provider_models[0] if provider_models else 'claude-sonnet-4-20250514'
)
# For OpenHands provider, directly show all verified models without the "use default" option
if provider == 'openhands':
print_formatted_text(HTML('\n<grey>Available OpenHands models:</grey>'))
# Create a list of models for the cli_confirm function
model_choices = VERIFIED_OPENHANDS_MODELS
model_choice = cli_confirm(
# Show the default model but allow changing it
print_formatted_text(
HTML(f'\n<grey>Default model: </grey><green>{default_model}</green>')
)
change_model = (
cli_confirm(
config,
'(Step 2/3) Select LLM Model:',
model_choices,
'Do you want to use a different model?',
[f'Use {default_model}', 'Select another model'],
)
== 1
)
# Get the selected model from the list
model = model_choices[model_choice]
if change_model:
model_completer = FuzzyWordCompleter(provider_models)
else:
# For other providers, show the default model but allow changing it
print_formatted_text(
HTML(f'\n<grey>Default model: </grey><green>{default_model}</green>')
)
change_model = (
cli_confirm(
config,
'Do you want to use a different model?',
[f'Use {default_model}', 'Select another model'],
)
== 1
)
# Define a validator function that allows custom models but shows a warning
def model_validator(x):
# Allow any non-empty model name
if not x.strip():
return False
if change_model:
model_completer = FuzzyWordCompleter(provider_models)
# Define a validator function that allows custom models but shows a warning
def model_validator(x):
# Allow any non-empty model name
if not x.strip():
return False
# Show a warning for models not in the predefined list, but still allow them
if x not in provider_models:
print_formatted_text(
HTML(
f'<yellow>Warning: {x} is not in the predefined list for provider {provider}. '
f'Make sure this model name is correct.</yellow>'
)
# Show a warning for models not in the predefined list, but still allow them
if x not in provider_models:
print_formatted_text(
HTML(
f'<yellow>Warning: {x} is not in the predefined list for provider {provider}. '
f'Make sure this model name is correct.</yellow>'
)
return True
)
return True
model = await get_validated_input(
session,
'(Step 2/3) Select LLM Model (TAB for options, CTRL-c to cancel): ',
completer=model_completer,
validator=model_validator,
error_message='Model name cannot be empty',
)
else:
# Use the default model
model = default_model
if provider == 'openhands':
print_formatted_text(
HTML(
'\nYou can find your OpenHands LLM API Key in the <a href="https://app.all-hands.dev/settings/api-keys">API Keys</a> tab of OpenHands Cloud: https://app.all-hands.dev/settings/api-keys'
)
model = await get_validated_input(
session,
'(Step 2/3) Select LLM Model (TAB for options, CTRL-c to cancel): ',
completer=model_completer,
validator=model_validator,
error_message='Model name cannot be empty',
)
else:
# Use the default model
model = default_model
api_key = await get_validated_input(
session,
-279
View File
@@ -1,279 +0,0 @@
"""Shell configuration management for OpenHands CLI aliases.
This module provides a simplified, more maintainable approach to managing
shell aliases across different shell types and platforms.
"""
import platform
import re
from pathlib import Path
from typing import Optional
from jinja2 import Template
try:
import shellingham
except ImportError:
shellingham = None
class ShellConfigManager:
"""Manages shell configuration files and aliases across different shells."""
# Shell configuration templates
ALIAS_TEMPLATES = {
'bash': Template("""
# OpenHands CLI aliases
alias openhands="{{ command }}"
alias oh="{{ command }}"
"""),
'zsh': Template("""
# OpenHands CLI aliases
alias openhands="{{ command }}"
alias oh="{{ command }}"
"""),
'fish': Template("""
# OpenHands CLI aliases
alias openhands="{{ command }}"
alias oh="{{ command }}"
"""),
'powershell': Template("""
# OpenHands CLI aliases
function openhands { {{ command }} $args }
function oh { {{ command }} $args }
"""),
}
# Shell configuration file patterns
SHELL_CONFIG_PATTERNS = {
'bash': ['.bashrc', '.bash_profile'],
'zsh': ['.zshrc'],
'fish': ['.config/fish/config.fish'],
'csh': ['.cshrc'],
'tcsh': ['.tcshrc'],
'ksh': ['.kshrc'],
'powershell': [
'Documents/PowerShell/Microsoft.PowerShell_profile.ps1',
'Documents/WindowsPowerShell/Microsoft.PowerShell_profile.ps1',
'.config/powershell/Microsoft.PowerShell_profile.ps1',
],
}
# Regex patterns for detecting existing aliases
ALIAS_PATTERNS = {
'bash': [
r'^\s*alias\s+openhands\s*=',
r'^\s*alias\s+oh\s*=',
],
'zsh': [
r'^\s*alias\s+openhands\s*=',
r'^\s*alias\s+oh\s*=',
],
'fish': [
r'^\s*alias\s+openhands\s*=',
r'^\s*alias\s+oh\s*=',
],
'powershell': [
r'^\s*function\s+openhands\s*\{',
r'^\s*function\s+oh\s*\{',
],
}
def __init__(
self, command: str = 'uvx --python 3.12 --from openhands-ai openhands'
):
"""Initialize the shell config manager.
Args:
command: The command that aliases should point to.
"""
self.command = command
self.is_windows = platform.system() == 'Windows'
def detect_shell(self) -> Optional[str]:
"""Detect the current shell using shellingham.
Returns:
Shell name if detected, None otherwise.
"""
if not shellingham:
return None
try:
shell_name, _ = shellingham.detect_shell()
return shell_name
except Exception:
return None
def get_shell_config_path(self, shell: Optional[str] = None) -> Path:
"""Get the path to the shell configuration file.
Args:
shell: Shell name. If None, will attempt to detect.
Returns:
Path to the shell configuration file.
"""
if shell is None:
shell = self.detect_shell()
home = Path.home()
# Try to find existing config file for the detected shell
if shell and shell in self.SHELL_CONFIG_PATTERNS:
for config_file in self.SHELL_CONFIG_PATTERNS[shell]:
config_path = home / config_file
if config_path.exists():
return config_path
# If no existing file found, return the first option
return home / self.SHELL_CONFIG_PATTERNS[shell][0]
# Fallback logic
if self.is_windows:
# Windows fallback to PowerShell
ps_profile = (
home / 'Documents' / 'PowerShell' / 'Microsoft.PowerShell_profile.ps1'
)
return ps_profile
else:
# Unix fallback to bash
bashrc = home / '.bashrc'
if bashrc.exists():
return bashrc
return home / '.bash_profile'
def get_shell_type_from_path(self, config_path: Path) -> str:
"""Determine shell type from configuration file path.
Args:
config_path: Path to the shell configuration file.
Returns:
Shell type name.
"""
path_str = str(config_path).lower()
if 'powershell' in path_str:
return 'powershell'
elif '.zshrc' in path_str:
return 'zsh'
elif 'fish' in path_str:
return 'fish'
elif '.bashrc' in path_str or '.bash_profile' in path_str:
return 'bash'
else:
return 'bash' # Default fallback
def aliases_exist(self, config_path: Optional[Path] = None) -> bool:
"""Check if OpenHands aliases already exist in the shell config.
Args:
config_path: Path to check. If None, will detect automatically.
Returns:
True if aliases exist, False otherwise.
"""
if config_path is None:
config_path = self.get_shell_config_path()
if not config_path.exists():
return False
shell_type = self.get_shell_type_from_path(config_path)
patterns = self.ALIAS_PATTERNS.get(shell_type, self.ALIAS_PATTERNS['bash'])
try:
with open(config_path, 'r', encoding='utf-8', errors='ignore') as f:
content = f.read()
for pattern in patterns:
if re.search(pattern, content, re.MULTILINE):
return True
return False
except Exception:
return False
def add_aliases(self, config_path: Optional[Path] = None) -> bool:
"""Add OpenHands aliases to the shell configuration.
Args:
config_path: Path to modify. If None, will detect automatically.
Returns:
True if successful, False otherwise.
"""
if config_path is None:
config_path = self.get_shell_config_path()
# Check if aliases already exist
if self.aliases_exist(config_path):
return True
try:
# Ensure parent directory exists
config_path.parent.mkdir(parents=True, exist_ok=True)
# Get the appropriate template
shell_type = self.get_shell_type_from_path(config_path)
template = self.ALIAS_TEMPLATES.get(
shell_type, self.ALIAS_TEMPLATES['bash']
)
# Render the aliases
aliases_content = template.render(command=self.command)
# Append to the config file
with open(config_path, 'a', encoding='utf-8') as f:
f.write(aliases_content)
return True
except Exception as e:
print(f'Error adding aliases: {e}')
return False
def get_reload_command(self, config_path: Optional[Path] = None) -> str:
"""Get the command to reload the shell configuration.
Args:
config_path: Path to the config file. If None, will detect automatically.
Returns:
Command to reload the shell configuration.
"""
if config_path is None:
config_path = self.get_shell_config_path()
shell_type = self.get_shell_type_from_path(config_path)
if shell_type == 'zsh':
return 'source ~/.zshrc'
elif shell_type == 'fish':
return 'source ~/.config/fish/config.fish'
elif shell_type == 'powershell':
return '. $PROFILE'
else: # bash and others
if '.bash_profile' in str(config_path):
return 'source ~/.bash_profile'
else:
return 'source ~/.bashrc'
# Convenience functions that use the ShellConfigManager
def add_aliases_to_shell_config() -> bool:
"""Add OpenHands aliases to the shell configuration."""
manager = ShellConfigManager()
return manager.add_aliases()
def aliases_exist_in_shell_config() -> bool:
"""Check if OpenHands aliases exist in the shell configuration."""
manager = ShellConfigManager()
return manager.aliases_exist()
def get_shell_config_path() -> Path:
"""Get the path to the shell configuration file."""
manager = ShellConfigManager()
return manager.get_shell_config_path()
+1 -14
View File
@@ -104,8 +104,6 @@ def extract_model_and_provider(model: str) -> ModelInfo:
return ModelInfo(provider='anthropic', model=split[0], separator='/')
if split[0] in VERIFIED_MISTRAL_MODELS:
return ModelInfo(provider='mistral', model=split[0], separator='/')
if split[0] in VERIFIED_OPENHANDS_MODELS:
return ModelInfo(provider='openhands', model=split[0], separator='/')
# return as model only
return ModelInfo(provider='', model=model, separator='')
@@ -147,7 +145,7 @@ def organize_models_and_providers(
return result_dict
VERIFIED_PROVIDERS = ['openhands', 'anthropic', 'openai', 'mistral']
VERIFIED_PROVIDERS = ['anthropic', 'openai', 'mistral']
VERIFIED_OPENAI_MODELS = [
'o4-mini',
@@ -177,17 +175,6 @@ VERIFIED_ANTHROPIC_MODELS = [
VERIFIED_MISTRAL_MODELS = [
'devstral-small-2505',
'devstral-small-2507',
'devstral-medium-2507',
]
VERIFIED_OPENHANDS_MODELS = [
'claude-sonnet-4-20250514',
'claude-opus-4-20250514',
'devstral-small-2507',
'devstral-medium-2507',
'o4-mini',
'gemini-2.5-pro',
]
+5 -9
View File
@@ -107,13 +107,13 @@ def attempt_vscode_extension_install():
f'INFO: First-time setup: attempting to install the OpenHands {editor_name} extension...'
)
# Attempt 1: Install from bundled .vsix
if _attempt_bundled_install(editor_command, editor_name):
# Attempt 1: Download from GitHub Releases (the new primary method)
if _attempt_github_install(editor_command, editor_name):
_mark_installation_successful(flag_file, editor_name)
return # Success! We are done.
# Attempt 2: Download from GitHub Releases
if _attempt_github_install(editor_command, editor_name):
# Attempt 2: Install from bundled .vsix
if _attempt_bundled_install(editor_command, editor_name):
_mark_installation_successful(flag_file, editor_name)
return # Success! We are done.
@@ -267,12 +267,8 @@ def _attempt_bundled_install(editor_command: str, editor_name: str) -> bool:
logger.debug(
f'Bundled .vsix installation failed: {process.stderr.strip()}'
)
else:
logger.debug(f'Bundled .vsix not found at {vsix_path}.')
except Exception as e:
logger.warning(
f'Could not auto-install extension. Please make sure "code" command is in PATH. Error: {e}'
)
logger.debug(f'Could not locate bundled .vsix: {e}.')
return False
@@ -89,7 +89,6 @@ class OpenHandsConfig(BaseModel):
run_as_openhands: bool = Field(default=True)
max_iterations: int = Field(default=OH_MAX_ITERATIONS)
max_budget_per_task: float | None = Field(default=None)
init_git_in_empty_workspace: bool = Field(default=False)
disable_color: bool = Field(default=False)
jwt_secret: SecretStr | None = Field(default=None)
-10
View File
@@ -104,7 +104,6 @@ MODELS_WITHOUT_STOP_WORDS = [
'o1-preview',
'o1',
'o1-2024-12-17',
'xai/grok-4-0709',
]
@@ -173,15 +172,6 @@ class LLM(RetryMixin, DebugMixin):
# litellm will handle it a bit differently than the openai-compatible params
kwargs['top_k'] = self.config.top_k
# Handle OpenHands provider - rewrite to litellm_proxy
if self.config.model.startswith('openhands/'):
model_name = self.config.model.removeprefix('openhands/')
self.config.model = f'litellm_proxy/{model_name}'
self.config.base_url = 'https://llm-proxy.app.all-hands.dev/'
logger.debug(
f'Rewrote openhands/{model_name} to {self.config.model} with base URL {self.config.base_url}'
)
if (
self.config.model.lower() in REASONING_EFFORT_SUPPORTED_MODELS
or self.config.model.split('/')[-1] in REASONING_EFFORT_SUPPORTED_MODELS
+59 -12
View File
@@ -367,7 +367,9 @@ class Runtime(FileEditRuntimeMixin):
selected_branch: str | None,
) -> str:
if not selected_repository:
if self.config.init_git_in_empty_workspace:
# In SaaS mode (indicated by user_id being set), always run git init
# In OSS mode, only run git init if workspace_base is not set
if self.user_id or not self.config.workspace_base:
logger.debug(
'No repository selected. Initializing a new git repository in the workspace.'
)
@@ -423,7 +425,61 @@ class Runtime(FileEditRuntimeMixin):
return dir_name
def maybe_run_setup_script(self):
"""Run .openhands/setup.sh if it exists in the workspace or repository."""
"""
Run setup.sh scripts if they exist in the workspace or repository.
Order of execution:
1. Organization-level setup.sh (from org/.openhands or org/openhands-config)
2. Repository-level setup.sh (from .openhands/setup.sh)
"""
# First, try to run the organization-level setup script if we have a repository
selected_repository = getattr(self, 'selected_repository', None)
if selected_repository:
repo_parts = selected_repository.split('/')
if len(repo_parts) >= 2:
org_name = repo_parts[0] # First part is the organization name
# Determine if this is a GitLab repository
self._is_gitlab_repository(selected_repository)
# For GitLab, use openhands-config (since .openhands is not a valid repo name)
# For other providers, use .openhands
# Check for org-level setup.sh in the temporary directory
# This assumes the org repo was already cloned during microagent loading
org_setup_script = f'/tmp/org_openhands_{org_name}/setup.sh'
# Try to read the org-level setup script
org_setup_read_obs = self.read(FileReadAction(path=org_setup_script))
if not isinstance(org_setup_read_obs, ErrorObservation):
self.log(
'info',
f'Found org-level setup.sh at {org_setup_script}',
)
if self.status_callback:
self.status_callback(
'info',
RuntimeStatus.SETTING_UP_WORKSPACE,
'Running organization-level setup script...',
)
# Run the org-level setup script
org_setup_action = CmdRunAction(
f'chmod +x {org_setup_script} && source {org_setup_script}',
blocking=True,
hidden=True,
)
org_setup_action.set_hard_timeout(600)
# Add the action to the event stream as an ENVIRONMENT event
source = EventSource.ENVIRONMENT
self.event_stream.add_event(org_setup_action, source)
# Execute the action
self.run_action(org_setup_action)
# Now run the repository-level setup script
setup_script = '.openhands/setup.sh'
read_obs = self.read(FileReadAction(path=setup_script))
if isinstance(read_obs, ErrorObservation):
@@ -1060,8 +1116,7 @@ fi
def get_git_changes(self, cwd: str) -> list[dict[str, str]] | None:
self.git_handler.set_cwd(cwd)
changes = self.git_handler.get_git_changes()
return changes
return self.git_handler.get_git_changes()
def get_git_diff(self, file_path: str, cwd: str) -> dict[str, str]:
self.git_handler.set_cwd(cwd)
@@ -1086,11 +1141,3 @@ fi
Returns False by default.
"""
return False
@classmethod
def setup(cls, config: OpenHandsConfig, headless_mode: bool = False):
"""Set up the environment for runtimes to be created."""
@classmethod
def teardown(cls, config: OpenHandsConfig):
"""Tear down the environment in which runtimes are created."""
+1 -1
View File
@@ -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.all-hands.dev/all-hands-ai/runtime:0.49-nikolaik"
runtime_container_image = "docker.all-hands.dev/all-hands-ai/runtime:0.48-nikolaik"
```
#### Additional Kubernetes Options
+125 -365
View File
@@ -36,7 +36,6 @@ from openhands.runtime.impl.docker.docker_runtime import (
VSCODE_PORT_RANGE,
)
from openhands.runtime.plugins import PluginRequirement
from openhands.runtime.plugins.vscode import VSCodeRequirement
from openhands.runtime.runtime_status import RuntimeStatus
from openhands.runtime.utils import find_available_tcp_port
from openhands.runtime.utils.command import get_action_execution_server_startup_command
@@ -61,9 +60,6 @@ class ActionExecutionServerInfo:
# Global dictionary to track running server processes by session ID
_RUNNING_SERVERS: dict[str, ActionExecutionServerInfo] = {}
# Global list to track warm servers waiting for use
_WARM_SERVERS: list[ActionExecutionServerInfo] = []
def get_user_info() -> tuple[int, str | None]:
"""Get user ID and username in a cross-platform way."""
@@ -209,9 +205,6 @@ class LocalRuntime(ActionExecutionClient):
"""Start the action_execution_server on the local machine or connect to an existing one."""
self.set_runtime_status(RuntimeStatus.STARTING_RUNTIME)
# Get environment variables for warm server configuration
desired_num_warm_servers = int(os.getenv('DESIRED_NUM_WARM_SERVERS', '0'))
# Check if there's already a server running for this session ID
if self.sid in _RUNNING_SERVERS:
self.log('info', f'Connecting to existing server for session {self.sid}')
@@ -259,99 +252,133 @@ class LocalRuntime(ActionExecutionClient):
f'Using workspace directory: {self.config.workspace_mount_path_in_sandbox}'
)
# Check if we have a warm server available
warm_server_available = False
if _WARM_SERVERS and not self.attach_to_existing:
try:
# Pop a warm server from the list
self.log('info', 'Using a warm server')
server_info = _WARM_SERVERS.pop(0)
# Start a new server
self._execution_server_port = self._find_available_port(
EXECUTION_SERVER_PORT_RANGE
)
self._vscode_port = int(
os.getenv('VSCODE_PORT')
or str(self._find_available_port(VSCODE_PORT_RANGE))
)
self._app_ports = [
int(
os.getenv('APP_PORT_1')
or str(self._find_available_port(APP_PORT_RANGE_1))
),
int(
os.getenv('APP_PORT_2')
or str(self._find_available_port(APP_PORT_RANGE_2))
),
]
self.api_url = (
f'{self.config.sandbox.local_runtime_url}:{self._execution_server_port}'
)
# Use the warm server
self.server_process = server_info.process
self._execution_server_port = server_info.execution_server_port
self._log_thread = server_info.log_thread
self._log_thread_exit_event = server_info.log_thread_exit_event
self._vscode_port = server_info.vscode_port
self._app_ports = server_info.app_ports
# Start the server process
cmd = get_action_execution_server_startup_command(
server_port=self._execution_server_port,
plugins=self.plugins,
app_config=self.config,
python_prefix=[],
python_executable=sys.executable,
override_user_id=self._user_id,
override_username=self._username,
)
# We need to clean up the warm server's temp workspace and create a new one
if server_info.temp_workspace:
shutil.rmtree(server_info.temp_workspace)
self.log('info', f'Starting server with command: {cmd}')
env = os.environ.copy()
# Get the code repo path
code_repo_path = os.path.dirname(os.path.dirname(openhands.__file__))
env['PYTHONPATH'] = os.pathsep.join(
[code_repo_path, env.get('PYTHONPATH', '')]
)
env['OPENHANDS_REPO_PATH'] = code_repo_path
env['LOCAL_RUNTIME_MODE'] = '1'
env['VSCODE_PORT'] = str(self._vscode_port)
# Create a new temp workspace for this session
if (
self._temp_workspace is None
and self.config.workspace_base is None
):
self._temp_workspace = tempfile.mkdtemp(
prefix=f'openhands_workspace_{self.sid}',
)
self.config.workspace_mount_path_in_sandbox = (
self._temp_workspace
)
# Derive environment paths using sys.executable
interpreter_path = sys.executable
python_bin_path = os.path.dirname(interpreter_path)
self.api_url = f'{self.config.sandbox.local_runtime_url}:{self._execution_server_port}'
# Prepend the interpreter's bin directory to PATH for subprocesses
env['PATH'] = f'{python_bin_path}{os.pathsep}{env.get("PATH", "")}'
logger.debug(f'Updated PATH for subprocesses: {env["PATH"]}')
# Store the server process in the global dictionary with the new workspace
_RUNNING_SERVERS[self.sid] = ActionExecutionServerInfo(
process=self.server_process,
execution_server_port=self._execution_server_port,
vscode_port=self._vscode_port,
app_ports=self._app_ports,
log_thread=self._log_thread,
log_thread_exit_event=self._log_thread_exit_event,
temp_workspace=self._temp_workspace,
workspace_mount_path=self.config.workspace_mount_path_in_sandbox,
# Check dependencies using the derived env_root_path if not skipped
if os.getenv('SKIP_DEPENDENCY_CHECK', '') != '1':
check_browser = self.config.enable_browser and sys.platform != 'win32'
check_dependencies(code_repo_path, check_browser)
self.server_process = subprocess.Popen( # noqa: S603
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
universal_newlines=True,
bufsize=1,
env=env,
cwd=code_repo_path, # Explicitly set the working directory
)
# Start a thread to read and log server output
def log_output() -> None:
if not self.server_process or not self.server_process.stdout:
self.log(
'error', 'Server process or stdout not available for logging.'
)
return
try:
# Read lines while the process is running and stdout is available
while self.server_process.poll() is None:
if self._log_thread_exit_event.is_set(): # Check exit event
self.log('info', 'Log thread received exit signal.')
break # Exit loop if signaled
line = self.server_process.stdout.readline()
if not line:
# Process might have exited between poll() and readline()
break
self.log('info', f'Server: {line.strip()}')
# Capture any remaining output after the process exits OR if signaled
if (
not self._log_thread_exit_event.is_set()
): # Check again before reading remaining
self.log(
'info', 'Server process exited, reading remaining output.'
)
for line in self.server_process.stdout:
if (
self._log_thread_exit_event.is_set()
): # Check inside loop too
self.log(
'info',
'Log thread received exit signal while reading remaining output.',
)
break
self.log('info', f'Server (remaining): {line.strip()}')
warm_server_available = True
except IndexError:
# No warm servers available
self.log('info', 'No warm servers available, starting a new server')
warm_server_available = False
except Exception as e:
# Error using warm server
self.log('error', f'Error using warm server: {e}')
warm_server_available = False
# Log the error, but don't prevent the thread from potentially exiting
self.log('error', f'Error reading server output: {e}')
finally:
self.log(
'info', 'Log output thread finished.'
) # Add log for thread exit
# If no warm server is available, start a new one
if not warm_server_available:
# Create a new server
server_info, api_url = _create_server(
config=self.config,
plugins=self.plugins,
workspace_prefix=self.sid,
)
self._log_thread = threading.Thread(target=log_output, daemon=True)
self._log_thread.start()
# Set instance variables
self.server_process = server_info.process
self._execution_server_port = server_info.execution_server_port
self._vscode_port = server_info.vscode_port
self._app_ports = server_info.app_ports
self._log_thread = server_info.log_thread
self._log_thread_exit_event = server_info.log_thread_exit_event
# We need to use the existing temp workspace, not the one created by _create_server
if (
server_info.temp_workspace
and server_info.temp_workspace != self._temp_workspace
):
shutil.rmtree(server_info.temp_workspace)
self.api_url = api_url
# Store the server process in the global dictionary with the correct workspace
_RUNNING_SERVERS[self.sid] = ActionExecutionServerInfo(
process=self.server_process,
execution_server_port=self._execution_server_port,
vscode_port=self._vscode_port,
app_ports=self._app_ports,
log_thread=self._log_thread,
log_thread_exit_event=self._log_thread_exit_event,
temp_workspace=self._temp_workspace,
workspace_mount_path=self.config.workspace_mount_path_in_sandbox,
)
# Store the server process in the global dictionary
_RUNNING_SERVERS[self.sid] = ActionExecutionServerInfo(
process=self.server_process,
execution_server_port=self._execution_server_port,
vscode_port=self._vscode_port,
app_ports=self._app_ports,
log_thread=self._log_thread,
log_thread_exit_event=self._log_thread_exit_event,
temp_workspace=self._temp_workspace,
workspace_mount_path=self.config.workspace_mount_path_in_sandbox,
)
self.log('info', f'Waiting for server to become ready at {self.api_url}...')
self.set_runtime_status(RuntimeStatus.STARTING_RUNTIME)
@@ -369,38 +396,14 @@ class LocalRuntime(ActionExecutionClient):
self.set_runtime_status(RuntimeStatus.READY)
self._runtime_initialized = True
# Check if we need to create more warm servers after connecting
if (
desired_num_warm_servers > 0
and len(_WARM_SERVERS) < desired_num_warm_servers
):
num_to_create = desired_num_warm_servers - len(_WARM_SERVERS)
self.log(
'info',
f'Creating {num_to_create} additional warm servers to reach desired count',
)
for _ in range(num_to_create):
_create_warm_server_in_background(self.config, self.plugins)
@classmethod
def setup(cls, config: OpenHandsConfig, headless_mode: bool = False):
should_check_dependencies = os.getenv('SKIP_DEPENDENCY_CHECK', '') != '1'
if should_check_dependencies:
code_repo_path = os.path.dirname(os.path.dirname(openhands.__file__))
check_browser = config.enable_browser and sys.platform != 'win32'
check_dependencies(code_repo_path, check_browser)
initial_num_warm_servers = int(os.getenv('INITIAL_NUM_WARM_SERVERS', '0'))
# Initialize warm servers if needed
if initial_num_warm_servers > 0 and len(_WARM_SERVERS) == 0:
plugins = _get_plugins(config)
# Copy the logic from Runtime where we add a VSCodePlugin on init if missing
if not headless_mode:
plugins.append(VSCodeRequirement())
for _ in range(initial_num_warm_servers):
_create_warm_server(config, plugins)
def _find_available_port(
self, port_range: tuple[int, int], max_attempts: int = 5
) -> int:
port = port_range[1]
for _ in range(max_attempts):
port = find_available_tcp_port(port_range[0], port_range[1])
return port
return port
@tenacity.retry(
wait=tenacity.wait_fixed(2),
@@ -450,21 +453,6 @@ class LocalRuntime(ActionExecutionClient):
json={'action': event_to_dict(action)},
)
)
# After executing the action, check if we need to create more warm servers
desired_num_warm_servers = int(
os.getenv('DESIRED_NUM_WARM_SERVERS', '0')
)
if (
desired_num_warm_servers > 0
and len(_WARM_SERVERS) < desired_num_warm_servers
):
self.log(
'info',
f'Creating a new warm server to maintain desired count of {desired_num_warm_servers}',
)
_create_warm_server_in_background(self.config, self.plugins)
return observation_from_dict(response.json())
except httpx.NetworkError:
raise AgentRuntimeDisconnectedError('Server connection lost')
@@ -531,33 +519,6 @@ class LocalRuntime(ActionExecutionClient):
del _RUNNING_SERVERS[conversation_id]
logger.info(f'LocalRuntime for conversation {conversation_id} deleted')
# Also clean up any warm servers if this is the last conversation being deleted
if not _RUNNING_SERVERS:
logger.info('No active conversations, cleaning up warm servers')
for server_info in _WARM_SERVERS[:]:
# Signal the log thread to exit
server_info.log_thread_exit_event.set()
# Terminate the server process
if server_info.process:
server_info.process.terminate()
try:
server_info.process.wait(timeout=5)
except subprocess.TimeoutExpired:
server_info.process.kill()
# Wait for the log thread to finish
server_info.log_thread.join(timeout=5)
# Clean up temp workspace
if server_info.temp_workspace:
shutil.rmtree(server_info.temp_workspace)
# Remove from warm servers list
_WARM_SERVERS.remove(server_info)
logger.info('All warm servers cleaned up')
@property
def runtime_url(self) -> str:
runtime_url = os.getenv('RUNTIME_URL')
@@ -595,204 +556,3 @@ class LocalRuntime(ActionExecutionClient):
for port in self._app_ports:
hosts[f'{self.runtime_url}:{port}'] = port
return hosts
def _python_bin_path():
# Derive environment paths using sys.executable
interpreter_path = sys.executable
python_bin_path = os.path.dirname(interpreter_path)
return python_bin_path
def _create_server(
config: OpenHandsConfig,
plugins: list[PluginRequirement],
workspace_prefix: str,
) -> tuple[ActionExecutionServerInfo, str]:
logger.info('Creating a server')
# Set up workspace directory
temp_workspace = tempfile.mkdtemp(
prefix=f'openhands_workspace_{workspace_prefix}',
)
workspace_mount_path = temp_workspace
# Find available ports
execution_server_port = find_available_tcp_port(*EXECUTION_SERVER_PORT_RANGE)
vscode_port = int(
os.getenv('VSCODE_PORT') or str(find_available_tcp_port(*VSCODE_PORT_RANGE))
)
app_ports = [
int(os.getenv('APP_PORT_1') or str(find_available_tcp_port(*APP_PORT_RANGE_1))),
int(os.getenv('APP_PORT_2') or str(find_available_tcp_port(*APP_PORT_RANGE_2))),
]
# Get user info
user_id, username = get_user_info()
# Start the server process
cmd = get_action_execution_server_startup_command(
server_port=execution_server_port,
plugins=plugins,
app_config=config,
python_prefix=['poetry', 'run'],
override_user_id=user_id,
override_username=username,
)
logger.info(f'Starting server with command: {cmd}')
env = os.environ.copy()
# Get the code repo path
code_repo_path = os.path.dirname(os.path.dirname(openhands.__file__))
env['PYTHONPATH'] = os.pathsep.join([code_repo_path, env.get('PYTHONPATH', '')])
env['OPENHANDS_REPO_PATH'] = code_repo_path
env['LOCAL_RUNTIME_MODE'] = '1'
env['VSCODE_PORT'] = str(vscode_port)
# Prepend the interpreter's bin directory to PATH for subprocesses
env['PATH'] = f'{_python_bin_path()}{os.pathsep}{env.get("PATH", "")}'
logger.debug(f'Updated PATH for subprocesses: {env["PATH"]}')
server_process = subprocess.Popen( # noqa: S603
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
universal_newlines=True,
bufsize=1,
env=env,
cwd=code_repo_path,
)
log_thread_exit_event = threading.Event()
# Start a thread to read and log server output
def log_output() -> None:
if not server_process or not server_process.stdout:
logger.error('server process or stdout not available for logging.')
return
try:
# Read lines while the process is running and stdout is available
while server_process.poll() is None:
if log_thread_exit_event.is_set():
logger.info('server log thread received exit signal.')
break
line = server_process.stdout.readline()
if not line:
break
logger.info(f'server: {line.strip()}')
# Capture any remaining output
if not log_thread_exit_event.is_set():
logger.info('server process exited, reading remaining output.')
for line in server_process.stdout:
if log_thread_exit_event.is_set():
break
logger.info(f'server (remaining): {line.strip()}')
except Exception as e:
logger.error(f'Error reading server output: {e}')
finally:
logger.info('server log output thread finished.')
log_thread = threading.Thread(target=log_output, daemon=True)
log_thread.start()
# Create server info object
server_info = ActionExecutionServerInfo(
process=server_process,
execution_server_port=execution_server_port,
vscode_port=vscode_port,
app_ports=app_ports,
log_thread=log_thread,
log_thread_exit_event=log_thread_exit_event,
temp_workspace=temp_workspace,
workspace_mount_path=workspace_mount_path,
)
# API URL for the server
api_url = f'{config.sandbox.local_runtime_url}:{execution_server_port}'
return server_info, api_url
def _create_warm_server(
config: OpenHandsConfig,
plugins: list[PluginRequirement],
) -> None:
"""Create a warm server in the background."""
try:
server_info, api_url = _create_server(
config=config,
plugins=plugins,
workspace_prefix='warm',
)
# Wait for the server to be ready
session = httpx.Client(timeout=30)
# Use tenacity to retry the connection
@tenacity.retry(
wait=tenacity.wait_fixed(2),
stop=tenacity.stop_after_delay(120) | stop_if_should_exit(),
before_sleep=lambda retry_state: logger.debug(
f'Waiting for warm server to be ready... (attempt {retry_state.attempt_number})'
),
)
def wait_until_alive() -> bool:
if server_info.process.poll() is not None:
raise RuntimeError('Warm server process died')
try:
response = session.get(f'{api_url}/alive')
response.raise_for_status()
return True
except Exception as e:
logger.debug(f'Warm server not ready yet: {e}')
raise
wait_until_alive()
logger.info(f'Warm server ready at port {server_info.execution_server_port}')
# Add to the warm servers list
_WARM_SERVERS.append(server_info)
except Exception as e:
logger.error(f'Failed to create warm server: {e}')
# Clean up resources
if 'server_info' in locals():
server_info.log_thread_exit_event.set()
if server_info.process:
server_info.process.terminate()
try:
server_info.process.wait(timeout=5)
except subprocess.TimeoutExpired:
server_info.process.kill()
server_info.log_thread.join(timeout=5)
if server_info.temp_workspace:
shutil.rmtree(server_info.temp_workspace)
def _create_warm_server_in_background(
config: OpenHandsConfig,
plugins: list[PluginRequirement],
) -> None:
"""Start a new thread to create a warm server."""
thread = threading.Thread(
target=_create_warm_server, daemon=True, args=(config, plugins)
)
thread.start()
def _get_plugins(config: OpenHandsConfig) -> list[PluginRequirement]:
from openhands.controller.agent import Agent
from openhands.llm.llm import LLM
agent_config = config.get_agent_config(config.default_agent)
llm = LLM(
config=config.get_llm_config_from_agent(config.default_agent),
)
agent = Agent.get_cls(config.default_agent)(llm, agent_config)
plugins = agent.sandbox_plugins
return plugins
+1 -2
View File
@@ -62,11 +62,10 @@ class VSCodePlugin(Plugin):
f'Port {self.vscode_port} is not available. VSCode plugin will be disabled.'
)
return
workspace_path = os.getenv('WORKSPACE_MOUNT_PATH_IN_SANDBOX', '/workspace')
cmd = (
f"su - {username} -s /bin/bash << 'EOF'\n"
f'sudo chown -R {username}:{username} /openhands/.openvscode-server\n'
f'cd {workspace_path}\n'
'cd /workspace\n'
f'exec /openhands/.openvscode-server/bin/openvscode-server --host 0.0.0.0 --connection-token {self.vscode_connection_token} --port {self.vscode_port} --disable-workspace-trust\n'
'EOF'
)
@@ -13,39 +13,25 @@ ENV POETRY_VIRTUALENVS_PATH=/openhands/poetry \
{% macro setup_base_system() %}
# Install base system dependencies
{% if (('ubuntu' in base_image) or ('mswebench' in base_image)) %}
RUN apt-get update && \
apt-get install -y --no-install-recommends \
wget curl ca-certificates sudo apt-utils git jq tmux build-essential ripgrep ffmpeg \
{%- if (base_image.endswith(':latest') or base_image.endswith(':24.04') or ('mswebench' in base_image)) -%}
{%- if 'ubuntu' in base_image and (base_image.endswith(':latest') or base_image.endswith(':24.04')) -%}
libgl1 \
{%- else %}
libgl1-mesa-glx \
{% endif -%}
libasound2-plugins libatomic1 && \
{%- if 'ubuntu' in base_image -%}
curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && \
TZ=Etc/UTC DEBIAN_FRONTEND=noninteractive \
{%- if ('mswebench' in base_image) -%}
apt-get install -y --no-install-recommends nodejs python3 python-is-python3 python3-pip python3-venv && \
{%- else %}
apt-get install -y --no-install-recommends nodejs python3.12 python-is-python3 python3-pip python3.12-venv && \
{% endif -%}
corepack enable yarn && \
{% endif -%}
apt-get clean && \
rm -rf /var/lib/apt/lists/*
{% endif %}
{% if (('ubuntu' not in base_image) and ('mswebench' not in base_image)) %}
RUN apt-get update && \
apt-get install -y --no-install-recommends \
wget curl ca-certificates sudo apt-utils git jq tmux build-essential ripgrep ffmpeg \
libgl1-mesa-glx \
libasound2-plugins libatomic1 && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
{% endif %}
{% if (('ubuntu' in base_image) or ('mswebench' in base_image)) %}
{% if 'ubuntu' in base_image %}
RUN ln -s "$(dirname $(which node))/corepack" /usr/local/bin/corepack && \
npm install -g corepack && corepack enable yarn && \
curl -fsSL --compressed https://install.python-poetry.org | python -
@@ -119,22 +105,28 @@ RUN rm -rf ${OPENVSCODE_SERVER_ROOT}/extensions/{handlebars,pug,json,diff,grunt,
# Install all dependencies
WORKDIR /openhands/code
RUN \
/openhands/micromamba/bin/micromamba config set changeps1 False && \
# Configure micromamba and poetry
RUN /openhands/micromamba/bin/micromamba config set changeps1 False && \
/openhands/micromamba/bin/micromamba run -n openhands poetry config virtualenvs.path /openhands/poetry && \
/openhands/micromamba/bin/micromamba run -n openhands poetry env use python3.12 && \
# Install project dependencies
/openhands/micromamba/bin/micromamba run -n openhands poetry install --only main,runtime --no-interaction --no-root && \
# Update and install additional tools
# (There used to be an "apt-get update" here, hopefully we can skip it.)
/openhands/micromamba/bin/micromamba run -n openhands poetry run playwright install --with-deps chromium && \
# Set environment variables
/openhands/micromamba/bin/micromamba run -n openhands poetry run python -c "import sys; print('OH_INTERPRETER_PATH=' + sys.executable)" >> /etc/environment && \
# Set permissions
/openhands/micromamba/bin/micromamba run -n openhands poetry env use python3.12
# Install project dependencies in smaller chunks
RUN /openhands/micromamba/bin/micromamba run -n openhands poetry install --only main --no-interaction --no-root
RUN /openhands/micromamba/bin/micromamba run -n openhands poetry install --only runtime --no-interaction --no-root
# Install playwright and its dependencies
RUN apt-get update && \
/openhands/micromamba/bin/micromamba run -n openhands poetry run pip install playwright && \
/openhands/micromamba/bin/micromamba run -n openhands poetry run playwright install --with-deps chromium
# Set environment variables and permissions
RUN /openhands/micromamba/bin/micromamba run -n openhands poetry run python -c "import sys; print('OH_INTERPRETER_PATH=' + sys.executable)" >> /etc/environment && \
chmod -R g+rws /openhands/poetry && \
mkdir -p /openhands/workspace && chmod -R g+rws,o+rw /openhands/workspace && \
# Clean up
/openhands/micromamba/bin/micromamba run -n openhands poetry cache clear --all . -n && \
mkdir -p /openhands/workspace && chmod -R g+rws,o+rw /openhands/workspace
# Clear caches
RUN /openhands/micromamba/bin/micromamba run -n openhands poetry cache clear --all . -n && \
apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* && \
/openhands/micromamba/bin/micromamba clean --all
@@ -174,8 +166,6 @@ COPY ./code/pyproject.toml ./code/poetry.lock /openhands/code/
# ================================================================
{% endif %}
{{ setup_vscode_server() }}
# ================================================================
# Copy Project source files
# ================================================================
@@ -185,7 +175,7 @@ COPY ./code/pyproject.toml ./code/poetry.lock /openhands/code/
COPY ./code/openhands /openhands/code/openhands
RUN chmod a+rwx /openhands/code/openhands/__init__.py
{{ setup_vscode_server() }}
# ================================================================
# END: Build from versioned image
@@ -22,7 +22,6 @@ from openhands.events.nested_event_store import NestedEventStore
from openhands.events.stream import EventStream
from openhands.integrations.provider import PROVIDER_TOKEN_TYPE, ProviderHandler
from openhands.llm.llm import LLM
from openhands.runtime import get_runtime_cls
from openhands.runtime.impl.docker.docker_runtime import DockerRuntime
from openhands.server.config.server_config import ServerConfig
from openhands.server.conversation_manager.conversation_manager import (
@@ -57,12 +56,12 @@ class DockerNestedConversationManager(ConversationManager):
_runtime_container_image: str | None = None
async def __aenter__(self):
runtime_cls = get_runtime_cls(self.config.runtime)
runtime_cls.setup(self.config)
# No action is required on startup for this implementation
pass
async def __aexit__(self, exc_type, exc_value, traceback):
runtime_cls = get_runtime_cls(self.config.runtime)
runtime_cls.teardown(self.config)
# No action is required on shutdown for this implementation
pass
async def attach_to_conversation(
self, sid: str, user_id: str | None = None
@@ -87,7 +86,9 @@ class DockerNestedConversationManager(ConversationManager):
async def get_running_agent_loops(
self, user_id: str | None = None, filter_to_sids: set[str] | None = None
) -> set[str]:
"""Get the running agent loops directly from docker."""
"""
Get the running agent loops directly from docker.
"""
containers: list[Container] = self.docker_client.containers.list()
names = (container.name or '' for container in containers)
conversation_ids = {
@@ -379,10 +380,8 @@ class DockerNestedConversationManager(ConversationManager):
def get_agent_session(self, sid: str):
"""Get the agent session for a given session ID.
Args:
sid: The session ID.
Returns:
The agent session, or None if not found.
"""
@@ -498,7 +497,6 @@ class DockerNestedConversationManager(ConversationManager):
env_vars['SESSION_API_KEY'] = self._get_session_api_key_for_conversation(sid)
# We need to be able to specify the nested conversation id within the nested runtime
env_vars['ALLOW_SET_CONVERSATION_ID'] = '1'
env_vars['WORKSPACE_BASE'] = '/workspace'
env_vars['SANDBOX_CLOSE_DELAY'] = '0'
env_vars['SKIP_DEPENDENCY_CHECK'] = '1'
env_vars['INITIAL_NUM_WARM_SERVERS'] = '1'
@@ -12,7 +12,6 @@ from openhands.core.logger import openhands_logger as logger
from openhands.core.schema.agent import AgentState
from openhands.events.action import MessageAction
from openhands.events.stream import EventStreamSubscriber, session_exists
from openhands.runtime import get_runtime_cls
from openhands.server.config.server_config import ServerConfig
from openhands.server.data_models.agent_loop_info import AgentLoopInfo
from openhands.server.monitoring import MonitoringListener
@@ -73,14 +72,12 @@ class StandaloneConversationManager(ConversationManager):
# Grab a reference to the main event loop. This is the loop in which `await sio.emit` must be called
self._loop = asyncio.get_event_loop()
self._cleanup_task = asyncio.create_task(self._cleanup_stale())
get_runtime_cls(self.config.runtime).setup(self.config)
return self
async def __aexit__(self, exc_type, exc_value, traceback):
if self._cleanup_task:
self._cleanup_task.cancel()
self._cleanup_task = None
get_runtime_cls(self.config.runtime).teardown(self.config)
async def attach_to_conversation(
self, sid: str, user_id: str | None = None
+1 -2
View File
@@ -54,8 +54,7 @@ def refresh_files() -> list[str]:
@app.get('/api/options/config')
def get_config() -> dict[str, str]:
# return {'APP_MODE': 'oss'}
return {'APP_MODE': 'saas'}
return {'APP_MODE': 'oss'}
@app.get('/api/options/security-analyzers')
+8 -21
View File
@@ -4,17 +4,14 @@ from pydantic import BaseModel
from openhands.core.logger import openhands_logger as logger
from openhands.events.event_filter import EventFilter
from openhands.events.event_store import EventStore
from openhands.events.serialization.event import event_to_dict
from openhands.memory.memory import Memory
from openhands.microagent.types import InputMetadata
from openhands.runtime.base import Runtime
from openhands.server.dependencies import get_dependencies
from openhands.server.session.conversation import ServerConversation
from openhands.server.shared import conversation_manager, file_store
from openhands.server.user_auth import get_user_id
from openhands.server.utils import get_conversation, get_conversation_metadata
from openhands.storage.data_models.conversation_metadata import ConversationMetadata
from openhands.server.shared import conversation_manager
from openhands.server.utils import get_conversation
app = APIRouter(
prefix='/api/conversations/{conversation_id}', dependencies=get_dependencies()
@@ -104,31 +101,27 @@ async def get_hosts(
@app.get('/events')
async def search_events(
conversation_id: str,
start_id: int = 0,
end_id: int | None = None,
reverse: bool = False,
filter: EventFilter | None = None,
limit: int = 20,
metadata: ConversationMetadata = Depends(get_conversation_metadata),
user_id: str | None = Depends(get_user_id),
conversation: ServerConversation = Depends(get_conversation),
):
"""Search through the event stream with filtering and pagination.
Args:
conversation_id: The conversation ID
request: The incoming request object
start_id: Starting ID in the event stream. Defaults to 0
end_id: Ending ID in the event stream
reverse: Whether to retrieve events in reverse order. Defaults to False.
filter: Filter for events
limit: Maximum number of events to return. Must be between 1 and 100. Defaults to 20
metadata: Conversation metadata (injected by dependency)
user_id: User ID (injected by dependency)
Returns:
dict: Dictionary containing:
- events: List of matching events
- has_more: Whether there are more matching events after this batch
Raises:
HTTPException: If conversation is not found or access is denied
HTTPException: If conversation is not found
ValueError: If limit is less than 1 or greater than 100
"""
if limit < 0 or limit > 100:
@@ -136,16 +129,10 @@ async def search_events(
status_code=status.HTTP_400_BAD_REQUEST, detail='Invalid limit'
)
# Create an event store to access the events directly
event_store = EventStore(
sid=conversation_id,
file_store=file_store,
user_id=user_id,
)
# Get matching events from the store
# Get matching events from the stream
event_stream = conversation.event_stream
events = list(
event_store.search_events(
event_stream.search_events(
start_id=start_id,
end_id=end_id,
reverse=reverse,
@@ -554,122 +554,3 @@ def _get_contextual_events(event_stream: EventStream, event_id: int) -> str:
all_events = itertools.chain(ordered_context_before, context_after)
stringified_events = '\n'.join(str(event) for event in all_events)
return stringified_events
class UpdateConversationRequest(BaseModel):
"""Request model for updating conversation metadata."""
title: str = Field(
..., min_length=1, max_length=200, description='New conversation title'
)
model_config = ConfigDict(extra='forbid')
@app.patch('/conversations/{conversation_id}')
async def update_conversation(
conversation_id: str,
data: UpdateConversationRequest,
user_id: str | None = Depends(get_user_id),
conversation_store: ConversationStore = Depends(get_conversation_store),
) -> bool:
"""Update conversation metadata.
This endpoint allows updating conversation details like title.
Only the conversation owner can update the conversation.
Args:
conversation_id: The ID of the conversation to update
data: The conversation update data (title, etc.)
user_id: The authenticated user ID
conversation_store: The conversation store dependency
Returns:
bool: True if the conversation was updated successfully
Raises:
HTTPException: If conversation is not found or user lacks permission
"""
logger.info(
f'Updating conversation {conversation_id} with title: {data.title}',
extra={'session_id': conversation_id, 'user_id': user_id},
)
try:
# Get the existing conversation metadata
metadata = await conversation_store.get_metadata(conversation_id)
# Validate that the user owns this conversation
if user_id and metadata.user_id != user_id:
logger.warning(
f'User {user_id} attempted to update conversation {conversation_id} owned by {metadata.user_id}',
extra={'session_id': conversation_id, 'user_id': user_id},
)
return JSONResponse(
content={
'status': 'error',
'message': 'Permission denied: You can only update your own conversations',
'msg_id': 'AUTHORIZATION$PERMISSION_DENIED',
},
status_code=status.HTTP_403_FORBIDDEN,
)
# Update the conversation metadata
original_title = metadata.title
metadata.title = data.title.strip()
metadata.last_updated_at = datetime.now(timezone.utc)
# Save the updated metadata
await conversation_store.save_metadata(metadata)
# Emit a status update to connected clients about the title change
try:
status_update_dict = {
'status_update': True,
'type': 'info',
'message': conversation_id,
'conversation_title': metadata.title,
}
await conversation_manager.sio.emit(
'oh_event',
status_update_dict,
to=f'room:{conversation_id}',
)
except Exception as e:
logger.error(f'Error emitting title update event: {e}')
# Don't fail the update if we can't emit the event
logger.info(
f'Successfully updated conversation {conversation_id} title from "{original_title}" to "{metadata.title}"',
extra={'session_id': conversation_id, 'user_id': user_id},
)
return True
except FileNotFoundError:
logger.warning(
f'Conversation {conversation_id} not found for update',
extra={'session_id': conversation_id, 'user_id': user_id},
)
return JSONResponse(
content={
'status': 'error',
'message': 'Conversation not found',
'msg_id': 'CONVERSATION$NOT_FOUND',
},
status_code=status.HTTP_404_NOT_FOUND,
)
except Exception as e:
logger.error(
f'Error updating conversation {conversation_id}: {str(e)}',
extra={'session_id': conversation_id, 'user_id': user_id},
)
return JSONResponse(
content={
'status': 'error',
'message': f'Failed to update conversation: {str(e)}',
'msg_id': 'CONVERSATION$UPDATE_ERROR',
},
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
+1 -21
View File
@@ -3,14 +3,9 @@ import uuid
from fastapi import Depends, HTTPException, Request, status
from openhands.core.logger import openhands_logger as logger
from openhands.server.shared import (
ConversationStoreImpl,
config,
conversation_manager,
)
from openhands.server.shared import ConversationStoreImpl, config, conversation_manager
from openhands.server.user_auth import get_user_id
from openhands.storage.conversation.conversation_store import ConversationStore
from openhands.storage.data_models.conversation_metadata import ConversationMetadata
async def get_conversation_store(request: Request) -> ConversationStore | None:
@@ -34,21 +29,6 @@ async def generate_unique_conversation_id(
return conversation_id
async def get_conversation_metadata(
conversation_id: str,
conversation_store: ConversationStore = Depends(get_conversation_store),
) -> ConversationMetadata:
"""Get conversation metadata and validate user access without requiring an active conversation."""
try:
metadata = await conversation_store.get_metadata(conversation_id)
return metadata
except FileNotFoundError:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f'Conversation {conversation_id} not found',
)
async def get_conversation(
conversation_id: str, user_id: str | None = Depends(get_user_id)
):
-12
View File
@@ -53,16 +53,4 @@ def get_supported_llm_models(config: OpenHandsConfig) -> list[str]:
except httpx.HTTPError as e:
logger.error(f'Error getting OLLAMA models: {e}')
# Add OpenHands provider models
openhands_models = [
'openhands/claude-sonnet-4-20250514',
'openhands/claude-opus-4-20250514',
'openhands/gemini-2.5-pro',
'openhands/o4-mini',
'openhands/devstral-small-2505',
'openhands/devstral-small-2507',
'openhands/devstral-medium-2507',
]
model_list = openhands_models + model_list
return list(sorted(set(model_list)))
Generated
+34 -28
View File
@@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 2.1.2 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 = "aioboto3"
@@ -2003,40 +2003,48 @@ vision = ["Pillow (>=9.4.0)"]
[[package]]
name = "daytona"
version = "0.22.0"
version = "0.21.1"
description = "Python SDK for Daytona"
optional = true
python-versions = "<4.0,>=3.8"
python-versions = ">=3.7"
groups = ["main"]
markers = "extra == \"third-party-runtimes\""
files = [
{file = "daytona-0.22.0-py3-none-any.whl", hash = "sha256:9aea877cbbdbbde04db3b28586f76f1dfc083062a0d6f19d7984d55d883aac3f"},
{file = "daytona-0.22.0.tar.gz", hash = "sha256:181ee406922177987e23fa7149287175442f204af59e4b0a136da12f41f4f142"},
{file = "daytona-0.21.1-py3-none-any.whl", hash = "sha256:1ce6b352f52ef92e667098b7bdaa60c22ffbfb8e686a8cbd12418bf7698ac834"},
{file = "daytona-0.21.1.tar.gz", hash = "sha256:01d83dd2b627f87e82491fb97f41845768d75c33f0767eaa44f6e8378bd58e60"},
]
[package.dependencies]
aioboto3 = ">=13.0.0,<15.0.0"
aioboto3 = ">=14.0.0,<15.0.0"
aiofiles = ">=24.1.0,<24.2.0"
aiohttp = ">=3.12.0,<4.0.0"
aiohttp_retry = ">=2.9.0,<3.0.0"
boto3 = ">=1.0.0,<2.0.0"
daytona-api-client = "0.22.0"
daytona-api-client-async = "0.22.0"
daytona_api_client = ">=0.21.0,<0.22.0"
daytona_api_client_async = ">=0.21.0,<0.22.0"
Deprecated = ">=1.2.18,<2.0.0"
environs = ">=10.0.0,<15.0.0"
environs = ">=9.5.0,<10.0.0"
httpx = ">=0.28.0,<0.29.0"
marshmallow = ">=3.19.0,<4.0.0"
pydantic = ">=2.4.2,<3.0.0"
python-dateutil = ">=2.8.2,<3.0.0"
toml = ">=0.10.0,<0.11.0"
urllib3 = ">=2.0.7,<3.0.0"
[package.extras]
dev = ["black[jupyter] (>=23.1.0,<24.0.0)", "build (>=1.0.3)", "isort (>=5.10.0,<6.0.0)", "matplotlib (>=3.10.0,<3.11.0)", "nbqa (>=1.9.1,<2.0.0)", "pydoc-markdown (>=4.8.2)", "pylint (>=3.3.4,<4.0.0)", "setuptools (>=68.0.0)", "twine (>=4.0.2)", "unasync (>=0.6.0,<0.7.0)", "wheel (>=0.41.2)"]
[[package]]
name = "daytona-api-client"
version = "0.22.0"
version = "0.21.0"
description = "Daytona"
optional = true
python-versions = "<4.0,>=3.8"
python-versions = "*"
groups = ["main"]
markers = "extra == \"third-party-runtimes\""
files = [
{file = "daytona_api_client-0.22.0-py3-none-any.whl", hash = "sha256:328362d54ed846a11eefe360c9428bfb52afd070cec26098978012ea72aa798d"},
{file = "daytona_api_client-0.22.0.tar.gz", hash = "sha256:350200142e46450d06dcadc81a1ae3aa0616f92d17f2a4516c4f9461d09ed679"},
{file = "daytona_api_client-0.21.0-py3-none-any.whl", hash = "sha256:a8ff1f0fb397368dbd6ddb224c28d679e599c657eab2ec5821cf0c972a60229a"},
{file = "daytona_api_client-0.21.0.tar.gz", hash = "sha256:92d591c5a1750a827b5850425ce483441609b72b05d35a618d5353fbbba50bca"},
]
[package.dependencies]
@@ -2047,15 +2055,15 @@ urllib3 = ">=1.25.3,<3.0.0"
[[package]]
name = "daytona-api-client-async"
version = "0.22.0"
version = "0.21.0"
description = "Daytona"
optional = true
python-versions = "<4.0,>=3.8"
python-versions = "*"
groups = ["main"]
markers = "extra == \"third-party-runtimes\""
files = [
{file = "daytona_api_client_async-0.22.0-py3-none-any.whl", hash = "sha256:30ddad3ba60ed0e6e727366cf979b03a56d0b9886d22828af4cd36bf686af698"},
{file = "daytona_api_client_async-0.22.0.tar.gz", hash = "sha256:f3f2c61ec8bad5f25b0532f45334d8638ae3aab3ac4a1091ca302bc8168102c3"},
{file = "daytona_api_client_async-0.21.0-py3-none-any.whl", hash = "sha256:f5731963d0dd6c1e207b92bdc7f5b59952d3365444bc9dc8b013d77a4dddf377"},
{file = "daytona_api_client_async-0.21.0.tar.gz", hash = "sha256:08a22c0d1616f82efa8d157d7be6c432554fd43d75560725c4e0cef0228607d6"},
]
[package.dependencies]
@@ -2348,25 +2356,26 @@ files = [
[[package]]
name = "environs"
version = "14.2.0"
version = "9.5.0"
description = "simplified environment variable parsing"
optional = true
python-versions = ">=3.9"
python-versions = ">=3.6"
groups = ["main"]
markers = "extra == \"third-party-runtimes\""
files = [
{file = "environs-14.2.0-py3-none-any.whl", hash = "sha256:22669a58d53c5b86a25d0231c4a41a6ebeb82d3942b8fbd9cf645890c92a1843"},
{file = "environs-14.2.0.tar.gz", hash = "sha256:2b6c78a77dfefb57ca30d43a232270ecc82adabf67ab318e018084b9a3529e9b"},
{file = "environs-9.5.0-py2.py3-none-any.whl", hash = "sha256:1e549569a3de49c05f856f40bce86979e7d5ffbbc4398e7f338574c220189124"},
{file = "environs-9.5.0.tar.gz", hash = "sha256:a76307b36fbe856bdca7ee9161e6c466fd7fcffc297109a118c59b54e27e30c9"},
]
[package.dependencies]
marshmallow = ">=3.18.0"
marshmallow = ">=3.0.0"
python-dotenv = "*"
[package.extras]
dev = ["environs[tests]", "pre-commit (>=4.0,<5.0)", "tox"]
dev = ["dj-database-url", "dj-email-url", "django-cache-url", "flake8 (==4.0.1)", "flake8-bugbear (==21.9.2)", "mypy (==0.910)", "pre-commit (>=2.4,<3.0)", "pytest", "tox"]
django = ["dj-database-url", "dj-email-url", "django-cache-url"]
tests = ["backports.strenum ; python_version < \"3.11\"", "environs[django]", "packaging", "pytest"]
lint = ["flake8 (==4.0.1)", "flake8-bugbear (==21.9.2)", "mypy (==0.910)", "pre-commit (>=2.4,<3.0)"]
tests = ["dj-database-url", "dj-email-url", "django-cache-url", "pytest"]
[[package]]
name = "et-xmlfile"
@@ -5193,11 +5202,8 @@ files = [
{file = "lxml-5.4.0-cp36-cp36m-win_amd64.whl", hash = "sha256:7ce1a171ec325192c6a636b64c94418e71a1964f56d002cc28122fceff0b6121"},
{file = "lxml-5.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:795f61bcaf8770e1b37eec24edf9771b307df3af74d1d6f27d812e15a9ff3872"},
{file = "lxml-5.4.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:29f451a4b614a7b5b6c2e043d7b64a15bd8304d7e767055e8ab68387a8cacf4e"},
{file = "lxml-5.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:891f7f991a68d20c75cb13c5c9142b2a3f9eb161f1f12a9489c82172d1f133c0"},
{file = "lxml-5.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4aa412a82e460571fad592d0f93ce9935a20090029ba08eca05c614f99b0cc92"},
{file = "lxml-5.4.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:ac7ba71f9561cd7d7b55e1ea5511543c0282e2b6450f122672a2694621d63b7e"},
{file = "lxml-5.4.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:c5d32f5284012deaccd37da1e2cd42f081feaa76981f0eaa474351b68df813c5"},
{file = "lxml-5.4.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:ce31158630a6ac85bddd6b830cffd46085ff90498b397bd0a259f59d27a12188"},
{file = "lxml-5.4.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:31e63621e073e04697c1b2d23fcb89991790eef370ec37ce4d5d469f40924ed6"},
{file = "lxml-5.4.0-cp37-cp37m-win32.whl", hash = "sha256:be2ba4c3c5b7900246a8f866580700ef0d538f2ca32535e991027bdaba944063"},
{file = "lxml-5.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:09846782b1ef650b321484ad429217f5154da4d6e786636c38e434fa32e94e49"},
@@ -11790,4 +11796,4 @@ third-party-runtimes = ["daytona", "e2b", "modal", "runloop-api-client"]
[metadata]
lock-version = "2.1"
python-versions = "^3.12,<3.14"
content-hash = "8af146b6bbc131f9f4d8d6c5098865cc4f4aadaeb0158b97665b86295a246d5c"
content-hash = "02fd5b48daa903d386eedc989d7173f4c78a3e1a101017e619f931b7a3515f2a"
+2 -3
View File
@@ -6,7 +6,7 @@ requires = [
[tool.poetry]
name = "openhands-ai"
version = "0.49.0"
version = "0.48.0"
description = "OpenHands: Code Less, Make More"
authors = [ "OpenHands" ]
license = "MIT"
@@ -72,7 +72,6 @@ anyio = "4.9.0"
pythonnet = "*"
fastmcp = "^2.5.2"
python-frontmatter = "^1.1.0"
shellingham = "^1.5.4"
# TODO: Should these go into the runtime group?
ipywidgets = "^8.1.5"
qtconsole = "^5.6.1"
@@ -99,7 +98,7 @@ jupyter_kernel_gateway = "*"
e2b = { version = ">=1.0.5,<1.6.0", optional = true }
modal = { version = ">=0.66.26,<1.1.0", optional = true }
runloop-api-client = { version = "0.43.0", optional = true }
daytona = { version = "0.22.0", optional = true }
daytona = { version = "0.21.1", optional = true }
[tool.poetry.extras]
third_party_runtimes = [ "e2b", "modal", "runloop-api-client", "daytona" ]
+68 -108
View File
@@ -142,24 +142,16 @@ def test_extension_detection_partial_match_ignored(mock_env_and_dependencies):
),
subprocess.CompletedProcess(
returncode=0, args=[], stdout='', stderr=''
), # Bundled install succeeds
), # GitHub install succeeds
]
mock_env_and_dependencies['download'].return_value = '/fake/path/to/github.vsix'
# Mock bundled VSIX to succeed
mock_vsix_path = mock.MagicMock()
mock_vsix_path.exists.return_value = True
mock_vsix_path.__str__.return_value = '/fake/path/to/bundled.vsix'
mock_env_and_dependencies[
'as_file'
].return_value.__enter__.return_value = mock_vsix_path
vscode_extension.attempt_vscode_extension_install()
with mock.patch('os.remove'), mock.patch('os.path.exists', return_value=True):
vscode_extension.attempt_vscode_extension_install()
# Should proceed with installation since exact match not found
assert mock_env_and_dependencies['subprocess'].call_count == 2
mock_env_and_dependencies['as_file'].assert_called_once()
# GitHub download should not be attempted since bundled install succeeds
mock_env_and_dependencies['download'].assert_not_called()
mock_env_and_dependencies['download'].assert_called_once()
def test_list_extensions_fails_continues_installation(mock_env_and_dependencies):
@@ -167,31 +159,23 @@ def test_list_extensions_fails_continues_installation(mock_env_and_dependencies)
os.environ['TERM_PROGRAM'] = 'vscode'
mock_env_and_dependencies['exists'].return_value = False
# --list-extensions fails, but bundled install succeeds
# --list-extensions fails, but GitHub install succeeds
mock_env_and_dependencies['subprocess'].side_effect = [
subprocess.CompletedProcess(
returncode=1, args=[], stdout='', stderr='Command failed'
),
subprocess.CompletedProcess(
returncode=0, args=[], stdout='', stderr=''
), # Bundled install succeeds
), # GitHub install succeeds
]
mock_env_and_dependencies['download'].return_value = '/fake/path/to/github.vsix'
# Mock bundled VSIX to succeed
mock_vsix_path = mock.MagicMock()
mock_vsix_path.exists.return_value = True
mock_vsix_path.__str__.return_value = '/fake/path/to/bundled.vsix'
mock_env_and_dependencies[
'as_file'
].return_value.__enter__.return_value = mock_vsix_path
vscode_extension.attempt_vscode_extension_install()
with mock.patch('os.remove'), mock.patch('os.path.exists', return_value=True):
vscode_extension.attempt_vscode_extension_install()
# Should proceed with installation
assert mock_env_and_dependencies['subprocess'].call_count == 2
mock_env_and_dependencies['as_file'].assert_called_once()
# GitHub download should not be attempted since bundled install succeeds
mock_env_and_dependencies['download'].assert_not_called()
mock_env_and_dependencies['download'].assert_called_once()
def test_list_extensions_exception_continues_installation(mock_env_and_dependencies):
@@ -199,60 +183,43 @@ def test_list_extensions_exception_continues_installation(mock_env_and_dependenc
os.environ['TERM_PROGRAM'] = 'vscode'
mock_env_and_dependencies['exists'].return_value = False
# --list-extensions throws exception, but bundled install succeeds
# --list-extensions throws exception, but GitHub install succeeds
mock_env_and_dependencies['subprocess'].side_effect = [
FileNotFoundError('code command not found'),
subprocess.CompletedProcess(
returncode=0, args=[], stdout='', stderr=''
), # Bundled install succeeds
), # GitHub install succeeds
]
mock_env_and_dependencies['download'].return_value = '/fake/path/to/github.vsix'
# Mock bundled VSIX to succeed
mock_vsix_path = mock.MagicMock()
mock_vsix_path.exists.return_value = True
mock_vsix_path.__str__.return_value = '/fake/path/to/bundled.vsix'
mock_env_and_dependencies[
'as_file'
].return_value.__enter__.return_value = mock_vsix_path
vscode_extension.attempt_vscode_extension_install()
with mock.patch('os.remove'), mock.patch('os.path.exists', return_value=True):
vscode_extension.attempt_vscode_extension_install()
# Should proceed with installation
assert mock_env_and_dependencies['subprocess'].call_count == 2
mock_env_and_dependencies['as_file'].assert_called_once()
# GitHub download should not be attempted since bundled install succeeds
mock_env_and_dependencies['download'].assert_not_called()
mock_env_and_dependencies['download'].assert_called_once()
def test_mark_installation_successful_os_error(mock_env_and_dependencies):
"""Should log error but continue if flag file creation fails."""
os.environ['TERM_PROGRAM'] = 'vscode'
mock_env_and_dependencies['exists'].return_value = False
# Mock bundled VSIX to succeed
mock_vsix_path = mock.MagicMock()
mock_vsix_path.exists.return_value = True
mock_vsix_path.__str__.return_value = '/fake/path/to/bundled.vsix'
mock_env_and_dependencies[
'as_file'
].return_value.__enter__.return_value = mock_vsix_path
mock_env_and_dependencies['subprocess'].side_effect = [
subprocess.CompletedProcess(
returncode=0, args=[], stdout='', stderr=''
), # --list-extensions (empty)
subprocess.CompletedProcess(
returncode=0, args=[], stdout='', stderr=''
), # Bundled install succeeds
), # GitHub install succeeds
]
mock_env_and_dependencies['download'].return_value = '/fake/path/to/github.vsix'
mock_env_and_dependencies['touch'].side_effect = OSError('Permission denied')
vscode_extension.attempt_vscode_extension_install()
with mock.patch('os.remove'), mock.patch('os.path.exists', return_value=True):
vscode_extension.attempt_vscode_extension_install()
# Should still complete installation
mock_env_and_dependencies['as_file'].assert_called_once()
# GitHub download should not be attempted since bundled install succeeds
mock_env_and_dependencies['download'].assert_not_called()
mock_env_and_dependencies['download'].assert_called_once()
mock_env_and_dependencies['touch'].assert_called_once()
# Should log the error
mock_env_and_dependencies['logger'].assert_any_call(
@@ -284,62 +251,12 @@ def test_installation_failure_no_flag_created(mock_env_and_dependencies):
)
def test_install_succeeds_from_bundled(mock_env_and_dependencies):
"""Should successfully install from bundled VSIX on the first try."""
os.environ['TERM_PROGRAM'] = 'vscode'
mock_env_and_dependencies['exists'].return_value = False
mock_vsix_path = mock.MagicMock()
mock_vsix_path.exists.return_value = True
mock_vsix_path.__str__.return_value = '/fake/path/to/bundled.vsix'
mock_env_and_dependencies[
'as_file'
].return_value.__enter__.return_value = mock_vsix_path
# Mock subprocess calls: first --list-extensions (returns empty), then install
mock_env_and_dependencies['subprocess'].side_effect = [
subprocess.CompletedProcess(
returncode=0, args=[], stdout='', stderr=''
), # --list-extensions
subprocess.CompletedProcess(
returncode=0, args=[], stdout='', stderr=''
), # --install-extension
]
vscode_extension.attempt_vscode_extension_install()
mock_env_and_dependencies['as_file'].assert_called_once()
# Should have two subprocess calls: list-extensions and install-extension
assert mock_env_and_dependencies['subprocess'].call_count == 2
mock_env_and_dependencies['subprocess'].assert_any_call(
['code', '--list-extensions'],
capture_output=True,
text=True,
check=False,
)
mock_env_and_dependencies['subprocess'].assert_any_call(
['code', '--install-extension', '/fake/path/to/bundled.vsix', '--force'],
capture_output=True,
text=True,
check=False,
)
mock_env_and_dependencies['print'].assert_any_call(
'INFO: Bundled VS Code extension installed successfully.'
)
mock_env_and_dependencies['touch'].assert_called_once()
# GitHub download should not be attempted
mock_env_and_dependencies['download'].assert_not_called()
def test_bundled_fails_falls_back_to_github(mock_env_and_dependencies):
"""Should fall back to GitHub if bundled VSIX installation fails."""
def test_install_succeeds_from_github(mock_env_and_dependencies):
"""Should successfully install from GitHub on the first try."""
os.environ['TERM_PROGRAM'] = 'vscode'
mock_env_and_dependencies['exists'].return_value = False
mock_env_and_dependencies['download'].return_value = '/fake/path/to/github.vsix'
# Mock bundled VSIX to fail
mock_env_and_dependencies['as_file'].side_effect = FileNotFoundError
# Mock subprocess calls: first --list-extensions (returns empty), then install
mock_env_and_dependencies['subprocess'].side_effect = [
subprocess.CompletedProcess(
@@ -356,7 +273,6 @@ def test_bundled_fails_falls_back_to_github(mock_env_and_dependencies):
):
vscode_extension.attempt_vscode_extension_install()
mock_env_and_dependencies['as_file'].assert_called_once()
mock_env_and_dependencies['download'].assert_called_once()
# Should have two subprocess calls: list-extensions and install-extension
assert mock_env_and_dependencies['subprocess'].call_count == 2
@@ -379,6 +295,50 @@ def test_bundled_fails_falls_back_to_github(mock_env_and_dependencies):
mock_env_and_dependencies['touch'].assert_called_once()
def test_github_fails_falls_back_to_bundled(mock_env_and_dependencies):
"""Should fall back to bundled VSIX if GitHub download fails."""
os.environ['TERM_PROGRAM'] = 'vscode'
mock_env_and_dependencies['exists'].return_value = False
mock_env_and_dependencies['download'].return_value = None
mock_vsix_path = mock.MagicMock()
mock_vsix_path.exists.return_value = True
mock_vsix_path.__str__.return_value = '/fake/path/to/bundled.vsix'
mock_env_and_dependencies[
'as_file'
].return_value.__enter__.return_value = mock_vsix_path
# Mock subprocess calls: first --list-extensions (returns empty), then install
mock_env_and_dependencies['subprocess'].side_effect = [
subprocess.CompletedProcess(
returncode=0, args=[], stdout='', stderr=''
), # --list-extensions
subprocess.CompletedProcess(
returncode=0, args=[], stdout='', stderr=''
), # --install-extension
]
vscode_extension.attempt_vscode_extension_install()
mock_env_and_dependencies['download'].assert_called_once()
mock_env_and_dependencies['as_file'].assert_called_once()
# Should have two subprocess calls: list-extensions and install-extension
assert mock_env_and_dependencies['subprocess'].call_count == 2
mock_env_and_dependencies['subprocess'].assert_any_call(
['code', '--list-extensions'],
capture_output=True,
text=True,
check=False,
)
mock_env_and_dependencies['subprocess'].assert_any_call(
['code', '--install-extension', '/fake/path/to/bundled.vsix', '--force'],
capture_output=True,
text=True,
check=False,
)
mock_env_and_dependencies['touch'].assert_called_once()
def test_all_methods_fail(mock_env_and_dependencies):
"""Should show a final failure message if all installation methods fail."""
os.environ['TERM_PROGRAM'] = 'vscode'
-30
View File
@@ -334,9 +334,7 @@ async def test_run_session_with_initial_action(
@patch('openhands.cli.main.LLMSummarizingCondenserConfig')
@patch('openhands.cli.main.NoOpCondenserConfig')
@patch('openhands.cli.main.finalize_config')
@patch('openhands.cli.main.aliases_exist_in_shell_config')
async def test_main_without_task(
mock_aliases_exist,
mock_finalize_config,
mock_noop_condenser,
mock_llm_condenser,
@@ -350,9 +348,6 @@ async def test_main_without_task(
"""Test main function without a task."""
loop = asyncio.get_running_loop()
# Mock alias setup functions to prevent the alias setup flow
mock_aliases_exist.return_value = True
# Mock arguments
mock_args = MagicMock()
mock_args.agent_cls = None
@@ -425,9 +420,7 @@ async def test_main_without_task(
@patch('openhands.cli.main.LLMSummarizingCondenserConfig')
@patch('openhands.cli.main.NoOpCondenserConfig')
@patch('openhands.cli.main.finalize_config')
@patch('openhands.cli.main.aliases_exist_in_shell_config')
async def test_main_with_task(
mock_aliases_exist,
mock_finalize_config,
mock_noop_condenser,
mock_llm_condenser,
@@ -441,9 +434,6 @@ async def test_main_with_task(
"""Test main function with a task."""
loop = asyncio.get_running_loop()
# Mock alias setup functions to prevent the alias setup flow
mock_aliases_exist.return_value = True
# Mock arguments
mock_args = MagicMock()
mock_args.agent_cls = 'custom-agent'
@@ -527,9 +517,7 @@ async def test_main_with_task(
@patch('openhands.cli.main.LLMSummarizingCondenserConfig')
@patch('openhands.cli.main.NoOpCondenserConfig')
@patch('openhands.cli.main.finalize_config')
@patch('openhands.cli.main.aliases_exist_in_shell_config')
async def test_main_with_session_name_passes_name_to_run_session(
mock_aliases_exist,
mock_finalize_config,
mock_noop_condenser,
mock_llm_condenser,
@@ -544,9 +532,6 @@ async def test_main_with_session_name_passes_name_to_run_session(
loop = asyncio.get_running_loop()
test_session_name = 'my_named_session'
# Mock alias setup functions to prevent the alias setup flow
mock_aliases_exist.return_value = True
# Mock arguments
mock_args = MagicMock()
mock_args.agent_cls = None
@@ -718,9 +703,7 @@ async def test_run_session_with_name_attempts_state_restore(
@patch('openhands.cli.main.LLMSummarizingCondenserConfig')
@patch('openhands.cli.main.NoOpCondenserConfig')
@patch('openhands.cli.main.finalize_config')
@patch('openhands.cli.main.aliases_exist_in_shell_config')
async def test_main_security_check_fails(
mock_aliases_exist,
mock_finalize_config,
mock_noop_condenser,
mock_llm_condenser,
@@ -734,9 +717,6 @@ async def test_main_security_check_fails(
"""Test main function when security check fails."""
loop = asyncio.get_running_loop()
# Mock alias setup functions to prevent the alias setup flow
mock_aliases_exist.return_value = True
# Mock arguments
mock_args = MagicMock()
mock_parse_args.return_value = mock_args
@@ -784,9 +764,7 @@ async def test_main_security_check_fails(
@patch('openhands.cli.main.LLMSummarizingCondenserConfig')
@patch('openhands.cli.main.NoOpCondenserConfig')
@patch('openhands.cli.main.finalize_config')
@patch('openhands.cli.main.aliases_exist_in_shell_config')
async def test_config_loading_order(
mock_aliases_exist,
mock_finalize_config,
mock_noop_condenser,
mock_llm_condenser,
@@ -806,9 +784,6 @@ async def test_config_loading_order(
"""
loop = asyncio.get_running_loop()
# Mock alias setup functions to prevent the alias setup flow
mock_aliases_exist.return_value = True
# Mock arguments with specific agent but no LLM config
mock_args = MagicMock()
mock_args.agent_cls = 'cmd-line-agent' # This should override settings
@@ -894,11 +869,9 @@ async def test_config_loading_order(
@patch('openhands.cli.main.LLMSummarizingCondenserConfig')
@patch('openhands.cli.main.NoOpCondenserConfig')
@patch('openhands.cli.main.finalize_config')
@patch('openhands.cli.main.aliases_exist_in_shell_config')
@patch('builtins.open', new_callable=MagicMock)
async def test_main_with_file_option(
mock_open,
mock_aliases_exist,
mock_finalize_config,
mock_noop_condenser,
mock_llm_condenser,
@@ -912,9 +885,6 @@ async def test_main_with_file_option(
"""Test main function with a file option."""
loop = asyncio.get_running_loop()
# Mock alias setup functions to prevent the alias setup flow
mock_aliases_exist.return_value = True
# Mock arguments
mock_args = MagicMock()
mock_args.agent_cls = None
-246
View File
@@ -1,246 +0,0 @@
"""Unit tests for CLI alias setup functionality."""
import tempfile
from pathlib import Path
from unittest.mock import patch
from openhands.cli.shell_config import (
ShellConfigManager,
add_aliases_to_shell_config,
aliases_exist_in_shell_config,
get_shell_config_path,
)
def test_get_shell_config_path_no_files_fallback():
"""Test shell config path fallback when no shell detection and no config files exist."""
with tempfile.TemporaryDirectory() as temp_dir:
with patch('openhands.cli.shell_config.Path.home', return_value=Path(temp_dir)):
# Mock shellingham to raise an exception (detection failure)
with patch(
'shellingham.detect_shell',
side_effect=Exception('Shell detection failed'),
):
profile_path = get_shell_config_path()
assert profile_path.name == '.bash_profile'
def test_get_shell_config_path_bash_fallback():
"""Test shell config path fallback to bash when it exists."""
with tempfile.TemporaryDirectory() as temp_dir:
with patch('openhands.cli.shell_config.Path.home', return_value=Path(temp_dir)):
# Create .bashrc
bashrc = Path(temp_dir) / '.bashrc'
bashrc.touch()
# Mock shellingham to raise an exception (detection failure)
with patch(
'shellingham.detect_shell',
side_effect=Exception('Shell detection failed'),
):
profile_path = get_shell_config_path()
assert profile_path.name == '.bashrc'
def test_get_shell_config_path_with_bash_detection():
"""Test shell config path when bash is detected."""
with tempfile.TemporaryDirectory() as temp_dir:
with patch('openhands.cli.shell_config.Path.home', return_value=Path(temp_dir)):
# Create .bashrc
bashrc = Path(temp_dir) / '.bashrc'
bashrc.touch()
# Mock shellingham to return bash
with patch('shellingham.detect_shell', return_value=('bash', 'bash')):
profile_path = get_shell_config_path()
assert profile_path.name == '.bashrc'
def test_get_shell_config_path_with_zsh_detection():
"""Test shell config path when zsh is detected."""
with tempfile.TemporaryDirectory() as temp_dir:
with patch('openhands.cli.shell_config.Path.home', return_value=Path(temp_dir)):
# Create .zshrc
zshrc = Path(temp_dir) / '.zshrc'
zshrc.touch()
# Mock shellingham to return zsh
with patch('shellingham.detect_shell', return_value=('zsh', 'zsh')):
profile_path = get_shell_config_path()
assert profile_path.name == '.zshrc'
def test_get_shell_config_path_with_fish_detection():
"""Test shell config path when fish is detected."""
with tempfile.TemporaryDirectory() as temp_dir:
with patch('openhands.cli.shell_config.Path.home', return_value=Path(temp_dir)):
# Create fish config directory and file
fish_config_dir = Path(temp_dir) / '.config' / 'fish'
fish_config_dir.mkdir(parents=True)
fish_config = fish_config_dir / 'config.fish'
fish_config.touch()
# Mock shellingham to return fish
with patch('shellingham.detect_shell', return_value=('fish', 'fish')):
profile_path = get_shell_config_path()
assert profile_path.name == 'config.fish'
assert 'fish' in str(profile_path)
def test_add_aliases_to_shell_config_bash():
"""Test adding aliases to bash config."""
with tempfile.TemporaryDirectory() as temp_dir:
with patch('openhands.cli.shell_config.Path.home', return_value=Path(temp_dir)):
# Mock shellingham to return bash
with patch('shellingham.detect_shell', return_value=('bash', 'bash')):
# Add aliases
success = add_aliases_to_shell_config()
assert success is True
# Get the actual path that was used
with patch('shellingham.detect_shell', return_value=('bash', 'bash')):
profile_path = get_shell_config_path()
# Check that the aliases were added
with open(profile_path, 'r') as f:
content = f.read()
assert 'alias openhands=' in content
assert 'alias oh=' in content
assert 'uvx --python 3.12 --from openhands-ai openhands' in content
def test_add_aliases_to_shell_config_zsh():
"""Test adding aliases to zsh config."""
with tempfile.TemporaryDirectory() as temp_dir:
with patch('openhands.cli.shell_config.Path.home', return_value=Path(temp_dir)):
# Mock shellingham to return zsh
with patch('shellingham.detect_shell', return_value=('zsh', 'zsh')):
# Add aliases
success = add_aliases_to_shell_config()
assert success is True
# Check that the aliases were added to .zshrc
profile_path = Path(temp_dir) / '.zshrc'
with open(profile_path, 'r') as f:
content = f.read()
assert 'alias openhands=' in content
assert 'alias oh=' in content
assert 'uvx --python 3.12 --from openhands-ai openhands' in content
def test_add_aliases_handles_existing_aliases():
"""Test that adding aliases handles existing aliases correctly."""
with tempfile.TemporaryDirectory() as temp_dir:
with patch('openhands.cli.shell_config.Path.home', return_value=Path(temp_dir)):
# Mock shellingham to return bash
with patch('shellingham.detect_shell', return_value=('bash', 'bash')):
# Add aliases first time
success = add_aliases_to_shell_config()
assert success is True
# Try adding again - should detect existing aliases
success = add_aliases_to_shell_config()
assert success is True
# Get the actual path that was used
with patch('shellingham.detect_shell', return_value=('bash', 'bash')):
profile_path = get_shell_config_path()
# Check that aliases weren't duplicated
with open(profile_path, 'r') as f:
content = f.read()
# Count occurrences of the alias
openhands_count = content.count('alias openhands=')
oh_count = content.count('alias oh=')
assert openhands_count == 1
assert oh_count == 1
def test_aliases_exist_in_shell_config_no_file():
"""Test alias detection when no shell config exists."""
with tempfile.TemporaryDirectory() as temp_dir:
with patch('openhands.cli.shell_config.Path.home', return_value=Path(temp_dir)):
# Mock shellingham to return bash
with patch('shellingham.detect_shell', return_value=('bash', 'bash')):
assert aliases_exist_in_shell_config() is False
def test_aliases_exist_in_shell_config_no_aliases():
"""Test alias detection when shell config exists but has no aliases."""
with tempfile.TemporaryDirectory() as temp_dir:
with patch('openhands.cli.shell_config.Path.home', return_value=Path(temp_dir)):
# Mock shellingham to return bash
with patch('shellingham.detect_shell', return_value=('bash', 'bash')):
# Create bash profile with other content
profile_path = get_shell_config_path()
with open(profile_path, 'w') as f:
f.write('export PATH=$PATH:/usr/local/bin\n')
assert aliases_exist_in_shell_config() is False
def test_aliases_exist_in_shell_config_with_aliases():
"""Test alias detection when aliases exist."""
with tempfile.TemporaryDirectory() as temp_dir:
with patch('openhands.cli.shell_config.Path.home', return_value=Path(temp_dir)):
# Mock shellingham to return bash
with patch('shellingham.detect_shell', return_value=('bash', 'bash')):
# Add aliases first
add_aliases_to_shell_config()
assert aliases_exist_in_shell_config() is True
def test_shell_config_manager_basic_functionality():
"""Test basic ShellConfigManager functionality."""
manager = ShellConfigManager()
# Test command customization
custom_manager = ShellConfigManager(command='custom-command')
assert custom_manager.command == 'custom-command'
# Test shell type detection from path
assert manager.get_shell_type_from_path(Path('/home/user/.bashrc')) == 'bash'
assert manager.get_shell_type_from_path(Path('/home/user/.zshrc')) == 'zsh'
assert (
manager.get_shell_type_from_path(Path('/home/user/.config/fish/config.fish'))
== 'fish'
)
def test_shell_config_manager_reload_commands():
"""Test reload command generation."""
manager = ShellConfigManager()
# Test different shell reload commands
assert 'source ~/.zshrc' in manager.get_reload_command(Path('/home/user/.zshrc'))
assert 'source ~/.bashrc' in manager.get_reload_command(Path('/home/user/.bashrc'))
assert 'source ~/.bash_profile' in manager.get_reload_command(
Path('/home/user/.bash_profile')
)
assert 'source ~/.config/fish/config.fish' in manager.get_reload_command(
Path('/home/user/.config/fish/config.fish')
)
def test_shell_config_manager_template_rendering():
"""Test that templates are properly rendered."""
manager = ShellConfigManager(command='test-command')
with tempfile.TemporaryDirectory() as temp_dir:
with patch('openhands.cli.shell_config.Path.home', return_value=Path(temp_dir)):
# Create a bash config file
bashrc = Path(temp_dir) / '.bashrc'
bashrc.touch()
# Mock shell detection
with patch.object(manager, 'detect_shell', return_value='bash'):
success = manager.add_aliases()
assert success is True
# Check that the custom command was used
with open(bashrc, 'r') as f:
content = f.read()
assert 'test-command' in content
assert 'alias openhands="test-command"' in content
assert 'alias oh="test-command"' in content
+1 -465
View File
@@ -1,21 +1,13 @@
import json
from datetime import datetime, timezone
from unittest.mock import AsyncMock, MagicMock, patch
from unittest.mock import MagicMock, patch
import pytest
from fastapi import status
from fastapi.responses import JSONResponse
from openhands.microagent.microagent import KnowledgeMicroagent, RepoMicroagent
from openhands.microagent.types import MicroagentMetadata, MicroagentType
from openhands.server.routes.conversation import get_microagents
from openhands.server.routes.manage_conversations import (
UpdateConversationRequest,
update_conversation,
)
from openhands.server.session.conversation import ServerConversation
from openhands.storage.conversation.conversation_store import ConversationStore
from openhands.storage.data_models.conversation_metadata import ConversationMetadata
@pytest.mark.asyncio
@@ -163,459 +155,3 @@ async def test_get_microagents_exception():
content = json.loads(response.body)
assert 'error' in content
assert 'Test exception' in content['error']
@pytest.mark.update_conversation
@pytest.mark.asyncio
async def test_update_conversation_success():
"""Test successful conversation update."""
# Mock data
conversation_id = 'test_conversation_123'
user_id = 'test_user_456'
original_title = 'Original Title'
new_title = 'Updated Title'
# Create mock metadata
mock_metadata = ConversationMetadata(
conversation_id=conversation_id,
user_id=user_id,
title=original_title,
selected_repository=None,
last_updated_at=datetime.now(timezone.utc),
)
# Create mock conversation store
mock_conversation_store = MagicMock(spec=ConversationStore)
mock_conversation_store.get_metadata = AsyncMock(return_value=mock_metadata)
mock_conversation_store.save_metadata = AsyncMock()
# Create update request
update_request = UpdateConversationRequest(title=new_title)
# Mock the conversation manager socket
mock_sio = AsyncMock()
with patch(
'openhands.server.routes.manage_conversations.conversation_manager'
) as mock_manager:
mock_manager.sio = mock_sio
# Call the function
result = await update_conversation(
conversation_id=conversation_id,
data=update_request,
user_id=user_id,
conversation_store=mock_conversation_store,
)
# Verify the result
assert result is True
# Verify metadata was fetched
mock_conversation_store.get_metadata.assert_called_once_with(conversation_id)
# Verify metadata was updated and saved
mock_conversation_store.save_metadata.assert_called_once()
saved_metadata = mock_conversation_store.save_metadata.call_args[0][0]
assert saved_metadata.title == new_title.strip()
assert saved_metadata.last_updated_at is not None
# Verify socket emission
mock_sio.emit.assert_called_once()
emit_call = mock_sio.emit.call_args
assert emit_call[0][0] == 'oh_event'
assert emit_call[0][1]['conversation_title'] == new_title
assert emit_call[1]['to'] == f'room:{conversation_id}'
@pytest.mark.update_conversation
@pytest.mark.asyncio
async def test_update_conversation_not_found():
"""Test conversation update when conversation doesn't exist."""
conversation_id = 'nonexistent_conversation'
user_id = 'test_user_456'
# Create mock conversation store that raises FileNotFoundError
mock_conversation_store = MagicMock(spec=ConversationStore)
mock_conversation_store.get_metadata = AsyncMock(side_effect=FileNotFoundError())
# Create update request
update_request = UpdateConversationRequest(title='New Title')
# Call the function
result = await update_conversation(
conversation_id=conversation_id,
data=update_request,
user_id=user_id,
conversation_store=mock_conversation_store,
)
# Verify the result is a 404 error response
assert isinstance(result, JSONResponse)
assert result.status_code == status.HTTP_404_NOT_FOUND
# Parse the JSON content
content = json.loads(result.body)
assert content['status'] == 'error'
assert content['message'] == 'Conversation not found'
assert content['msg_id'] == 'CONVERSATION$NOT_FOUND'
@pytest.mark.update_conversation
@pytest.mark.asyncio
async def test_update_conversation_permission_denied():
"""Test conversation update when user doesn't own the conversation."""
conversation_id = 'test_conversation_123'
user_id = 'test_user_456'
owner_id = 'different_user_789'
# Create mock metadata owned by different user
mock_metadata = ConversationMetadata(
conversation_id=conversation_id,
user_id=owner_id,
title='Original Title',
selected_repository=None,
last_updated_at=datetime.now(timezone.utc),
)
# Create mock conversation store
mock_conversation_store = MagicMock(spec=ConversationStore)
mock_conversation_store.get_metadata = AsyncMock(return_value=mock_metadata)
# Create update request
update_request = UpdateConversationRequest(title='New Title')
# Call the function
result = await update_conversation(
conversation_id=conversation_id,
data=update_request,
user_id=user_id,
conversation_store=mock_conversation_store,
)
# Verify the result is a 403 error response
assert isinstance(result, JSONResponse)
assert result.status_code == status.HTTP_403_FORBIDDEN
# Parse the JSON content
content = json.loads(result.body)
assert content['status'] == 'error'
assert (
content['message']
== 'Permission denied: You can only update your own conversations'
)
assert content['msg_id'] == 'AUTHORIZATION$PERMISSION_DENIED'
@pytest.mark.update_conversation
@pytest.mark.asyncio
async def test_update_conversation_permission_denied_no_user_id():
"""Test conversation update when user_id is None and metadata has user_id."""
conversation_id = 'test_conversation_123'
user_id = None
owner_id = 'some_user_789'
# Create mock metadata owned by a user
mock_metadata = ConversationMetadata(
conversation_id=conversation_id,
user_id=owner_id,
title='Original Title',
selected_repository=None,
last_updated_at=datetime.now(timezone.utc),
)
# Create mock conversation store
mock_conversation_store = MagicMock(spec=ConversationStore)
mock_conversation_store.get_metadata = AsyncMock(return_value=mock_metadata)
# Create update request
update_request = UpdateConversationRequest(title='New Title')
# Call the function
result = await update_conversation(
conversation_id=conversation_id,
data=update_request,
user_id=user_id,
conversation_store=mock_conversation_store,
)
# Verify the result is successful (current logic allows this)
assert result is True
@pytest.mark.update_conversation
@pytest.mark.asyncio
async def test_update_conversation_socket_emission_error():
"""Test conversation update when socket emission fails."""
conversation_id = 'test_conversation_123'
user_id = 'test_user_456'
new_title = 'Updated Title'
# Create mock metadata
mock_metadata = ConversationMetadata(
conversation_id=conversation_id,
user_id=user_id,
title='Original Title',
selected_repository=None,
last_updated_at=datetime.now(timezone.utc),
)
# Create mock conversation store
mock_conversation_store = MagicMock(spec=ConversationStore)
mock_conversation_store.get_metadata = AsyncMock(return_value=mock_metadata)
mock_conversation_store.save_metadata = AsyncMock()
# Create update request
update_request = UpdateConversationRequest(title=new_title)
# Mock the conversation manager socket to raise an exception
mock_sio = AsyncMock()
mock_sio.emit.side_effect = Exception('Socket error')
with patch(
'openhands.server.routes.manage_conversations.conversation_manager'
) as mock_manager:
mock_manager.sio = mock_sio
# Call the function (should still succeed despite socket error)
result = await update_conversation(
conversation_id=conversation_id,
data=update_request,
user_id=user_id,
conversation_store=mock_conversation_store,
)
# Verify the result is still successful
assert result is True
# Verify metadata was still saved
mock_conversation_store.save_metadata.assert_called_once()
@pytest.mark.update_conversation
@pytest.mark.asyncio
async def test_update_conversation_general_exception():
"""Test conversation update when an unexpected exception occurs."""
conversation_id = 'test_conversation_123'
user_id = 'test_user_456'
# Create mock conversation store that raises a general exception
mock_conversation_store = MagicMock(spec=ConversationStore)
mock_conversation_store.get_metadata = AsyncMock(
side_effect=Exception('Database error')
)
# Create update request
update_request = UpdateConversationRequest(title='New Title')
# Call the function
result = await update_conversation(
conversation_id=conversation_id,
data=update_request,
user_id=user_id,
conversation_store=mock_conversation_store,
)
# Verify the result is a 500 error response
assert isinstance(result, JSONResponse)
assert result.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR
# Parse the JSON content
content = json.loads(result.body)
assert content['status'] == 'error'
assert 'Failed to update conversation' in content['message']
assert content['msg_id'] == 'CONVERSATION$UPDATE_ERROR'
@pytest.mark.update_conversation
@pytest.mark.asyncio
async def test_update_conversation_title_whitespace_trimming():
"""Test that conversation title is properly trimmed of whitespace."""
conversation_id = 'test_conversation_123'
user_id = 'test_user_456'
title_with_whitespace = ' Trimmed Title '
expected_title = 'Trimmed Title'
# Create mock metadata
mock_metadata = ConversationMetadata(
conversation_id=conversation_id,
user_id=user_id,
title='Original Title',
selected_repository=None,
last_updated_at=datetime.now(timezone.utc),
)
# Create mock conversation store
mock_conversation_store = MagicMock(spec=ConversationStore)
mock_conversation_store.get_metadata = AsyncMock(return_value=mock_metadata)
mock_conversation_store.save_metadata = AsyncMock()
# Create update request with whitespace
update_request = UpdateConversationRequest(title=title_with_whitespace)
# Mock the conversation manager socket
mock_sio = AsyncMock()
with patch(
'openhands.server.routes.manage_conversations.conversation_manager'
) as mock_manager:
mock_manager.sio = mock_sio
# Call the function
result = await update_conversation(
conversation_id=conversation_id,
data=update_request,
user_id=user_id,
conversation_store=mock_conversation_store,
)
# Verify the result
assert result is True
# Verify metadata was updated with trimmed title
mock_conversation_store.save_metadata.assert_called_once()
saved_metadata = mock_conversation_store.save_metadata.call_args[0][0]
assert saved_metadata.title == expected_title
@pytest.mark.update_conversation
@pytest.mark.asyncio
async def test_update_conversation_user_owns_conversation():
"""Test successful update when user owns the conversation."""
conversation_id = 'test_conversation_123'
user_id = 'test_user_456'
new_title = 'Updated Title'
# Create mock metadata owned by the same user
mock_metadata = ConversationMetadata(
conversation_id=conversation_id,
user_id=user_id, # Same user
title='Original Title',
selected_repository=None,
last_updated_at=datetime.now(timezone.utc),
)
# Create mock conversation store
mock_conversation_store = MagicMock(spec=ConversationStore)
mock_conversation_store.get_metadata = AsyncMock(return_value=mock_metadata)
mock_conversation_store.save_metadata = AsyncMock()
# Create update request
update_request = UpdateConversationRequest(title=new_title)
# Mock the conversation manager socket
mock_sio = AsyncMock()
with patch(
'openhands.server.routes.manage_conversations.conversation_manager'
) as mock_manager:
mock_manager.sio = mock_sio
# Call the function
result = await update_conversation(
conversation_id=conversation_id,
data=update_request,
user_id=user_id,
conversation_store=mock_conversation_store,
)
# Verify success
assert result is True
mock_conversation_store.save_metadata.assert_called_once()
@pytest.mark.update_conversation
@pytest.mark.asyncio
async def test_update_conversation_last_updated_at_set():
"""Test that last_updated_at is properly set when updating."""
conversation_id = 'test_conversation_123'
user_id = 'test_user_456'
new_title = 'Updated Title'
# Create mock metadata
original_timestamp = datetime.now(timezone.utc)
mock_metadata = ConversationMetadata(
conversation_id=conversation_id,
user_id=user_id,
title='Original Title',
selected_repository=None,
last_updated_at=original_timestamp,
)
# Create mock conversation store
mock_conversation_store = MagicMock(spec=ConversationStore)
mock_conversation_store.get_metadata = AsyncMock(return_value=mock_metadata)
mock_conversation_store.save_metadata = AsyncMock()
# Create update request
update_request = UpdateConversationRequest(title=new_title)
# Mock the conversation manager socket
mock_sio = AsyncMock()
with patch(
'openhands.server.routes.manage_conversations.conversation_manager'
) as mock_manager:
mock_manager.sio = mock_sio
# Call the function
result = await update_conversation(
conversation_id=conversation_id,
data=update_request,
user_id=user_id,
conversation_store=mock_conversation_store,
)
# Verify success
assert result is True
# Verify last_updated_at was updated
mock_conversation_store.save_metadata.assert_called_once()
saved_metadata = mock_conversation_store.save_metadata.call_args[0][0]
assert saved_metadata.last_updated_at > original_timestamp
@pytest.mark.update_conversation
@pytest.mark.asyncio
async def test_update_conversation_no_user_id_no_metadata_user_id():
"""Test successful update when both user_id and metadata.user_id are None."""
conversation_id = 'test_conversation_123'
user_id = None
new_title = 'Updated Title'
# Create mock metadata with no user_id
mock_metadata = ConversationMetadata(
conversation_id=conversation_id,
user_id=None,
title='Original Title',
selected_repository=None,
last_updated_at=datetime.now(timezone.utc),
)
# Create mock conversation store
mock_conversation_store = MagicMock(spec=ConversationStore)
mock_conversation_store.get_metadata = AsyncMock(return_value=mock_metadata)
mock_conversation_store.save_metadata = AsyncMock()
# Create update request
update_request = UpdateConversationRequest(title=new_title)
# Mock the conversation manager socket
mock_sio = AsyncMock()
with patch(
'openhands.server.routes.manage_conversations.conversation_manager'
) as mock_manager:
mock_manager.sio = mock_sio
# Call the function
result = await update_conversation(
conversation_id=conversation_id,
data=update_request,
user_id=user_id,
conversation_store=mock_conversation_store,
)
# Verify success (should work when both are None)
assert result is True
mock_conversation_store.save_metadata.assert_called_once()
-39
View File
@@ -264,45 +264,6 @@ def test_llm_init_with_openrouter_model(mock_get_model_info, default_config):
mock_get_model_info.assert_called_once_with('openrouter:gpt-4o-mini')
@patch('openhands.llm.llm.litellm_completion')
def test_stop_parameter_handling(mock_litellm_completion, default_config):
"""Test that stop parameter is only added for supported models."""
from litellm.types.utils import ModelResponse
mock_response = ModelResponse(
id='test-id',
choices=[{'message': {'content': 'Test response'}}],
model='test-model',
)
mock_litellm_completion.return_value = mock_response
# Test with a model that supports stop parameter
default_config.model = (
'custom-model' # Use a model not in FUNCTION_CALLING_SUPPORTED_MODELS
)
llm = LLM(default_config)
llm.completion(
messages=[{'role': 'user', 'content': 'Hello!'}],
tools=[
{'type': 'function', 'function': {'name': 'test', 'description': 'test'}}
],
)
# Verify stop parameter was included
assert 'stop' in mock_litellm_completion.call_args[1]
# Test with Grok-4 model that doesn't support stop parameter
default_config.model = 'xai/grok-4-0709'
llm = LLM(default_config)
llm.completion(
messages=[{'role': 'user', 'content': 'Hello!'}],
tools=[
{'type': 'function', 'function': {'name': 'test', 'description': 'test'}}
],
)
# Verify stop parameter was not included
assert 'stop' not in mock_litellm_completion.call_args[1]
# Tests involving completion and retries
+26 -3
View File
@@ -219,10 +219,33 @@ async def test_export_latest_git_provider_tokens_token_update(runtime):
@pytest.mark.asyncio
async def test_clone_or_init_repo_no_repo_init_git_in_empty_workspace(temp_dir):
"""Test that git init is run when no repository is selected and init_git_in_empty_workspace"""
async def test_clone_or_init_repo_no_repo_with_user_id(temp_dir):
"""Test that git init is run when no repository is selected and user_id is set"""
config = OpenHandsConfig()
config.init_git_in_empty_workspace = True
file_store = get_file_store('local', temp_dir)
event_stream = EventStream('abc', file_store)
runtime = TestRuntime(
config=config, event_stream=event_stream, sid='test', user_id='test_user'
)
# Call the function with no repository
result = await runtime.clone_or_init_repo(None, None, None)
# Verify that git init was called
assert len(runtime.run_action_calls) == 1
assert isinstance(runtime.run_action_calls[0], CmdRunAction)
assert (
runtime.run_action_calls[0].command
== f'git init && git config --global --add safe.directory {runtime.workspace_root}'
)
assert result == ''
@pytest.mark.asyncio
async def test_clone_or_init_repo_no_repo_no_user_id_no_workspace_base(temp_dir):
"""Test that git init is run when no repository is selected, no user_id, and no workspace_base"""
config = OpenHandsConfig()
config.workspace_base = None # Ensure workspace_base is not set
file_store = get_file_store('local', temp_dir)
event_stream = EventStream('abc', file_store)
runtime = TestRuntime(
+4 -1
View File
@@ -50,6 +50,7 @@ class DaytonaRuntime(ActionExecutionClient):
daytona_api_key = os.getenv('DAYTONA_API_KEY')
if not daytona_api_key:
raise ValueError('DAYTONA_API_KEY environment variable is required for Daytona runtime')
daytona_api_url = os.getenv('DAYTONA_API_URL', 'https://app.daytona.io/api')
daytona_target = os.getenv('DAYTONA_TARGET', 'eu')
@@ -128,7 +129,9 @@ class DaytonaRuntime(ActionExecutionClient):
def _construct_api_url(self, port: int) -> str:
assert self.sandbox is not None, 'Sandbox is not initialized'
return self.sandbox.get_preview_link(port).url
assert self.sandbox.runner_domain is not None, 'Runner domain is not available'
return f'https://{port}-{self.sandbox.id}.{self.sandbox.runner_domain}'
@property
def action_execution_server_url(self) -> str: