mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a6e91d1021 | |||
| c7e9f99759 |
@@ -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 \
|
||||
|
||||
@@ -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"
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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` を実行してください。
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
|
||||
@@ -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"
|
||||
```
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
-283
@@ -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"],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
Generated
+759
-617
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
}, []);
|
||||
};
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
@@ -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 запустити веб-сервер, додаток з'явиться тут."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,9 +45,6 @@ export default function AcceptTOS() {
|
||||
navigate(finalRedirectUrl);
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
window.location.href = "/";
|
||||
},
|
||||
});
|
||||
|
||||
const handleAcceptTOS = () => {
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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: "" };
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ export const MAP_PROVIDER = {
|
||||
replicate: "Replicate",
|
||||
voyage: "Voyage AI",
|
||||
openrouter: "OpenRouter",
|
||||
openhands: "OpenHands",
|
||||
};
|
||||
|
||||
export const mapProvider = (provider: string) =>
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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!");
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,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>
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
@@ -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',
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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."""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
):
|
||||
|
||||
@@ -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
@@ -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
@@ -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" ]
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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()
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user