Compare commits

..

26 Commits

Author SHA1 Message Date
openhands
ae45159ac6 Rename 'Context Loaded' to 'MicroAgent Activated' and show microagent names in message 2025-04-01 14:34:02 +00:00
Xingyao Wang
a72938fd87 Merge branch 'main' into openhands-workspace-6zb2umk1 2025-04-01 07:29:18 -07:00
Xingyao Wang
f809b08df7 Merge branch 'main' into openhands-workspace-6zb2umk1 2025-03-30 08:46:50 -07:00
Xingyao Wang
c1b92311da remove initial ecall observation 2025-03-29 23:34:35 -04:00
Xingyao Wang
6cfeb525f5 Merge branch 'main' into openhands-workspace-6zb2umk1 2025-03-29 09:48:37 -07:00
openhands
dd2085c8c4 Fix TypeScript errors in account-settings.tsx and format pyproject.toml 2025-03-29 02:32:45 +00:00
openhands
6d993d4e21 Fix frontend linter issues 2025-03-29 02:24:29 +00:00
Xingyao Wang
350518f3d6 remove recall action for agent message 2025-03-28 19:12:11 -07:00
Xingyao Wang
dba430dd57 tweak ui 2025-03-28 19:05:12 -07:00
Xingyao Wang
ebd02bc383 tweak ui 2025-03-28 19:02:03 -07:00
Xingyao Wang
cac76026d4 stop showing additional content 2025-03-28 19:01:37 -07:00
Xingyao Wang
69ea4ddc42 stop showing recall type in ui 2025-03-28 19:00:03 -07:00
Xingyao Wang
403070f57f fixed recall observation visualization 2025-03-28 18:56:00 -07:00
openhands
46b1c96437 Add special handling for RecallObservation in ExpandableMessage component 2025-03-28 22:45:27 +00:00
openhands
cdab20d8a3 Fix RecallObservation translation ID handling 2025-03-28 22:44:24 +00:00
openhands
4417dd97c3 Implement direct RecallObservation visualization without requiring RecallAction 2025-03-28 22:32:48 +00:00
openhands
fa5e088ec1 Fix RecallObservation collapsible display by using hidden message pattern 2025-03-28 22:28:21 +00:00
Xingyao Wang
fdf981817d Linter fix 2025-03-28 15:20:54 -07:00
Xingyao Wang
cc8d3b6a98 Merge commit 'ac8b5e79342f1c75a922333fb82dad4eef080b45' into openhands-workspace-6zb2umk1 2025-03-28 15:20:26 -07:00
openhands
5b68893879 Fix failing tests in conversation-panel.test.tsx 2025-03-28 08:28:50 +00:00
openhands
2c0ad34ad7 Fix failing tests in conversation-panel.test.tsx 2025-03-28 08:10:13 +00:00
openhands
9dee3d5818 Simplify RecallObservation handling in listen_socket.py 2025-03-28 08:01:58 +00:00
openhands
1b34e5e3f0 Allow RecallObservation events to be sent to the frontend while keeping RecallAction events filtered out 2025-03-28 07:40:09 +00:00
openhands
044f5df408 Only visualize RecallObservation, not RecallAction 2025-03-28 06:40:58 +00:00
openhands
872f0edab8 Update RecallAction and RecallObservation translations to be more descriptive 2025-03-27 23:37:15 +00:00
openhands
c7ab36521b Implement frontend visualization for RecallAction and RecallObservation 2025-03-27 23:31:04 +00:00
280 changed files with 3319 additions and 8313 deletions

View File

@@ -5,12 +5,11 @@ labels: ['bug']
body:
- type: markdown
attributes:
value: Thank you for taking the time to fill out this bug report. Please provide as much information as possible
to help us understand and address the issue effectively.
value: Thank you for taking the time to fill out this bug report. Please provide as much information as possible to help us understand and address the issue effectively.
- type: checkboxes
attributes:
label: Is there an existing issue for the same bug? (If one exists, thumbs up or comment on the issue instead).
label: Is there an existing issue for the same bug?
description: Please check if an issue already exists for the bug you encountered.
options:
- label: I have checked the existing issues.

View File

@@ -1,6 +1,6 @@
---
name: Feature Request or Enhancement
about: Suggest an idea for an OpenHands feature or enhancement
name: Feature Request
about: Suggest an idea for OpenHands features
title: ''
labels: 'enhancement'
assignees: ''
@@ -9,6 +9,10 @@ assignees: ''
**What problem or use case are you trying to solve?**
**Describe the UX or technical implementation you have in mind**
**Describe the UX of the solution you'd like**
**Do you have thoughts on the technical implementation?**
**Describe alternatives you've considered**
**Additional context**

View File

@@ -0,0 +1,18 @@
---
name: Technical Proposal
about: Propose a new architecture or technology
title: ''
labels: 'proposal'
assignees: ''
---
**Summary**
**Motivation**
**Technical Design**
**Alternatives to Consider**
**Additional context**

View File

@@ -25,6 +25,7 @@ concurrency:
cancel-in-progress: true
env:
BASE_IMAGE_FOR_HASH_EQUIVALENCE_TEST: nikolaik/python-nodejs:python3.12-nodejs22
RELEVANT_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
jobs:
@@ -36,7 +37,6 @@ jobs:
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
@@ -89,8 +89,6 @@ jobs:
base_image:
- image: 'nikolaik/python-nodejs:python3.12-nodejs22'
tag: nikolaik
- image: 'ubuntu:24.04'
tag: ubuntu
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -118,7 +116,6 @@ jobs:
with:
path: |
~/.cache/pypoetry
~/.cache/ms-playwright
~/.virtualenvs
key: ${{ runner.os }}-poetry-${{ hashFiles('**/poetry.lock') }}
restore-keys: |
@@ -159,8 +156,6 @@ jobs:
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:
@@ -170,7 +165,6 @@ jobs:
with:
path: |
~/.cache/pypoetry
~/.cache/ms-playwright
~/.virtualenvs
key: ${{ runner.os }}-poetry-${{ hashFiles('**/poetry.lock') }}
restore-keys: |
@@ -214,7 +208,7 @@ jobs:
strategy:
fail-fast: false
matrix:
base_image: ['nikolaik', 'ubuntu']
base_image: ['nikolaik']
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
@@ -236,7 +230,6 @@ jobs:
with:
path: |
~/.cache/pypoetry
~/.cache/ms-playwright
~/.virtualenvs
key: ${{ runner.os }}-poetry-${{ hashFiles('**/poetry.lock') }}
restore-keys: |
@@ -262,9 +255,6 @@ jobs:
image_name=ghcr.io/${{ env.REPO_OWNER }}/runtime:${{ env.RELEVANT_SHA }}-${{ matrix.base_image }}
# Setting RUN_AS_OPENHANDS to false means use root.
# That should mean SANDBOX_USER_ID is ignored but some tests do not check for RUN_AS_OPENHANDS.
TEST_RUNTIME=docker \
SANDBOX_USER_ID=$(id -u) \
SANDBOX_RUNTIME_CONTAINER_IMAGE=$image_name \
@@ -283,7 +273,7 @@ jobs:
needs: [ghcr_build_runtime]
strategy:
matrix:
base_image: [nikolaik, ubuntu]
base_image: ['nikolaik']
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx

View File

@@ -145,15 +145,13 @@ jobs:
fi
- name: Set environment variables
env:
REVIEW_BODY: ${{ github.event.review.body || '' }}
run: |
# Handle pull request events first
if [ -n "${{ github.event.pull_request.number }}" ]; then
echo "ISSUE_NUMBER=${{ github.event.pull_request.number }}" >> $GITHUB_ENV
echo "ISSUE_TYPE=pr" >> $GITHUB_ENV
# Handle pull request review events
elif [ -n "$REVIEW_BODY" ]; then
elif [ -n "${{ github.event.review.body }}" ]; then
echo "ISSUE_NUMBER=${{ github.event.pull_request.number }}" >> $GITHUB_ENV
echo "ISSUE_TYPE=pr" >> $GITHUB_ENV
# Handle issue comment events that reference a PR
@@ -166,7 +164,7 @@ jobs:
echo "ISSUE_TYPE=issue" >> $GITHUB_ENV
fi
if [ -n "$REVIEW_BODY" ]; then
if [ -n "${{ github.event.review.body }}" ]; then
echo "COMMENT_ID=${{ github.event.review.id || 'None' }}" >> $GITHUB_ENV
else
echo "COMMENT_ID=${{ github.event.comment.id || 'None' }}" >> $GITHUB_ENV

View File

@@ -19,10 +19,9 @@ jobs:
ref: ${{ github.head_ref }}
- name: Trigger remote job
env:
PR_BRANCH: ${{ github.head_ref }}
run: |
REPO_URL="https://github.com/${{ github.repository }}"
PR_BRANCH="${{ github.head_ref }}"
echo "Repository URL: $REPO_URL"
echo "PR Branch: $PR_BRANCH"

View File

@@ -118,7 +118,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.31-nikolaik`
Example: `export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.30-nikolaik`
## Develop inside Docker container

View File

@@ -43,17 +43,17 @@ See the [Running OpenHands](https://docs.all-hands.dev/modules/usage/installatio
system requirements and more information.
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.31-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.30-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.31-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.30-nikolaik \
-e LOG_ALL_EVENTS=true \
-v /var/run/docker.sock:/var/run/docker.sock \
-v ~/.openhands-state:/.openhands-state \
-p 3000:3000 \
--add-host host.docker.internal:host-gateway \
--name openhands-app \
docker.all-hands.dev/all-hands-ai/openhands:0.31
docker.all-hands.dev/all-hands-ai/openhands:0.30
```
> [!WARNING]

View File

@@ -11,7 +11,7 @@ services:
- BACKEND_HOST=${BACKEND_HOST:-"0.0.0.0"}
- SANDBOX_API_HOSTNAME=host.docker.internal
#
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.31-nikolaik}
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.30-nikolaik}
- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234}
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
ports:

View File

@@ -7,7 +7,7 @@ services:
image: openhands:latest
container_name: openhands-app-${DATE:-}
environment:
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-docker.all-hands.dev/all-hands-ai/runtime:0.31-nikolaik}
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-docker.all-hands.dev/all-hands-ai/runtime:0.30-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-state for this user
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
ports:

View File

@@ -32,8 +32,6 @@ For instructions or processes that need to be followed in a specific order, use
Example:
1. Step one: Do this.
- First this sub step.
- Then this sub step.
2. Step two: Complete this action.
3. Step three: Verify the result.
@@ -49,14 +47,6 @@ docker run -it \
...
```
### Use of Note and Warning
When adding a note or warning, use the built-in note and warning syntax.
Example:
:::note
This section is for advanced users only.
:::
### Referring to UI Elements

View File

@@ -60,7 +60,25 @@ Félicitations !
## Explication technique
Veuillez consulter le [chapitre sur les images Docker personnalisées dans la documentation d'exécution](https://docs.all-hands.dev/fr/modules/usage/architecture/runtime) pour obtenir des explications plus détaillées.
Lorsqu'une image personnalisée est utilisée pour la première fois, elle ne sera pas trouvée et donc elle sera construite (à l'exécution ultérieure, l'image construite sera trouvée et renvoyée).
L'image personnalisée est construite avec [_build_sandbox_image()](https://github.com/All-Hands-AI/OpenHands/blob/main/openhands/runtime/docker/image_agnostic_util.py#L29), qui crée un fichier docker en utilisant votre image personnalisée comme base et configure ensuite l'environnement pour OpenHands, comme ceci:
```python
dockerfile_content = (
f'FROM {base_image}\n'
'RUN apt update && apt install -y openssh-server wget sudo\n'
'RUN mkdir -p -m0755 /var/run/sshd\n'
'RUN mkdir -p /openhands && mkdir -p /openhands/logs && chmod 777 /openhands/logs\n'
'RUN wget "https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-$(uname)-$(uname -m).sh"\n'
'RUN bash Miniforge3-$(uname)-$(uname -m).sh -b -p /openhands/miniforge3\n'
'RUN bash -c ". /openhands/miniforge3/etc/profile.d/conda.sh && conda config --set changeps1 False && conda config --append channels conda-forge"\n'
'RUN echo "export PATH=/openhands/miniforge3/bin:$PATH" >> ~/.bashrc\n'
'RUN echo "export PATH=/openhands/miniforge3/bin:$PATH" >> /openhands/bash.bashrc\n'
).strip()
```
> Remarque: Le nom de l'image est modifié via [_get_new_image_name()](https://github.com/All-Hands-AI/OpenHands/blob/main/openhands/runtime/docker/image_agnostic_util.py#L63) et c'est ce nom modifié qui sera recherché lors des exécutions ultérieures.
## Dépannage / Erreurs

View File

@@ -52,7 +52,7 @@ LLM_API_KEY="sk_test_12345"
```bash
docker run -it \
--pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.31-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.30-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
-e LLM_API_KEY=$LLM_API_KEY \
@@ -61,7 +61,7 @@ docker run -it \
-v /var/run/docker.sock:/var/run/docker.sock \
--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.31 \
docker.all-hands.dev/all-hands-ai/openhands:0.30 \
python -m openhands.core.cli
```

View File

@@ -46,7 +46,7 @@ LLM_API_KEY="sk_test_12345"
```bash
docker run -it \
--pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.31-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.30-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
-e LLM_API_KEY=$LLM_API_KEY \
@@ -56,6 +56,6 @@ docker run -it \
-v /var/run/docker.sock:/var/run/docker.sock \
--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.31 \
docker.all-hands.dev/all-hands-ai/openhands:0.30 \
python -m openhands.core.main -t "write a bash script that prints hi" --no-auto-continue
```

View File

@@ -13,16 +13,16 @@
La façon la plus simple d'exécuter OpenHands est avec Docker.
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.31-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.30-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.31-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.30-nikolaik \
-e LOG_ALL_EVENTS=true \
-v /var/run/docker.sock:/var/run/docker.sock \
-p 3000:3000 \
--add-host host.docker.internal:host-gateway \
--name openhands-app \
docker.all-hands.dev/all-hands-ai/openhands:0.31
docker.all-hands.dev/all-hands-ai/openhands:0.30
```
Vous pouvez également exécuter OpenHands en mode [headless scriptable](https://docs.all-hands.dev/modules/usage/how-to/headless-mode), en tant que [CLI interactive](https://docs.all-hands.dev/modules/usage/how-to/cli-mode), ou en utilisant l'[Action GitHub OpenHands](https://docs.all-hands.dev/modules/usage/how-to/github-action).

View File

@@ -13,7 +13,7 @@ C'est le Runtime par défaut qui est utilisé lorsque vous démarrez OpenHands.
```
docker run # ...
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.31-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.30-nikolaik \
-v /var/run/docker.sock:/var/run/docker.sock \
# ...
```

View File

@@ -60,7 +60,25 @@ base_container_image="custom_image"
## 技術的な説明
詳細な説明については、[実行時ドキュメントのカスタムDockerイメージの章](https://docs.all-hands.dev/ja/modules/usage/architecture/runtime)を参照してください
カスタムイメージが初めて使用される場合、イメージが見つからないため、ビルドされます (その後の実行では、ビルドされたイメージが見つかり、返されます)
カスタムイメージは [_build_sandbox_image()](https://github.com/All-Hands-AI/OpenHands/blob/main/openhands/runtime/docker/image_agnostic_util.py#L29) でビルドされます。これは、カスタムイメージをベースとして使用して docker ファイルを作成し、次のように OpenHands の環境を設定します:
```python
dockerfile_content = (
f'FROM {base_image}\n'
'RUN apt update && apt install -y openssh-server wget sudo\n'
'RUN mkdir -p -m0755 /var/run/sshd\n'
'RUN mkdir -p /openhands && mkdir -p /openhands/logs && chmod 777 /openhands/logs\n'
'RUN wget "https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-$(uname)-$(uname -m).sh"\n'
'RUN bash Miniforge3-$(uname)-$(uname -m).sh -b -p /openhands/miniforge3\n'
'RUN bash -c ". /openhands/miniforge3/etc/profile.d/conda.sh && conda config --set changeps1 False && conda config --append channels conda-forge"\n'
'RUN echo "export PATH=/openhands/miniforge3/bin:$PATH" >> ~/.bashrc\n'
'RUN echo "export PATH=/openhands/miniforge3/bin:$PATH" >> /openhands/bash.bashrc\n'
).strip()
```
> 注: イメージ名は [_get_new_image_name()](https://github.com/All-Hands-AI/OpenHands/blob/main/openhands/runtime/docker/image_agnostic_util.py#L63) で変更され、この変更された名前が後続の実行時に検索されます。
## トラブルシューティング / エラー

View File

@@ -34,7 +34,7 @@ Docker で OpenHands を CLI モードで実行するには:
```bash
docker run -it \
--pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.31-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.30-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
-e LLM_API_KEY=$LLM_API_KEY \
@@ -44,7 +44,7 @@ docker run -it \
-v ~/.openhands-state:/.openhands-state \
--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.31 \
docker.all-hands.dev/all-hands-ai/openhands:0.30 \
python -m openhands.core.cli
```

View File

@@ -31,7 +31,7 @@ DockerでOpenHandsをヘッドレスモードで実行するには:
```bash
docker run -it \
--pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.31-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.30-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
-e LLM_API_KEY=$LLM_API_KEY \
@@ -42,7 +42,7 @@ docker run -it \
-v ~/.openhands-state:/.openhands-state \
--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.31 \
docker.all-hands.dev/all-hands-ai/openhands:0.30 \
python -m openhands.core.main -t "write a bash script that prints hi"
```

View File

@@ -25,7 +25,7 @@ nikolaik の `SANDBOX_RUNTIME_CONTAINER_IMAGE` は、ランタイムサーバー
```bash
docker run # ...
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.31-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.30-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
-v $WORKSPACE_BASE:/opt/workspace_base \
@@ -82,5 +82,5 @@ docker network create openhands-network
# 分離されたネットワークで OpenHands を実行
docker run # ... \
--network openhands-network \
docker.all-hands.dev/all-hands-ai/openhands:0.31
docker.all-hands.dev/all-hands-ai/openhands:0.30
```

View File

@@ -35,7 +35,7 @@ Para executar o OpenHands no modo CLI com Docker:
```bash
docker run -it \
--pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.31-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.30-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
-e LLM_API_KEY=$LLM_API_KEY \
@@ -45,7 +45,7 @@ docker run -it \
-v ~/.openhands-state:/.openhands-state \
--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.31 \
docker.all-hands.dev/all-hands-ai/openhands:0.30 \
python -m openhands.core.cli
```

View File

@@ -32,7 +32,7 @@ Para executar o OpenHands no modo Headless com Docker:
```bash
docker run -it \
--pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.31-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.30-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
-e LLM_API_KEY=$LLM_API_KEY \
@@ -43,7 +43,7 @@ docker run -it \
-v ~/.openhands-state:/.openhands-state \
--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.31 \
docker.all-hands.dev/all-hands-ai/openhands:0.30 \
python -m openhands.core.main -t "escreva um script bash que imprima oi"
```

View File

@@ -58,17 +58,17 @@
A maneira mais fácil de executar o OpenHands é no Docker.
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.31-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.30-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.31-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.30-nikolaik \
-e LOG_ALL_EVENTS=true \
-v /var/run/docker.sock:/var/run/docker.sock \
-v ~/.openhands-state:/.openhands-state \
-p 3000:3000 \
--add-host host.docker.internal:host-gateway \
--name openhands-app \
docker.all-hands.dev/all-hands-ai/openhands:0.31
docker.all-hands.dev/all-hands-ai/openhands:0.30
```
Você encontrará o OpenHands em execução em http://localhost:3000!

View File

@@ -13,7 +13,7 @@ Este é o Runtime padrão que é usado quando você inicia o OpenHands. Você po
```
docker run # ...
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.31-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.30-nikolaik \
-v /var/run/docker.sock:/var/run/docker.sock \
# ...
```

View File

@@ -1,6 +1,6 @@
# 💿 如何创建自定义 Docker 沙箱
默认的 OpenHands 沙箱包含一个[最小化 ubuntu 配置](https://github.com/All-Hands-AI/OpenHands/blob/main/containers/e2b-sandbox/Dockerfile)。您的应用场景可能需要在默认状态下安装额外的软件。本指南将教您如何通过使用自定义 Docker 映像来实现这一目标。
默认的 OpenHands 沙箱包含一个[最小化 ubuntu 配置](https://github.com/All-Hands-AI/OpenHands/blob/main/containers/sandbox/Dockerfile)。您的应用场景可能需要在默认状态下安装额外的软件。本指南将教您如何通过使用自定义 Docker 映像来实现这一目标。
目前提供两种实现方案:
1. 从 Docker Hub 拉取已有镜像。例如,如果您想安装 `nodejs` ,您可以通过使用 `node:20` 镜像来实现。

View File

@@ -50,7 +50,7 @@ LLM_API_KEY="sk_test_12345"
```bash
docker run -it \
--pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.31-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.30-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
-e LLM_API_KEY=$LLM_API_KEY \
@@ -59,7 +59,7 @@ docker run -it \
-v /var/run/docker.sock:/var/run/docker.sock \
--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.31 \
docker.all-hands.dev/all-hands-ai/openhands:0.30 \
python -m openhands.core.cli
```

View File

@@ -47,7 +47,7 @@ LLM_API_KEY="sk_test_12345"
```bash
docker run -it \
--pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.31-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.30-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
-e LLM_API_KEY=$LLM_API_KEY \
@@ -57,6 +57,6 @@ docker run -it \
-v /var/run/docker.sock:/var/run/docker.sock \
--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.31 \
docker.all-hands.dev/all-hands-ai/openhands:0.30 \
python -m openhands.core.main -t "write a bash script that prints hi" --no-auto-continue
```

View File

@@ -11,16 +11,16 @@
在 Docker 中运行 OpenHands 是最简单的方式。
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.31-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.30-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.31-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.30-nikolaik \
-e LOG_ALL_EVENTS=true \
-v /var/run/docker.sock:/var/run/docker.sock \
-p 3000:3000 \
--add-host host.docker.internal:host-gateway \
--name openhands-app \
docker.all-hands.dev/all-hands-ai/openhands:0.31
docker.all-hands.dev/all-hands-ai/openhands:0.30
```
你也可以在可脚本化的[无头模式](https://docs.all-hands.dev/modules/usage/how-to/headless-mode)下运行 OpenHands作为[交互式 CLI](https://docs.all-hands.dev/modules/usage/how-to/cli-mode),或使用 [OpenHands GitHub Action](https://docs.all-hands.dev/modules/usage/how-to/github-action)。

View File

@@ -11,7 +11,7 @@
```
docker run # ...
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.31-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.30-nikolaik \
-v /var/run/docker.sock:/var/run/docker.sock \
# ...
```

View File

@@ -4,7 +4,9 @@ OpenHands Cloud is the cloud hosted version of OpenHands by All Hands AI.
## Accessing OpenHands Cloud
OpenHands Cloud can be accessed at https://app.all-hands.dev/.
Currently, users are being admitted to access OpenHands Cloud in waves. To sign up,
[join the waitlist](https://www.all-hands.dev/join-waitlist). Once you are approved, you will get an email with
instructions on how to access it.
## Getting Started

View File

@@ -1,8 +1,10 @@
# Configuration Options
This guide details all configuration options available for OpenHands, helping you customize its behavior and integrate it with other services.
:::note
This page outlines all available configuration options for OpenHands, allowing you to customize its behavior and
integrate it with other services. In GUI Mode, any settings applied through the Settings UI will take precedence.
If you are running in [GUI Mode](https://docs.all-hands.dev/modules/usage/how-to/gui-mode), the settings available in the Settings UI will always
take precedence.
:::
## Core Configuration

View File

@@ -3,97 +3,109 @@
So you've [run OpenHands](./installation) and have
[set up your LLM](./installation#setup). Now what?
OpenHands can assist with a range of engineering tasks. However, the technology is still new, and were far from having
agents that can handle complex tasks independently. Its important to understand what the agent does well and where it
needs support.
OpenHands can help you tackle a wide variety of engineering tasks. But the technology
is still new, and we're a long way off from having agents that can take on large, complicated
engineering tasks without any guidance. So it's important to get a feel for what the agent
does well, and where it might need some help.
## Hello World
Start with a simple "hello world" example. It might be trickier than it seems!
The first thing you might want to try is a simple "hello world" example.
This can be more complicated than it sounds!
Prompt the agent with:
> Write a bash script hello.sh that prints "hello world!"
Try prompting the agent with:
> Please write a bash script hello.sh that prints "hello world!"
The agent will write the script, set the correct permissions, and run it to check the output.
You should see that the agent not only writes the script, it sets the correct
permissions and runs the script to check the output.
You can continue prompting the agent to refine your code. This is a great way to
work with agents. Start simple, and iterate.
> Modify hello.sh so that it accepts a name as the first argument, but defaults to "world"
> Please modify hello.sh so that it accepts a name as the first argument, but defaults to "world"
You can also use any language you need. The agent may need time to set up the environment.
You can also work in any language you need, though the agent might need to spend some
time setting up its environment!
> Please convert hello.sh to a Ruby script, and run it
## Building From Scratch
Agents excel at "greenfield" tasks, where they dont need context about existing code and
they can start from scratch.
Begin with a simple task and iterate from there. Be specific about what you want and the tech stack.
Agents do exceptionally well at "greenfield" tasks (tasks where they don't need
any context about an existing codebase) and they can just start from scratch.
It's best to start with a simple task, and then iterate on it. It's also best to be
as specific as possible about what you want, what the tech stack should be, etc.
For example, we might build a TODO app:
> Build a frontend-only TODO app in React. All state should be stored in localStorage.
> Please build a basic TODO list app in React. It should be frontend-only, and all state
> should be kept in localStorage.
Once the basic structure is in place, continue refining:
We can keep iterating on the app once the skeleton is there:
> Allow adding an optional due date to each task.
> Please allow adding an optional due date to every task.
Just like normal development, commit and push your code often.
Just like with normal development, it's good to commit and push your code frequently.
This way you can always revert back to an old state if the agent goes off track.
You can ask the agent to commit and push for you:
> Commit the changes and push them to a new branch called "feature/due-dates"
> Please commit the changes and push them to a new branch called "feature/due-dates"
## Adding New Code
OpenHands is great at adding new code to an existing codebase.
OpenHands can also do a great job adding new code to an existing code base.
For instance, you can ask OpenHands to add a GitHub action that lints your code. It might check your codebase to
determine the language, then create a new file in `./github/workflows/lint.yml`.
For example, you can ask OpenHands to add a new GitHub action to your project
which lints your code. OpenHands may take a peek at your codebase to see what language
it should use and then drop a new file into `./github/workflows/lint.yml`.
> Add a GitHub action that lints the code in this repository.
> Please add a GitHub action that lints the code in this repository.
Some tasks need more context. While OpenHands can use commands like ls and grep to search, providing context upfront
speeds things up and reduces token usage.
Some tasks might require a bit more context. While OpenHands can use `ls` and `grep`
to search through your codebase, providing context up front allows it to move faster,
and more accurately. And it'll cost you fewer tokens!
> Modify ./backend/api/routes.js to add a new route that returns a list of all tasks.
> Please modify ./backend/api/routes.js to add a new route that returns a list of all tasks.
> Add a new React component to the ./frontend/components directory to display a list of Widgets.
> It should use the existing Widget component.
> Please add a new React component that displays a list of Widgets to the ./frontend/components
> directory. It should use the existing Widget component.
## Refactoring
OpenHands does great at refactoring code in small chunks. Rather than rearchitecting the entire codebase,
it's more effective to break up long files and functions or rename variables.
OpenHands does great at refactoring existing code, especially in small chunks.
You probably don't want to try rearchitecting your whole codebase, but breaking up
long files and functions, renaming variables, etc. tend to work very well.
> Rename all the single-letter variables in ./app.go.
> Please rename all the single-letter variables in ./app.go.
> Split the `build_and_deploy_widgets` function into two functions, `build_widgets` and `deploy_widgets` in widget.php.
> Please break the function `build_and_deploy_widgets` into two functions, `build_widgets` and `deploy_widgets` in widget.php.
> Break ./api/routes.js into separate files for each route.
> Please break ./api/routes.js into separate files for each route.
## Bug Fixes
OpenHands can help track down and fix bugs, but bug fixing can be tricky and often requires more context.
Its helpful if youve already diagnosed the issue and just need OpenHands to handle the logic.
OpenHands can also help you track down and fix bugs in your code. But as any
developer knows, bug fixing can be extremely tricky, and often OpenHands will need more context.
It helps if you've diagnosed the bug, but want OpenHands to figure out the logic.
> The email field in the `/subscribe` endpoint is rejecting .io domains. Fix this.
> Currently the email field in the `/subscribe` endpoint is rejecting .io domains. Please fix this.
> The `search_widgets` function in ./app.py is doing a case-sensitive search. Make it case-insensitive.
> The `search_widgets` function in ./app.py is doing a case-sensitive search. Please make it case-insensitive.
For bug fixing, test-driven development can be really useful. You can ask the agent to write a new test and iterate
until the bug is fixed:
It often helps to do test-driven development when bug fixing with an agent.
You can ask the agent to write a new test, and then iterate until it fixes the bug:
> The `hello` function crashes on the empty string. Write a test that reproduces this bug, then fix the code so it passes.
> The `hello` function crashes on the empty string. Please write a test that reproduces this bug, then fix the code so it passes.
## More
OpenHands can assist with nearly any coding task, but it takes some practice to get the best results.
Keep these tips in mind:
OpenHands is capable of helping out on just about any coding task but it takes some practice
to get the most out of it. Remember to:
* Keep your tasks small.
* Be specific.
* Provide plenty of context.
* Be as specific as possible.
* Provide as much context as possible.
* Commit and push frequently.
See [Prompting Best Practices](./prompting/prompting-best-practices) for more tips on how to get the most out of OpenHands.

View File

@@ -35,7 +35,7 @@ To run OpenHands in CLI mode with Docker:
```bash
docker run -it \
--pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.31-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.30-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
-e LLM_API_KEY=$LLM_API_KEY \
@@ -45,8 +45,52 @@ docker run -it \
-v ~/.openhands-state:/.openhands-state \
--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.31 \
docker.all-hands.dev/all-hands-ai/openhands:0.30 \
python -m openhands.core.cli
```
This command will start an interactive session in Docker where you can input tasks and receive responses from OpenHands.
## Examples of CLI Commands and Expected Outputs
Here are some examples of CLI commands and their expected outputs:
### Example 1: Simple Task
```bash
>> Write a Python script that prints "Hello, World!"
```
Expected Output:
```bash
🤖 Sure! Here is a Python script that prints "Hello, World!":
print("Hello, World!")
```
### Example 2: Bash Command
```bash
>> Create a directory named "test_dir"
```
Expected Output:
```bash
🤖 Creating a directory named "test_dir":
mkdir test_dir
```
### Example 3: Error Handling
```bash
>> Delete a non-existent file
```
Expected Output:
```bash
🤖 An error occurred. Please try again.
```

View File

@@ -1,9 +1,5 @@
# Custom Sandbox
:::note
This guide is for users that would like to use their own custom Docker image for the runtime, e.g. with certain tools or programming languages pre-installed
:::
The sandbox is where the agent performs its tasks. Instead of running commands directly on your computer
(which could be risky), the agent runs them inside a Docker container.

View File

@@ -1,6 +1,6 @@
# Using the OpenHands GitHub Action
This guide explains how to use the OpenHands GitHub Action in your own projects.
This guide explains how to use the OpenHands GitHub Action, both within the OpenHands repository and in your own projects.
## Using the Action in the OpenHands Repository

View File

@@ -11,10 +11,9 @@ OpenHands provides a Graphical User Interface (GUI) mode for interacting with th
### Initial Setup
1. Upon first launch, you'll see a settings popup.
1. Upon first launch, you'll see a settings page.
2. Select an `LLM Provider` and `LLM Model` from the dropdown menus. If the required model does not exist in the list,
select `see advanced settings`. Then toggle `Advanced` options and enter it with the correct prefix in the
`Custom Model` text box.
toggle `Advanced` options and enter it with the correct prefix in the `Custom Model` text box.
3. Enter the corresponding `API Key` for your chosen provider.
4. Click `Save Changes` to apply the settings.
@@ -33,7 +32,7 @@ OpenHands automatically exports a `GITHUB_TOKEN` to the shell environment if it
- `repo` (Full control of private repositories)
- **Fine-Grained Tokens**
- All Repositories (You can select specific repositories, but this will impact what returns in repo search)
- Minimal Permissions ( Select `Meta Data = Read-only` read for search, `Pull Requests = Read and Write` and `Content = Read and Write` for branch creation)
- Minimal Permissions ( Select **Meta Data = Read-only** read for search, **Pull Requests = Read and Write**, **Content = Read and Write** for branch creation)
2. **Enter Token in OpenHands**:
- Click the Settings button (gear icon).
- Navigate to the `GitHub Settings` section.

View File

@@ -32,7 +32,7 @@ To run OpenHands in Headless mode with Docker:
```bash
docker run -it \
--pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.31-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.30-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
-e LLM_API_KEY=$LLM_API_KEY \
@@ -43,7 +43,7 @@ docker run -it \
-v ~/.openhands-state:/.openhands-state \
--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.31 \
docker.all-hands.dev/all-hands-ai/openhands:0.30 \
python -m openhands.core.main -t "write a bash script that prints hi"
```

View File

@@ -58,31 +58,30 @@ A system with a modern processor and a minimum of **4GB RAM** is recommended to
The easiest way to run OpenHands is in Docker.
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.31-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.30-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.31-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.30-nikolaik \
-e LOG_ALL_EVENTS=true \
-v /var/run/docker.sock:/var/run/docker.sock \
-v ~/.openhands-state:/.openhands-state \
-p 3000:3000 \
--add-host host.docker.internal:host-gateway \
--name openhands-app \
docker.all-hands.dev/all-hands-ai/openhands:0.31
docker.all-hands.dev/all-hands-ai/openhands:0.30
```
You'll find OpenHands running at http://localhost:3000!
You can also [connect OpenHands to your local filesystem](https://docs.all-hands.dev/modules/usage/runtimes/docker#connecting-to-your-filesystem),
You can also [connect OpenHands to your local filesystem](https://docs.all-hands.dev/modules/usage/runtimes#connecting-to-your-filesystem),
run OpenHands in a scriptable [headless mode](https://docs.all-hands.dev/modules/usage/how-to/headless-mode),
interact with it via a [friendly CLI](https://docs.all-hands.dev/modules/usage/how-to/cli-mode),
or run it on tagged issues with [a GitHub action](https://docs.all-hands.dev/modules/usage/how-to/github-action).
## Setup
After launching OpenHands, you **must** select an `LLM Provider` and `LLM Model` and enter a corresponding `API Key`.
This can be done during the initial settings popup or by selecting the `Settings`
button (gear icon) in the UI.
Upon launching OpenHands, you'll see a Settings page. You **must** select an `LLM Provider` and `LLM Model` and enter a corresponding `API Key`.
These can be changed at any time by selecting the `Settings` button (gear icon) in the UI.
If the required model does not exist in the list, you can toggle `Advanced` options and manually enter it with the correct prefix
in the `Custom Model` text box.
@@ -94,17 +93,17 @@ OpenHands requires an API key to access most language models. Here's how to get
#### Anthropic (Claude)
1. [Create an Anthropic account](https://console.anthropic.com/).
2. [Generate an API key](https://console.anthropic.com/settings/keys).
3. [Set up billing](https://console.anthropic.com/settings/billing).
1. [Create an Anthropic account](https://console.anthropic.com/)
2. [Generate an API key](https://console.anthropic.com/settings/keys)
3. [Set up billing](https://console.anthropic.com/settings/billing)
Consider setting usage limits to control costs.
#### OpenAI
1. [Create an OpenAI account](https://platform.openai.com/).
2. [Generate an API key](https://platform.openai.com/api-keys).
3. [Set up billing](https://platform.openai.com/account/billing/overview).
1. [Create an OpenAI account](https://platform.openai.com/)
2. [Generate an API key](https://platform.openai.com/api-keys)
3. [Set up billing](https://platform.openai.com/account/billing/overview)
Now you're ready to [get started with OpenHands](./getting-started).
@@ -116,6 +115,8 @@ We use SemVer so `0.9` will automatically point to the latest `0.9.x` release, a
- For the most up-to-date development version, replace $VERSION in `openhands:$VERSION` and `runtime:$VERSION`, with `main`.
This version is unstable and is recommended for testing or development purposes only.
You can choose the tag that best suits your needs based on stability requirements and desired features.
For the development workflow, see [Development.md](https://github.com/All-Hands-AI/OpenHands/blob/main/Development.md).
Are you having trouble? Check out our [Troubleshooting Guide](https://docs.all-hands.dev/modules/usage/troubleshooting).

View File

@@ -1,28 +1,60 @@
# OpenHands Feature Overview
![overview](/img/oh-features.png)
![overview](https://www.all-hands.dev/assets/product/product-slide-1.webp)
### Chat Panel
- Displays the conversation between the user and OpenHands.
- OpenHands explains its actions in this panel.
### Workspace
- Browse project files and directories.
- Use the `Open in VS Code` option to:
* Modify files
## 1. Workspace
The Workspace feature provides a comprehensive development environment with the following key capabilities:
- File Explorer: Browse, view, and manage project files and directories
- Project Management: Import, create, and navigate between different projects
- Integrated Development Tools: Seamless integration with various development workflows
- File Operations:
* View file contents
* Create new files and folders
* Upload and download files
* Basic file manipulation
### Jupyter
- Shows all Python commands that were executed by OpenHands.
- Particularly handy when using OpenHands to perform data visualization tasks.
## 2. Jupyter Notebook
The Jupyter Notebook feature offers an interactive coding and data analysis environment:
- Interactive Code Cells: Execute Python code in a cell-based interface
- Input and Output Tracking: Maintain a history of code inputs and their corresponding outputs
- Persistent Session: Preserve code execution context between cells
- Supports various Python operations and data analysis tasks
- Real-time code execution and result visualization
### App
- Shows the web server when OpenHands runs an application.
- Users can interact with the running application.
## 3. Browser (Beta)
The Browser feature provides web interaction capabilities:
- Web Page Navigation: Open and browse websites within the application
- Screenshot Capture: Automatically generate screenshots of web pages
- Interaction Tools:
* Click elements
* Fill out forms
* Scroll pages
* Navigate through web content
- Supports 15 different browser interaction functions
### Browser
- Used by OpenHands to browse websites.
- The browser is non-interactive.
## 4. Terminal
The Terminal feature offers a command-line interface within the application:
- Execute Shell Commands: Run bash and system commands
- Command History: Track and recall previous commands
- Environment Interaction: Interact directly with the system's command line
- Support for various programming and system administration tasks
### Terminal
- A space for OpenHands and users to run terminal commands.
## 5. Chat / AI Conversation
The Chat interface provides an AI-powered conversational experience:
- Interactive AI Assistant: Engage in natural language conversations
- Context-Aware Responses: AI understands and responds to development-related queries
- Action Suggestions: Provides actionable recommendations for tasks
- Conversation Management: Create, delete, and manage different conversation threads
## 6. App (Beta)
The main application interface combines all these features:
- Integrated Workspace: Seamless integration of workspace, browser, terminal, and AI chat
- Configurable Layout: Customize the arrangement of different feature panels
- State Management: Maintain context and state across different features
- Security and Privacy Controls: Manage application settings and permissions
### Additional Notes
- The application is currently in beta, with ongoing improvements and feature additions
- Supports various development workflows and AI-assisted coding
- Designed to enhance developer productivity through integrated tools and AI assistance

View File

@@ -25,7 +25,7 @@ You will need your ChatGPT deployment name which can be found on the deployments
<deployment-name> below.
:::
1. Enable `Advanced` options.
1. Enable `Advanced` options
2. Set the following:
- `Custom Model` to azure/<deployment-name>
- `Base URL` to your Azure API Base URL (e.g. `https://example-endpoint.openai.azure.com`)

View File

@@ -1,9 +1,5 @@
# 🤖 LLM Backends
:::note
This section is for users who want to connect OpenHands to different LLMs.
:::
OpenHands can connect to any LLM supported by LiteLLM. However, it requires a powerful model to work.
## Model Recommendations
@@ -13,12 +9,10 @@ recommendations for model selection. Our latest benchmarking results can be foun
Based on these findings and community feedback, the following models have been verified to work reasonably well with OpenHands:
- [anthropic/claude-3-7-sonnet-20250219](https://www.anthropic.com/api) (recommended)
- [gemini/gemini-2.5-pro](https://blog.google/technology/google-deepmind/gemini-model-thinking-updates-march-2025/)
- [deepseek/deepseek-chat](https://api-docs.deepseek.com/)
- [openai/o3-mini](https://openai.com/index/openai-o3-mini/)
- [all-hands/openhands-lm-32b-v0.1](https://www.all-hands.dev/blog/introducing-openhands-lm-32b----a-strong-open-coding-agent-model) -- available through [OpenRouter](https://openrouter.ai/all-hands/openhands-lm-32b-v0.1)
- anthropic/claude-3-5-sonnet-20241022 (recommended)
- anthropic/claude-3-5-haiku-20241022
- deepseek/deepseek-chat
- gpt-4o
:::warning
OpenHands will issue many prompts to the LLM you configure. Most of these LLMs cost money, so be sure to set spending
@@ -62,10 +56,10 @@ We have a few guides for running OpenHands with specific model providers:
- [Azure](llms/azure-llms)
- [Google](llms/google-llms)
- [Groq](llms/groq)
- [Local LLMs with SGLang or vLLM](llms/../local-llms.md)
- [LiteLLM Proxy](llms/litellm-proxy)
- [OpenAI](llms/openai-llms)
- [OpenRouter](llms/openrouter)
- [Local LLMs with SGLang or vLLM](llms/../local-llms.md)
### API retries and rate limits

View File

@@ -64,7 +64,7 @@ Ensure `config.toml` exists by running `make setup-config` which will create one
```
[core]
workspace_base="/path/to/your/workspace"
workspace_base="./workspace"
[llm]
embedding_model="local"

View File

@@ -1,38 +1,37 @@
# Prompting Best Practices
When working with OpenHands AI software developer, providing clear and effective prompts is key to getting accurate
and useful responses. This guide outlines best practices for crafting effective prompts.
When working with OpenHands AI software developer, it's crucial to provide clear and effective prompts. This guide outlines best practices for creating prompts that will yield the most accurate and useful responses.
## Characteristics of Good Prompts
Good prompts are:
- **Concrete**: Clearly describe what functionality should be added or what error needs fixing.
- **Location-specific**: Specify the locations in the codebase that should be modified, if known.
- **Appropriately scoped**: Focus on a single feature, typically not exceeding 100 lines of code.
- **Concrete**: They explain exactly what functionality should be added or what error needs to be fixed.
- **Location-specific**: If known, they explain the locations in the code base that should be modified.
- **Appropriately scoped**: They should be the size of a single feature, typically not exceeding 100 lines of code.
## Examples
### Good Prompt Examples
- Add a function `calculate_average` in `utils/math_operations.py` that takes a list of numbers as input and returns their average.
- Fix the TypeError in `frontend/src/components/UserProfile.tsx` occurring on line 42. The error suggests we're trying to access a property of undefined.
- Implement input validation for the email field in the registration form. Update `frontend/src/components/RegistrationForm.tsx` to check if the email is in a valid format before submission.
- "Add a function `calculate_average` in `utils/math_operations.py` that takes a list of numbers as input and returns their average."
- "Fix the TypeError in `frontend/src/components/UserProfile.tsx` occurring on line 42. The error suggests we're trying to access a property of undefined."
- "Implement input validation for the email field in the registration form. Update `frontend/src/components/RegistrationForm.tsx` to check if the email is in a valid format before submission."
### Bad Prompt Examples
- Make the code better. (Too vague, not concrete)
- Rewrite the entire backend to use a different framework. (Not appropriately scoped)
- There's a bug somewhere in the user authentication. Can you find and fix it? (Lacks specificity and location information)
- "Make the code better." (Too vague, not concrete)
- "Rewrite the entire backend to use a different framework." (Not appropriately scoped)
- "There's a bug somewhere in the user authentication. Can you find and fix it?" (Lacks specificity and location information)
## Tips for Effective Prompting
- Be as specific as possible about the desired outcome or the problem to be solved.
- Provide context, including relevant file paths and line numbers if available.
- Break large tasks into smaller, manageable prompts.
- Include relevant error messages or logs.
- Specify the programming language or framework, if not obvious.
- Break down large tasks into smaller, manageable prompts.
- Include any relevant error messages or logs.
- Specify the programming language or framework if it's not obvious from the context.
The more precise and informative your prompt, the better OpenHands can assist you.
Remember, the more precise and informative your prompt is, the better the AI can assist you in developing or modifying the OpenHands software.
See [Getting Started with OpenHands](../getting-started) for more examples of helpful prompts.

View File

@@ -1,26 +1,24 @@
# Runtime Configuration
:::note
This section is for users that would like to use a runtime other than Docker for OpenHands.
:::
A Runtime is an environment where the OpenHands agent can edit files and run
commands.
By default, OpenHands uses a [Docker-based runtime](./runtimes/docker), running on your local computer.
By default, OpenHands uses a Docker-based runtime, running on your local computer.
This means you only have to pay for the LLM you're using, and your code is only ever sent to the LLM.
We also support other runtimes, which are typically managed by third-parties.
We also support "remote" runtimes, which are typically managed by third-parties.
They can make setup a bit simpler and more scalable, especially
if you're running many OpenHands conversations in parallel (e.g. to do evaluation).
Additionally, we provide a [Local Runtime](./runtimes/local) that runs directly on your machine without Docker,
Additionally, we provide a "local" runtime that runs directly on your machine without Docker,
which can be useful in controlled environments like CI pipelines.
## Available Runtimes
OpenHands supports several different runtime environments:
- [Docker Runtime](./runtimes/docker.md) - The default runtime that uses Docker containers for isolation (recommended for most users).
- [OpenHands Remote Runtime](./runtimes/remote.md) - Cloud-based runtime for parallel execution (beta).
- [Modal Runtime](./runtimes/modal.md) - Runtime provided by our partners at Modal.
- [Daytona Runtime](./runtimes/daytona.md) - Runtime provided by Daytona.
- [Local Runtime](./runtimes/local.md) - Direct execution on your local machine without Docker.
- [Docker Runtime](./runtimes/docker.md) - The default runtime that uses Docker containers for isolation (recommended for most users)
- [OpenHands Remote Runtime](./runtimes/remote.md) - Cloud-based runtime for parallel execution (beta)
- [Modal Runtime](./runtimes/modal.md) - Runtime provided by our partners at Modal
- [Daytona Runtime](./runtimes/daytona.md) - Runtime provided by Daytona
- [Local Runtime](./runtimes/local.md) - Direct execution on your local machine without Docker

View File

@@ -8,7 +8,7 @@ that contains our Runtime server, as well as some basic utilities for Python and
You can also [build your own runtime image](../how-to/custom-sandbox-guide).
## Connecting to Your filesystem
A useful feature is the ability to connect to your local filesystem. To mount your filesystem into the runtime:
One useful feature here is the ability to connect to your local filesystem. To mount your filesystem into the runtime:
1. Set `WORKSPACE_BASE`:
```bash
@@ -40,20 +40,20 @@ but seems to work well on most systems.
## Hardened Docker Installation
When deploying OpenHands in environments where security is a priority, you should consider implementing a hardened
Docker configuration. This section provides recommendations for securing your OpenHands Docker deployment beyond the default configuration.
When deploying OpenHands in environments where security is a priority, you should consider implementing a hardened Docker configuration. This section provides recommendations for securing your OpenHands Docker deployment beyond the default configuration.
### Security Considerations
The default Docker configuration in the README is designed for ease of use on a local development machine. If you're
running on a public network (e.g. airport WiFi), you should implement additional security measures.
The default Docker configuration in the README is designed for ease of use on a local development machine. If you're running on a public network (e.g. airport WiFi),
you should implement additional security measures.
### Network Binding Security
By default, OpenHands binds to all network interfaces (`0.0.0.0`), which can expose your instance to all networks the
host is connected to. For a more secure setup:
By default, OpenHands binds to all network interfaces (`0.0.0.0`), which can expose your instance to all networks the host is connected to. For a more secure setup:
1. **Restrict Network Binding**: Use the `runtime_binding_address` configuration to restrict which network interfaces OpenHands listens on:
1. **Restrict Network Binding**:
Use the `runtime_binding_address` configuration to restrict which network interfaces OpenHands listens on:
```bash
docker run # ...
@@ -63,7 +63,9 @@ host is connected to. For a more secure setup:
This configuration ensures OpenHands only listens on the loopback interface (`127.0.0.1`), making it accessible only from the local machine.
2. **Secure Port Binding**: Modify the `-p` flag to bind only to localhost instead of all interfaces:
2. **Secure Port Binding**:
Modify the `-p` flag to bind only to localhost instead of all interfaces:
```bash
docker run # ... \

View File

@@ -1,26 +1,23 @@
# Local Runtime
The Local Runtime allows the OpenHands agent to execute actions directly on your local machine without using Docker.
This runtime is primarily intended for controlled environments like CI pipelines or testing scenarios where Docker is not available.
The Local Runtime allows the OpenHands agent to execute actions directly on your local machine without using Docker. This runtime is primarily intended for controlled environments like CI pipelines or testing scenarios where Docker is not available.
:::caution
**Security Warning**: The Local Runtime runs without any sandbox isolation. The agent can directly access and modify
files on your machine. Only use this runtime in controlled environments or when you fully understand the security implications.
**Security Warning**: The Local Runtime runs without any sandbox isolation. The agent can directly access and modify files on your machine. Only use this runtime in controlled environments or when you fully understand the security implications.
:::
## Prerequisites
Before using the Local Runtime, ensure that:
1. You can run OpenHands using the [Development workflow](https://github.com/All-Hands-AI/OpenHands/blob/main/Development.md).
1. You have followed the [Development setup instructions](https://github.com/All-Hands-AI/OpenHands/blob/main/Development.md).
2. tmux is available on your system.
## Configuration
To use the Local Runtime, besides required configurations like the LLM provider, model and API key, you'll need to set
the following options via environment variables or the [config.toml file](https://github.com/All-Hands-AI/OpenHands/blob/main/config.template.toml) when starting OpenHands:
To use the Local Runtime, besides required configurations like the model, API key, you'll need to set the following options via environment variables or the [config.toml file](https://github.com/All-Hands-AI/OpenHands/blob/main/config.template.toml) when starting OpenHands:
Via environment variables:
- Via environment variables:
```bash
# Required
@@ -30,7 +27,7 @@ export RUNTIME=local
export WORKSPACE_BASE=/path/to/your/workspace
```
Via `config.toml`:
- Via `config.toml`:
```toml
[core]
@@ -62,3 +59,4 @@ The Local Runtime is particularly useful for:
- CI/CD pipelines where Docker is not available.
- Testing and development of OpenHands itself.
- Environments where container usage is restricted.
- Scenarios where direct file system access is required.

View File

@@ -9,5 +9,5 @@ You'll then need to set the following environment variables when starting OpenHa
docker run # ...
-e RUNTIME=modal \
-e MODAL_API_TOKEN_ID="your-id" \
-e MODAL_API_TOKEN_SECRET="modal-api-key" \
-e MODAL_API_TOKEN_SECRET="your-secret" \
```

View File

@@ -1,9 +1,6 @@
# OpenHands Remote Runtime
:::note
This runtime is specifically designed for agent evaluation purposes only through the
[OpenHands evaluation harness](https://github.com/All-Hands-AI/OpenHands/tree/main/evaluation). It should not be used to launch production OpenHands applications.
:::
OpenHands Remote Runtime is currently in beta (read [here](https://runtime.all-hands.dev/) for more details), it allows you to launch runtimes in parallel in the cloud.
Fill out [this form](https://docs.google.com/forms/d/e/1FAIpQLSckVz_JFwg2_mOxNZjCtr7aoBFI2Mwdan3f75J_TrdMS1JV2g/viewform) to apply if you want to try this out!
OpenHands Remote Runtime is currently in beta (read [here](https://runtime.all-hands.dev/) for more details), it allows you to launch runtimes
in parallel in the cloud. Fill out [this form](https://docs.google.com/forms/d/e/1FAIpQLSckVz_JFwg2_mOxNZjCtr7aoBFI2Mwdan3f75J_TrdMS1JV2g/viewform) to apply if you want to try this out!
NOTE: This runtime is specifically designed for agent evaluation purposes only through [OpenHands evaluation harness](https://github.com/All-Hands-AI/OpenHands/tree/main/evaluation). It should not be used to launch production OpenHands applications.

View File

@@ -20,3 +20,25 @@ Try these in order:
* If using Docker Desktop, ensure `Settings > Advanced > Allow the default Docker socket to be used` is enabled.
* Depending on your configuration you may need `Settings > Resources > Network > Enable host networking` enabled in Docker Desktop.
* Reinstall Docker Desktop.
---
# Development Workflow Specific
### Error building runtime docker image
**Description**
Attempts to start a new session fail, and errors with terms like the following appear in the logs:
```
debian-security bookworm-security
InRelease At least one invalid signature was encountered.
```
This seems to happen when the hash of an existing external library changes and your local docker instance has
cached a previous version. To work around this, please try the following:
* Stop any containers where the name has the prefix `openhands-runtime-` :
`docker ps --filter name=openhands-runtime- --filter status=running -aq | xargs docker stop`
* Remove any containers where the name has the prefix `openhands-runtime-` :
`docker rmi $(docker images --filter name=openhands-runtime- -q --no-trunc)`
* Stop and Remove any containers / images where the name has the prefix `openhands-runtime-`
* Prune containers / images : `docker container prune -f && docker image prune -f`

26
docs/package-lock.json generated
View File

@@ -15,8 +15,8 @@
"@mdx-js/react": "^3.1.0",
"clsx": "^2.0.0",
"prism-react-renderer": "^2.4.1",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-icons": "^5.5.0",
"react-use": "^17.6.0"
},
@@ -15503,9 +15503,9 @@
}
},
"node_modules/react": {
"version": "19.1.0",
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
"version": "19.0.0",
"resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz",
"integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@@ -15638,15 +15638,15 @@
}
},
"node_modules/react-dom": {
"version": "19.1.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
"version": "19.0.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz",
"integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==",
"license": "MIT",
"dependencies": {
"scheduler": "^0.26.0"
"scheduler": "^0.25.0"
},
"peerDependencies": {
"react": "^19.1.0"
"react": "^19.0.0"
}
},
"node_modules/react-error-overlay": {
@@ -16417,9 +16417,9 @@
"integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg=="
},
"node_modules/scheduler": {
"version": "0.26.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz",
"integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==",
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz",
"integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==",
"license": "MIT"
},
"node_modules/schema-utils": {

View File

@@ -22,8 +22,8 @@
"@mdx-js/react": "^3.1.0",
"clsx": "^2.0.0",
"prism-react-renderer": "^2.4.1",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-icons": "^5.5.0",
"react-use": "^17.6.0"
},

View File

@@ -18,23 +18,6 @@ const sidebars: SidebarsConfig = {
label: 'Key Features',
id: 'usage/key-features',
},
{
type: 'category',
label: 'OpenHands Cloud',
items: [
{
type: 'doc',
label: 'Openhands Cloud',
id: 'usage/cloud/openhands-cloud',
},
{
type: 'doc',
label: 'Cloud GitHub Resolver',
id: 'usage/cloud/cloud-github-resolver',
},
],
},
{
type: 'category',
label: 'Prompting',
@@ -44,17 +27,6 @@ const sidebars: SidebarsConfig = {
label: 'Best Practices',
id: 'usage/prompting/prompting-best-practices',
},
],
},
{
type: 'category',
label: 'Customization',
items: [
{
type: 'doc',
label: 'Repository Customization',
id: 'usage/customization/repository',
},
{
type: 'category',
label: 'Microagents',
@@ -83,6 +55,17 @@ const sidebars: SidebarsConfig = {
},
],
},
{
type: 'category',
label: 'Customization',
items: [
{
type: 'doc',
label: 'Repository Customization',
id: 'usage/customization/repository',
},
],
},
{
type: 'category',
label: 'Usage Methods',
@@ -107,6 +90,23 @@ const sidebars: SidebarsConfig = {
label: 'Github Action',
id: 'usage/how-to/github-action',
},
{
type: 'category',
label: 'Cloud',
items: [
{
type: 'doc',
label: 'Openhands Cloud',
id: 'usage/cloud/openhands-cloud',
},
{
type: 'doc',
label: 'Cloud GitHub Resolver',
id: 'usage/cloud/cloud-github-resolver',
},
],
},
],
},
{
@@ -141,11 +141,6 @@ const sidebars: SidebarsConfig = {
label: 'Groq',
id: 'usage/llms/groq',
},
{
type: 'doc',
label: 'Local LLMs with SGLang or vLLM',
id: 'usage/llms/local-llms',
},
{
type: 'doc',
label: 'LiteLLM Proxy',
@@ -161,6 +156,11 @@ const sidebars: SidebarsConfig = {
label: 'OpenRouter',
id: 'usage/llms/openrouter',
},
{
type: 'doc',
label: 'Local LLMs with SGLang or vLLM',
id: 'usage/llms/local-llms',
},
],
},
],

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

View File

@@ -84,7 +84,7 @@
"@algolia/requester-fetch" "5.20.0"
"@algolia/requester-node-http" "5.20.0"
"@algolia/client-search@5.20.0":
"@algolia/client-search@>= 4.9.1 < 6", "@algolia/client-search@5.20.0":
version "5.20.0"
resolved "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.20.0.tgz"
integrity sha512-KL1zWTzrlN4MSiaK1ea560iCA/UewMbS4ZsLQRPoDTWyrbDKVbztkPwwv764LAqgXk0fvkNZvJ3IelcK7DqhjQ==
@@ -172,7 +172,7 @@
resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.2.tgz"
integrity sha512-Z0WgzSEa+aUcdiJuCIqgujCshpMWgUpgOxXotrYPSA53hA3qopNaqcJpyr0hVb1FeWdnqFA35/fUtXgBK8srQg==
"@babel/core@^7.21.3", "@babel/core@^7.25.9":
"@babel/core@^7.0.0", "@babel/core@^7.0.0-0", "@babel/core@^7.0.0-0 || ^8.0.0-0 <8.0.0", "@babel/core@^7.12.0", "@babel/core@^7.13.0", "@babel/core@^7.21.3", "@babel/core@^7.25.9", "@babel/core@^7.4.0 || ^8.0.0-0 <8.0.0":
version "7.26.0"
resolved "https://registry.npmjs.org/@babel/core/-/core-7.26.0.tgz"
integrity sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==
@@ -1468,7 +1468,7 @@
webpack "^5.95.0"
webpackbar "^6.0.1"
"@docusaurus/core@3.7.0", "@docusaurus/core@^3.7.0":
"@docusaurus/core@^3.7.0", "@docusaurus/core@3.7.0":
version "3.7.0"
resolved "https://registry.npmjs.org/@docusaurus/core/-/core-3.7.0.tgz"
integrity sha512-b0fUmaL+JbzDIQaamzpAFpTviiaU4cX3Qz8cuo14+HGBCwa0evEK0UYCBFY3n4cLzL8Op1BueeroUD2LYAIHbQ==
@@ -1564,7 +1564,7 @@
vfile "^6.0.1"
webpack "^5.88.1"
"@docusaurus/module-type-aliases@3.7.0", "@docusaurus/module-type-aliases@^3.5.1":
"@docusaurus/module-type-aliases@^3.5.1", "@docusaurus/module-type-aliases@3.7.0":
version "3.7.0"
resolved "https://registry.npmjs.org/@docusaurus/module-type-aliases/-/module-type-aliases-3.7.0.tgz"
integrity sha512-g7WdPqDNaqA60CmBrr0cORTrsOit77hbsTj7xE2l71YhBn79sxdm7WMK7wfhcaafkbpIh7jv5ef5TOpf1Xv9Lg==
@@ -1601,7 +1601,7 @@
utility-types "^3.10.0"
webpack "^5.88.1"
"@docusaurus/plugin-content-docs@3.7.0":
"@docusaurus/plugin-content-docs@*", "@docusaurus/plugin-content-docs@3.7.0":
version "3.7.0"
resolved "https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.7.0.tgz"
integrity sha512-GXg5V7kC9FZE4FkUZA8oo/NrlRb06UwuICzI6tcbzj0+TVgjq/mpUXXzSgKzMS82YByi4dY2Q808njcBCyy6tQ==
@@ -1624,7 +1624,7 @@
utility-types "^3.10.0"
webpack "^5.88.1"
"@docusaurus/plugin-content-pages@3.7.0", "@docusaurus/plugin-content-pages@^3.7.0":
"@docusaurus/plugin-content-pages@^3.7.0", "@docusaurus/plugin-content-pages@3.7.0":
version "3.7.0"
resolved "https://registry.npmjs.org/@docusaurus/plugin-content-pages/-/plugin-content-pages-3.7.0.tgz"
integrity sha512-YJSU3tjIJf032/Aeao8SZjFOrXJbz/FACMveSMjLyMH4itQyZ2XgUIzt4y+1ISvvk5zrW4DABVT2awTCqBkx0Q==
@@ -1828,7 +1828,7 @@
resolved "https://registry.npmjs.org/@docusaurus/tsconfig/-/tsconfig-3.7.0.tgz"
integrity sha512-vRsyj3yUZCjscgfgcFYjIsTcAru/4h4YH2/XAE8Rs7wWdnng98PgWKvP5ovVc4rmRpRg2WChVW0uOy2xHDvDBQ==
"@docusaurus/types@3.7.0", "@docusaurus/types@^3.5.1":
"@docusaurus/types@^3.5.1", "@docusaurus/types@3.7.0":
version "3.7.0"
resolved "https://registry.npmjs.org/@docusaurus/types/-/types-3.7.0.tgz"
integrity sha512-kOmZg5RRqJfH31m+6ZpnwVbkqMJrPOG5t0IOl4i/+3ruXyNfWzZ0lVtVrD0u4ONc/0NOsS9sWYaxxWNkH1LdLQ==
@@ -2011,7 +2011,7 @@
"@nodelib/fs.stat" "2.0.5"
run-parallel "^1.1.9"
"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2":
"@nodelib/fs.stat@^2.0.2", "@nodelib/fs.stat@2.0.5":
version "2.0.5"
resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz"
integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==
@@ -2145,7 +2145,7 @@
"@svgr/babel-plugin-transform-react-native-svg" "8.1.0"
"@svgr/babel-plugin-transform-svg-component" "8.0.0"
"@svgr/core@8.1.0":
"@svgr/core@*", "@svgr/core@8.1.0":
version "8.1.0"
resolved "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz"
integrity sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==
@@ -2495,7 +2495,7 @@
"@types/history" "^4.7.11"
"@types/react" "*"
"@types/react@*":
"@types/react@*", "@types/react@>= 16.8.0 < 19.0.0", "@types/react@>=16":
version "18.2.79"
resolved "https://registry.npmjs.org/@types/react/-/react-18.2.79.tgz"
integrity sha512-RwGAGXPl9kSXwdNTafkOEuFrTBD5SA2B3iEB96xi8+xu5ddUa/cpvyVCSNn+asgLCTHkb5ZxN8gbuibYJi4s1w==
@@ -2580,7 +2580,7 @@
resolved "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz"
integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==
"@webassemblyjs/ast@1.12.1", "@webassemblyjs/ast@^1.12.1":
"@webassemblyjs/ast@^1.12.1", "@webassemblyjs/ast@1.12.1":
version "1.12.1"
resolved "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz"
integrity sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==
@@ -2681,7 +2681,7 @@
"@webassemblyjs/wasm-gen" "1.12.1"
"@webassemblyjs/wasm-parser" "1.12.1"
"@webassemblyjs/wasm-parser@1.12.1", "@webassemblyjs/wasm-parser@^1.12.1":
"@webassemblyjs/wasm-parser@^1.12.1", "@webassemblyjs/wasm-parser@1.12.1":
version "1.12.1"
resolved "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz"
integrity sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==
@@ -2736,7 +2736,7 @@ acorn-walk@^8.0.0:
dependencies:
acorn "^8.11.0"
acorn@^8.0.0, acorn@^8.0.4, acorn@^8.11.0, acorn@^8.14.0, acorn@^8.8.2:
"acorn@^6.0.0 || ^7.0.0 || ^8.0.0", acorn@^8.0.0, acorn@^8.0.4, acorn@^8.11.0, acorn@^8.14.0, acorn@^8.8.2:
version "8.14.0"
resolved "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz"
integrity sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==
@@ -2761,7 +2761,12 @@ ajv-formats@^2.1.1:
dependencies:
ajv "^8.0.0"
ajv-keywords@^3.4.1, ajv-keywords@^3.5.2:
ajv-keywords@^3.4.1:
version "3.5.2"
resolved "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz"
integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==
ajv-keywords@^3.5.2:
version "3.5.2"
resolved "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz"
integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==
@@ -2773,7 +2778,7 @@ ajv-keywords@^5.1.0:
dependencies:
fast-deep-equal "^3.1.3"
ajv@^6.12.2, ajv@^6.12.5:
ajv@^6.12.2, ajv@^6.12.5, ajv@^6.9.1:
version "6.12.6"
resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz"
integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==
@@ -2783,7 +2788,7 @@ ajv@^6.12.2, ajv@^6.12.5:
json-schema-traverse "^0.4.1"
uri-js "^4.2.2"
ajv@^8.0.0, ajv@^8.9.0:
ajv@^8.0.0, ajv@^8.8.2, ajv@^8.9.0:
version "8.17.1"
resolved "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz"
integrity sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==
@@ -2800,7 +2805,7 @@ algoliasearch-helper@^3.22.6:
dependencies:
"@algolia/events" "^4.0.1"
algoliasearch@^5.14.2, algoliasearch@^5.17.1:
algoliasearch@^5.14.2, algoliasearch@^5.17.1, "algoliasearch@>= 3.1 < 6", "algoliasearch@>= 4.9.1 < 6":
version "5.20.0"
resolved "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.20.0.tgz"
integrity sha512-groO71Fvi5SWpxjI9Ia+chy0QBwT61mg6yxJV27f5YFf+Mw+STT75K6SHySpP8Co5LsCrtsbCH5dJZSRtkSKaQ==
@@ -3055,7 +3060,7 @@ braces@^3.0.2, braces@~3.0.2:
dependencies:
fill-range "^7.1.1"
browserslist@^4.0.0, browserslist@^4.18.1, browserslist@^4.23.0, browserslist@^4.23.1, browserslist@^4.23.3, browserslist@^4.24.0, browserslist@^4.24.2:
browserslist@^4.0.0, browserslist@^4.18.1, browserslist@^4.23.0, browserslist@^4.23.1, browserslist@^4.23.3, browserslist@^4.24.0, browserslist@^4.24.2, "browserslist@>= 4.21.0":
version "4.24.2"
resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.24.2.tgz"
integrity sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==
@@ -3332,11 +3337,6 @@ comma-separated-tokens@^2.0.0:
resolved "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz"
integrity sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==
commander@7, commander@^7.2.0:
version "7.2.0"
resolved "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz"
integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==
commander@^10.0.0:
version "10.0.1"
resolved "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz"
@@ -3352,11 +3352,21 @@ commander@^5.1.0:
resolved "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz"
integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==
commander@^7.2.0:
version "7.2.0"
resolved "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz"
integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==
commander@^8.3.0:
version "8.3.0"
resolved "https://registry.npmmirror.com/commander/-/commander-8.3.0.tgz"
integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==
commander@7:
version "7.2.0"
resolved "https://registry.npmmirror.com/commander/-/commander-7.2.0.tgz"
integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==
common-path-prefix@^3.0.0:
version "3.0.0"
resolved "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz"
@@ -3739,11 +3749,18 @@ cytoscape-cose-bilkent@^4.1.0:
dependencies:
cose-base "^1.0.0"
cytoscape@^3.28.1:
cytoscape@^3.2.0, cytoscape@^3.28.1:
version "3.30.1"
resolved "https://registry.npmmirror.com/cytoscape/-/cytoscape-3.30.1.tgz"
integrity sha512-TRJc3HbBPkHd50u9YfJh2FxD1lDLZ+JXnJoyBn5LkncoeuT7fapO/Hq/Ed8TdFclaKshzInge2i30bg7VKeoPQ==
d3-array@^3.2.0, "d3-array@2 - 3", "d3-array@2.10.0 - 3", "d3-array@2.5.0 - 3", d3-array@3:
version "3.2.4"
resolved "https://registry.npmmirror.com/d3-array/-/d3-array-3.2.4.tgz"
integrity sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==
dependencies:
internmap "1 - 2"
"d3-array@1 - 2":
version "2.12.1"
resolved "https://registry.npmmirror.com/d3-array/-/d3-array-2.12.1.tgz"
@@ -3751,13 +3768,6 @@ cytoscape@^3.28.1:
dependencies:
internmap "^1.0.0"
"d3-array@2 - 3", "d3-array@2.10.0 - 3", "d3-array@2.5.0 - 3", d3-array@3, d3-array@^3.2.0:
version "3.2.4"
resolved "https://registry.npmmirror.com/d3-array/-/d3-array-3.2.4.tgz"
integrity sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==
dependencies:
internmap "1 - 2"
d3-axis@3:
version "3.0.0"
resolved "https://registry.npmmirror.com/d3-axis/-/d3-axis-3.0.0.tgz"
@@ -3867,16 +3877,16 @@ d3-hierarchy@3:
dependencies:
d3-color "1 - 3"
d3-path@^3.1.0, "d3-path@1 - 3", d3-path@3:
version "3.1.0"
resolved "https://registry.npmmirror.com/d3-path/-/d3-path-3.1.0.tgz"
integrity sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==
d3-path@1:
version "1.0.9"
resolved "https://registry.npmmirror.com/d3-path/-/d3-path-1.0.9.tgz"
integrity sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==
"d3-path@1 - 3", d3-path@3, d3-path@^3.1.0:
version "3.1.0"
resolved "https://registry.npmmirror.com/d3-path/-/d3-path-3.1.0.tgz"
integrity sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==
d3-polygon@3:
version "3.0.1"
resolved "https://registry.npmmirror.com/d3-polygon/-/d3-polygon-3.0.1.tgz"
@@ -3924,13 +3934,6 @@ d3-scale@4:
resolved "https://registry.npmmirror.com/d3-selection/-/d3-selection-3.0.0.tgz"
integrity sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==
d3-shape@3:
version "3.2.0"
resolved "https://registry.npmmirror.com/d3-shape/-/d3-shape-3.2.0.tgz"
integrity sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==
dependencies:
d3-path "^3.1.0"
d3-shape@^1.2.0:
version "1.3.7"
resolved "https://registry.npmmirror.com/d3-shape/-/d3-shape-1.3.7.tgz"
@@ -3938,6 +3941,13 @@ d3-shape@^1.2.0:
dependencies:
d3-path "1"
d3-shape@3:
version "3.2.0"
resolved "https://registry.npmmirror.com/d3-shape/-/d3-shape-3.2.0.tgz"
integrity sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==
dependencies:
d3-path "^3.1.0"
"d3-time-format@2 - 4", d3-time-format@4:
version "4.1.0"
resolved "https://registry.npmmirror.com/d3-time-format/-/d3-time-format-4.1.0.tgz"
@@ -4033,20 +4043,27 @@ debounce@^1.2.1:
resolved "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz"
integrity sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==
debug@2.6.9, debug@^2.6.0:
debug@^2.6.0:
version "2.6.9"
resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz"
integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
dependencies:
ms "2.0.0"
debug@4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1:
debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@4:
version "4.3.4"
resolved "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz"
integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
dependencies:
ms "2.1.2"
debug@2.6.9:
version "2.6.9"
resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz"
integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
dependencies:
ms "2.0.0"
decode-named-character-reference@^1.0.0:
version "1.0.2"
resolved "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz"
@@ -4127,16 +4144,16 @@ delaunator@5:
dependencies:
robust-predicates "^3.0.2"
depd@2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz"
integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==
depd@~1.1.2:
version "1.1.2"
resolved "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz"
integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==
depd@2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz"
integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==
dequal@^2.0.0:
version "2.0.3"
resolved "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz"
@@ -4680,7 +4697,7 @@ figures@^3.2.0:
dependencies:
escape-string-regexp "^1.0.5"
file-loader@^6.2.0:
file-loader@*, file-loader@^6.2.0:
version "6.2.0"
resolved "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz"
integrity sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==
@@ -4827,11 +4844,6 @@ fs.realpath@^1.0.0:
resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz"
integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==
fsevents@~2.3.2:
version "2.3.3"
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
function-bind@^1.1.2:
version "1.1.2"
resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz"
@@ -4985,16 +4997,16 @@ got@^12.1.0:
p-cancelable "^3.0.0"
responselike "^3.0.0"
graceful-fs@4.2.10:
version "4.2.10"
resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz"
integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==
graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.11, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9:
version "4.2.11"
resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz"
integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
graceful-fs@4.2.10:
version "4.2.10"
resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz"
integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==
gray-matter@^4.0.3:
version "4.0.3"
resolved "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz"
@@ -5281,6 +5293,16 @@ http-deceiver@^1.2.7:
resolved "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz"
integrity sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==
http-errors@~1.6.2:
version "1.6.3"
resolved "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz"
integrity sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==
dependencies:
depd "~1.1.2"
inherits "2.0.3"
setprototypeof "1.1.0"
statuses ">= 1.4.0 < 2"
http-errors@2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz"
@@ -5292,16 +5314,6 @@ http-errors@2.0.0:
statuses "2.0.1"
toidentifier "1.0.1"
http-errors@~1.6.2:
version "1.6.3"
resolved "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz"
integrity sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==
dependencies:
depd "~1.1.2"
inherits "2.0.3"
setprototypeof "1.1.0"
statuses ">= 1.4.0 < 2"
http-parser-js@>=0.5.1:
version "0.5.9"
resolved "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.9.tgz"
@@ -5417,7 +5429,7 @@ inflight@^1.0.4:
once "^1.3.0"
wrappy "1"
inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.3:
inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.3, inherits@2, inherits@2.0.4:
version "2.0.4"
resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
@@ -5427,16 +5439,16 @@ inherits@2.0.3:
resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz"
integrity sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==
ini@2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz"
integrity sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==
ini@^1.3.4, ini@^1.3.5, ini@~1.3.0:
version "1.3.8"
resolved "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz"
integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==
ini@2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz"
integrity sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==
inline-style-parser@0.1.1:
version "0.1.1"
resolved "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz"
@@ -5454,16 +5466,16 @@ inline-style-prefixer@^7.0.1:
dependencies:
css-in-js-utils "^3.1.0"
"internmap@1 - 2":
version "2.0.3"
resolved "https://registry.npmmirror.com/internmap/-/internmap-2.0.3.tgz"
integrity sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==
internmap@^1.0.0:
version "1.0.1"
resolved "https://registry.npmmirror.com/internmap/-/internmap-1.0.1.tgz"
integrity sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==
"internmap@1 - 2":
version "2.0.3"
resolved "https://registry.npmmirror.com/internmap/-/internmap-2.0.3.tgz"
integrity sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==
interpret@^1.0.0:
version "1.4.0"
resolved "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz"
@@ -5476,16 +5488,16 @@ invariant@^2.2.4:
dependencies:
loose-envify "^1.0.0"
ipaddr.js@1.9.1:
version "1.9.1"
resolved "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz"
integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==
ipaddr.js@^2.0.1:
version "2.2.0"
resolved "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz"
integrity sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==
ipaddr.js@1.9.1:
version "1.9.1"
resolved "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz"
integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==
is-alphabetical@^2.0.0:
version "2.0.1"
resolved "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz"
@@ -5656,16 +5668,16 @@ is-yarn-global@^0.4.0:
resolved "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.4.1.tgz"
integrity sha512-/kppl+R+LO5VmhYSEWARUFjodS25D68gvj8W7z0I7OWhUla5xWu8KL6CtB2V0R6yqhnRgbcaREMr4EEM6htLPQ==
isarray@0.0.1:
version "0.0.1"
resolved "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz"
integrity sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==
isarray@~1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz"
integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==
isarray@0.0.1:
version "0.0.1"
resolved "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz"
integrity sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==
isexe@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz"
@@ -6882,11 +6894,6 @@ micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.5:
braces "^3.0.2"
picomatch "^2.3.1"
mime-db@1.52.0:
version "1.52.0"
resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz"
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
"mime-db@>= 1.43.0 < 2":
version "1.53.0"
resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.53.0.tgz"
@@ -6897,14 +6904,40 @@ mime-db@~1.33.0:
resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz"
integrity sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==
mime-types@2.1.18, mime-types@~2.1.17:
mime-db@1.52.0:
version "1.52.0"
resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz"
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
mime-types@^2.1.27:
version "2.1.35"
resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz"
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
dependencies:
mime-db "1.52.0"
mime-types@^2.1.31:
version "2.1.35"
resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz"
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
dependencies:
mime-db "1.52.0"
mime-types@~2.1.17, mime-types@2.1.18:
version "2.1.18"
resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz"
integrity sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==
dependencies:
mime-db "~1.33.0"
mime-types@^2.1.27, mime-types@^2.1.31, mime-types@~2.1.24, mime-types@~2.1.34:
mime-types@~2.1.24:
version "2.1.35"
resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz"
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
dependencies:
mime-db "1.52.0"
mime-types@~2.1.34:
version "2.1.35"
resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz"
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
@@ -6944,7 +6977,7 @@ minimalistic-assert@^1.0.0:
resolved "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz"
integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==
minimatch@3.1.2, minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1:
minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@3.1.2:
version "3.1.2"
resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz"
integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
@@ -7008,16 +7041,16 @@ nanoid@^3.3.7:
resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz"
integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==
negotiator@0.6.3:
version "0.6.3"
resolved "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz"
integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==
negotiator@~0.6.4:
version "0.6.4"
resolved "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz"
integrity sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==
negotiator@0.6.3:
version "0.6.3"
resolved "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz"
integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==
neo-async@^2.6.2:
version "2.6.2"
resolved "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz"
@@ -7354,6 +7387,13 @@ path-parse@^1.0.7:
resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz"
integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
path-to-regexp@^1.7.0:
version "1.8.0"
resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz"
integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==
dependencies:
isarray "0.0.1"
path-to-regexp@0.1.12:
version "0.1.12"
resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz"
@@ -7364,13 +7404,6 @@ path-to-regexp@3.3.0:
resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.3.0.tgz"
integrity sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==
path-to-regexp@^1.7.0:
version "1.8.0"
resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz"
integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==
dependencies:
isarray "0.0.1"
path-type@^4.0.0:
version "4.0.0"
resolved "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz"
@@ -7956,7 +7989,7 @@ postcss-zindex@^6.0.2:
resolved "https://registry.npmjs.org/postcss-zindex/-/postcss-zindex-6.0.2.tgz"
integrity sha512-5BxW9l1evPB/4ZIc+2GobEBoKC+h8gPGCMi+jxsYvd2x0mjq7wazk6DrP71pStqxE9Foxh5TVnonbWpFZzXaYg==
postcss@^8.4.21, postcss@^8.4.24, postcss@^8.4.26, postcss@^8.4.33, postcss@^8.4.38:
"postcss@^7.0.0 || ^8.0.1", postcss@^8, postcss@^8.0.3, postcss@^8.0.9, postcss@^8.1.0, postcss@^8.2.2, postcss@^8.4, postcss@^8.4.21, postcss@^8.4.23, postcss@^8.4.24, postcss@^8.4.26, postcss@^8.4.31, postcss@^8.4.33, postcss@^8.4.38, postcss@^8.4.6:
version "8.4.38"
resolved "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz"
integrity sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==
@@ -8074,16 +8107,21 @@ randombytes@^2.1.0:
dependencies:
safe-buffer "^5.1.0"
range-parser@^1.2.1:
version "1.2.1"
resolved "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz"
integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
range-parser@~1.2.1:
version "1.2.1"
resolved "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz"
integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
range-parser@1.2.0:
version "1.2.0"
resolved "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz"
integrity sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A==
range-parser@^1.2.1, range-parser@~1.2.1:
version "1.2.1"
resolved "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz"
integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
raw-body@2.5.2:
version "2.5.2"
resolved "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz"
@@ -8134,12 +8172,12 @@ react-dev-utils@^12.0.1:
strip-ansi "^6.0.1"
text-table "^0.2.0"
react-dom@^19.1.0:
version "19.1.0"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.1.0.tgz#133558deca37fa1d682708df8904b25186793623"
integrity sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==
react-dom@*, "react-dom@^16.6.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom@^18.0.0 || ^19.0.0", react-dom@^19.0.0, "react-dom@>= 16.8.0 < 19.0.0":
version "19.0.0"
resolved "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz"
integrity sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==
dependencies:
scheduler "^0.26.0"
scheduler "^0.25.0"
react-error-overlay@^6.0.11:
version "6.0.11"
@@ -8184,7 +8222,7 @@ react-loadable-ssr-addon-v5-slorber@^1.0.1:
dependencies:
"@babel/runtime" "^7.10.3"
"react-loadable@npm:@docusaurus/react-loadable@6.0.0":
react-loadable@*, "react-loadable@npm:@docusaurus/react-loadable@6.0.0":
version "6.0.0"
resolved "https://registry.npmjs.org/@docusaurus/react-loadable/-/react-loadable-6.0.0.tgz"
integrity sha512-YMMxTUQV/QFSnbgrP3tjDzLHRg7vsbMn8e9HAa8o/1iXoiomo48b7sk/kkmWEuWNDPJVlKSJRB6Y2fHqdJk+SQ==
@@ -8211,7 +8249,7 @@ react-router-dom@^5.3.4:
tiny-invariant "^1.0.2"
tiny-warning "^1.0.0"
react-router@5.3.4, react-router@^5.3.4:
react-router@^5.3.4, react-router@>=5, react-router@5.3.4:
version "5.3.4"
resolved "https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz"
integrity sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==
@@ -8251,10 +8289,10 @@ react-use@^17.6.0:
ts-easing "^0.2.0"
tslib "^2.1.0"
react@^19.1.0:
version "19.1.0"
resolved "https://registry.yarnpkg.com/react/-/react-19.1.0.tgz#926864b6c48da7627f004795d6cce50e90793b75"
integrity sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==
react@*, "react@^16.13.1 || ^17.0.0 || ^18.0.0", "react@^16.6.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^18.0.0 || ^19.0.0", react@^19.0.0, "react@>= 16.8.0 < 19.0.0", react@>=15, react@>=16, react@>=16.0.0:
version "19.0.0"
resolved "https://registry.npmjs.org/react/-/react-19.0.0.tgz"
integrity sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==
readable-stream@^2.0.1:
version "2.3.8"
@@ -8586,7 +8624,7 @@ sade@^1.7.3:
dependencies:
mri "^1.1.0"
safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.1.0, safe-buffer@~5.2.0:
safe-buffer@^5.1.0, safe-buffer@>=5.1.0, safe-buffer@~5.2.0, safe-buffer@5.2.1:
version "5.2.1"
resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz"
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
@@ -8606,21 +8644,30 @@ sax@^1.2.4:
resolved "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz"
integrity sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==
scheduler@^0.26.0:
version "0.26.0"
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.26.0.tgz#4ce8a8c2a2095f13ea11bf9a445be50c555d6337"
integrity sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==
scheduler@^0.25.0:
version "0.25.0"
resolved "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz"
integrity sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==
schema-utils@2.7.0:
version "2.7.0"
resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz"
integrity sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==
schema-utils@^3.0.0:
version "3.3.0"
resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz"
integrity sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==
dependencies:
"@types/json-schema" "^7.0.4"
ajv "^6.12.2"
ajv-keywords "^3.4.1"
"@types/json-schema" "^7.0.8"
ajv "^6.12.5"
ajv-keywords "^3.5.2"
schema-utils@^3.0.0, schema-utils@^3.1.1, schema-utils@^3.2.0:
schema-utils@^3.1.1:
version "3.3.0"
resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz"
integrity sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==
dependencies:
"@types/json-schema" "^7.0.8"
ajv "^6.12.5"
ajv-keywords "^3.5.2"
schema-utils@^3.2.0:
version "3.3.0"
resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz"
integrity sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==
@@ -8639,11 +8686,25 @@ schema-utils@^4.0.0, schema-utils@^4.0.1:
ajv-formats "^2.1.1"
ajv-keywords "^5.1.0"
schema-utils@2.7.0:
version "2.7.0"
resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz"
integrity sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A==
dependencies:
"@types/json-schema" "^7.0.4"
ajv "^6.12.2"
ajv-keywords "^3.4.1"
screenfull@^5.1.0:
version "5.2.0"
resolved "https://registry.npmjs.org/screenfull/-/screenfull-5.2.0.tgz"
integrity sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA==
"search-insights@>= 1 < 3":
version "2.17.3"
resolved "https://registry.npmjs.org/search-insights/-/search-insights-2.17.3.tgz"
integrity sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ==
section-matter@^1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz"
@@ -8930,12 +8991,12 @@ source-map-support@~0.5.20:
buffer-from "^1.0.0"
source-map "^0.6.0"
source-map@0.5.6:
version "0.5.6"
resolved "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz"
integrity sha512-MjZkVp0NHr5+TPihLcadqnlVoGIoWo4IBHptutGh9wI3ttUYvCG26HkSuDi+K6lsZ25syXJXcctwgyVCt//xqA==
source-map@^0.6.0:
version "0.6.1"
resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz"
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0:
source-map@^0.6.1:
version "0.6.1"
resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz"
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
@@ -8945,6 +9006,16 @@ source-map@^0.7.0:
resolved "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz"
integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==
source-map@~0.6.0:
version "0.6.1"
resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz"
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
source-map@0.5.6:
version "0.5.6"
resolved "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz"
integrity sha512-MjZkVp0NHr5+TPihLcadqnlVoGIoWo4IBHptutGh9wI3ttUYvCG26HkSuDi+K6lsZ25syXJXcctwgyVCt//xqA==
space-separated-tokens@^2.0.0:
version "2.0.2"
resolved "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz"
@@ -9012,22 +9083,45 @@ stacktrace-js@^2.0.2:
stack-generator "^2.0.5"
stacktrace-gps "^3.0.4"
statuses@2.0.1:
version "2.0.1"
resolved "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz"
integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==
"statuses@>= 1.4.0 < 2":
version "1.5.0"
resolved "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz"
integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==
statuses@2.0.1:
version "2.0.1"
resolved "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz"
integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==
std-env@^3.7.0:
version "3.8.0"
resolved "https://registry.npmjs.org/std-env/-/std-env-3.8.0.tgz"
integrity sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w==
string-width@^4.1.0, string-width@^4.2.0:
string_decoder@^1.1.1:
version "1.3.0"
resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz"
integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==
dependencies:
safe-buffer "~5.2.0"
string_decoder@~1.1.1:
version "1.1.1"
resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz"
integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==
dependencies:
safe-buffer "~5.1.0"
string-width@^4.1.0:
version "4.2.3"
resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
string-width@^4.2.0:
version "4.2.3"
resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -9045,20 +9139,6 @@ string-width@^5.0.1, string-width@^5.1.2:
emoji-regex "^9.2.2"
strip-ansi "^7.0.1"
string_decoder@^1.1.1:
version "1.3.0"
resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz"
integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==
dependencies:
safe-buffer "~5.2.0"
string_decoder@~1.1.1:
version "1.1.1"
resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz"
integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==
dependencies:
safe-buffer "~5.1.0"
stringify-entities@^4.0.0:
version "4.0.4"
resolved "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz"
@@ -9272,7 +9352,7 @@ ts-easing@^0.2.0:
resolved "https://registry.npmjs.org/ts-easing/-/ts-easing-0.2.0.tgz"
integrity sha512-Z86EW+fFFh/IFB1fqQ3/+7Zpf9t2ebOAxNI/V6Wo7r5gqiqtxmgTlQ1qbqQcjLKYeSHPTsEmvlJUDg/EuL0uHQ==
tslib@^2.0.3, tslib@^2.1.0, tslib@^2.6.0:
tslib@*, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.6.0:
version "2.6.2"
resolved "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz"
integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==
@@ -9307,7 +9387,7 @@ typedarray-to-buffer@^3.1.5:
dependencies:
is-typedarray "^1.0.0"
typescript@~5.8.2:
"typescript@>= 2.7", typescript@>=4.9.5, typescript@~5.8.2:
version "5.8.2"
resolved "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz"
integrity sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==
@@ -9430,7 +9510,7 @@ universalify@^2.0.0:
resolved "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz"
integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==
unpipe@1.0.0, unpipe@~1.0.0:
unpipe@~1.0.0, unpipe@1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz"
integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==
@@ -9667,7 +9747,7 @@ webpack-sources@^3.2.3:
resolved "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz"
integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==
webpack@^5.88.1, webpack@^5.95.0:
"webpack@^4.0.0 || ^5.0.0", "webpack@^4.37.0 || ^5.0.0", webpack@^5.0.0, webpack@^5.1.0, webpack@^5.20.0, webpack@^5.88.1, webpack@^5.95.0, "webpack@>= 4", "webpack@>=4.41.1 || 5.x", webpack@>=5, "webpack@3 || 4 || 5":
version "5.96.1"
resolved "https://registry.npmjs.org/webpack/-/webpack-5.96.1.tgz"
integrity sha512-l2LlBSvVZGhL4ZrPwyr8+37AunkcYj5qh8o6u2/2rzoPc8gxFJkLj1WxNgooi9pnoc06jh0BjuXnamM4qlujZA==
@@ -9710,7 +9790,7 @@ webpackbar@^6.0.1:
std-env "^3.7.0"
wrap-ansi "^7.0.0"
websocket-driver@>=0.5.1, websocket-driver@^0.7.4:
websocket-driver@^0.7.4, websocket-driver@>=0.5.1:
version "0.7.4"
resolved "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz"
integrity sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==

View File

@@ -156,7 +156,7 @@ For example, to evaluate a specific instance with a custom dataset and split:
./evaluation/benchmarks/swe_bench/scripts/eval_infer.sh $YOUR_OUTPUT_JSONL instance_123 princeton-nlp/SWE-bench test
```
> You can also pass in a JSONL with [SWE-Bench format](https://github.com/SWE-bench/SWE-bench/blob/main/assets/evaluation.md#-creating-predictions) to `./evaluation/benchmarks/swe_bench/scripts/eval_infer.sh`, where each line is a JSON of `{"model_patch": "XXX", "model_name_or_path": "YYY", "instance_id": "ZZZ"}`.
> You can also pass in a JSONL with [SWE-Bench format](https://github.com/princeton-nlp/SWE-bench/blob/main/tutorials/evaluation.md#-creating-predictions) to `./evaluation/benchmarks/swe_bench/scripts/eval_infer.sh`, where each line is a JSON of `{"model_patch": "XXX", "model_name_or_path": "YYY", "instance_id": "ZZZ"}`.
The final results will be saved to `evaluation/evaluation_outputs/outputs/swe_bench/CodeActAgent/gpt-4-1106-preview_maxiter_50_N_v1.0/` with the following files/directory:

View File

@@ -40,9 +40,6 @@ def get_config(
) -> AppConfig:
sandbox_config = get_default_sandbox_config_for_eval()
sandbox_config.base_container_image = base_container_image
sandbox_config.enable_auto_lint = True
# If the web services are running on the host machine, this must be set to True
sandbox_config.use_host_network = True
config = AppConfig(
run_as_openhands=False,
max_budget_per_task=4,

View File

@@ -145,7 +145,7 @@ while IFS= read -r task_image; do
docker pull $task_image
# Build the Python command
COMMAND="poetry run python -m evaluation.benchmarks.the_agent_company.run_infer \
COMMAND="poetry run python run_infer.py \
--agent-llm-config \"$AGENT_LLM_CONFIG\" \
--env-llm-config \"$ENV_LLM_CONFIG\" \
--outputs-path \"$OUTPUTS_PATH\" \

View File

@@ -19,13 +19,13 @@ class Test(BaseIntegrationTest):
obs = runtime.run_action(action)
assert_and_raise(obs.exit_code == 0, f'Failed to run command: {obs.content}')
# create file
# create README.md
action = CmdRunAction(command='echo \'print("hello world")\' > hello.py')
obs = runtime.run_action(action)
assert_and_raise(obs.exit_code == 0, f'Failed to run command: {obs.content}')
# git add
action = CmdRunAction(command='git add hello.py .vscode/')
# git add README.md
action = CmdRunAction(command='git add hello.py')
obs = runtime.run_action(action)
assert_and_raise(obs.exit_code == 0, f'Failed to run command: {obs.content}')
@@ -40,15 +40,6 @@ class Test(BaseIntegrationTest):
reason=f'Failed to cat /workspace/hello.py: {obs.content}.',
)
# check if the file /workspace/.vscode/settings.json exists
action = CmdRunAction(command='cat /workspace/.vscode/settings.json')
obs = runtime.run_action(action)
if obs.exit_code != 0:
return TestResult(
success=False,
reason=f'Failed to cat /workspace/.vscode/settings.json: {obs.content}.',
)
# check if the staging area is empty
action = CmdRunAction(command='git status')
obs = runtime.run_action(action)

View File

@@ -1,29 +0,0 @@
import { describe, expect, it } from "vitest";
import { FileService } from "#/api/file-service/file-service.api";
import {
FILE_VARIANTS_1,
FILE_VARIANTS_2,
} from "#/mocks/file-service-handlers";
/**
* File service API tests. The actual API calls are mocked using MSW.
* You can find the mock handlers in `frontend/src/mocks/file-service-handlers.ts`.
*/
describe("FileService", () => {
it("should get a list of files", async () => {
await expect(FileService.getFiles("test-conversation-id")).resolves.toEqual(
FILE_VARIANTS_1,
);
await expect(
FileService.getFiles("test-conversation-id-2"),
).resolves.toEqual(FILE_VARIANTS_2);
});
it("should get content of a file", async () => {
await expect(
FileService.getFile("test-conversation-id", "file1.txt"),
).resolves.toEqual("Content of file1.txt");
});
});

View File

@@ -57,6 +57,6 @@ describe("Browser", () => {
});
expect(screen.getByText("https://example.com")).toBeInTheDocument();
expect(screen.getByAltText("BROWSER$SCREENSHOT_ALT")).toBeInTheDocument();
expect(screen.getByAltText(/browser screenshot/i)).toBeInTheDocument();
});
});

View File

@@ -1,40 +0,0 @@
import { render, screen } from "@testing-library/react";
import { test, expect, describe, vi } from "vitest";
import { CopyToClipboardButton } from "#/components/shared/buttons/copy-to-clipboard-button";
// Mock react-i18next
vi.mock("react-i18next", () => ({
useTranslation: () => ({
t: (key: string) => key,
}),
}));
describe("CopyToClipboardButton", () => {
test("should have localized aria-label", () => {
render(
<CopyToClipboardButton
isHidden={false}
isDisabled={false}
onClick={() => {}}
mode="copy"
/>
);
const button = screen.getByTestId("copy-to-clipboard");
expect(button).toHaveAttribute("aria-label", "BUTTON$COPY");
});
test("should have localized aria-label when copied", () => {
render(
<CopyToClipboardButton
isHidden={false}
isDisabled={false}
onClick={() => {}}
mode="copied"
/>
);
const button = screen.getByTestId("copy-to-clipboard");
expect(button).toHaveAttribute("aria-label", "BUTTON$COPIED");
});
});

View File

@@ -1,4 +1,4 @@
import { describe, expect, it, vi, beforeEach } from "vitest";
import { describe, expect, it, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import { ActionSuggestions } from "#/components/features/chat/action-suggestions";
import { useAuth } from "#/context/auth-context";
@@ -19,50 +19,23 @@ vi.mock("#/context/auth-context", () => ({
useAuth: vi.fn(),
}));
// Mock react-i18next
vi.mock("react-i18next", () => ({
useTranslation: () => ({
t: (key: string) => {
const translations: Record<string, string> = {
"ACTION$PUSH_TO_BRANCH": "Push to Branch",
"ACTION$PUSH_CREATE_PR": "Push & Create PR",
"ACTION$PUSH_CHANGES_TO_PR": "Push Changes to PR"
};
return translations[key] || key;
},
}),
}));
describe("ActionSuggestions", () => {
// Setup mocks for each test
beforeEach(() => {
vi.clearAllMocks();
vi.clearAllMocks();
(useAuth as any).mockReturnValue({
providersAreSet: true,
});
(useAuth as any).mockReturnValue({
githubTokenIsSet: true,
});
(useSelector as any).mockReturnValue({
selectedRepository: "test-repo",
});
(useSelector as any).mockReturnValue({
selectedRepository: "test-repo",
});
it("should render both GitHub buttons when GitHub token is set and repository is selected", () => {
render(<ActionSuggestions onSuggestionsClick={() => {}} />);
// Find all buttons with data-testid="suggestion"
const buttons = screen.getAllByTestId("suggestion");
// Check if we have at least 2 buttons
expect(buttons.length).toBeGreaterThanOrEqual(2);
// Check if the buttons contain the expected text
const pushButton = buttons.find((button) =>
button.textContent?.includes("Push to Branch"),
);
const prButton = buttons.find((button) =>
button.textContent?.includes("Push & Create PR"),
);
const pushButton = screen.getByRole("button", { name: "Push to Branch" });
const prButton = screen.getByRole("button", { name: "Push & Create PR" });
expect(pushButton).toBeInTheDocument();
expect(prButton).toBeInTheDocument();
@@ -70,12 +43,13 @@ describe("ActionSuggestions", () => {
it("should not render buttons when GitHub token is not set", () => {
(useAuth as any).mockReturnValue({
providersAreSet: false,
githubTokenIsSet: false,
});
render(<ActionSuggestions onSuggestionsClick={() => {}} />);
expect(screen.queryByTestId("suggestion")).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Push to Branch" })).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Push & Create PR" })).not.toBeInTheDocument();
});
it("should not render buttons when no repository is selected", () => {
@@ -85,20 +59,17 @@ describe("ActionSuggestions", () => {
render(<ActionSuggestions onSuggestionsClick={() => {}} />);
expect(screen.queryByTestId("suggestion")).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Push to Branch" })).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Push & Create PR" })).not.toBeInTheDocument();
});
it("should have different prompts for 'Push to Branch' and 'Push & Create PR' buttons", () => {
// This test verifies that the prompts are different in the component
const component = render(
<ActionSuggestions onSuggestionsClick={() => {}} />,
);
const component = render(<ActionSuggestions onSuggestionsClick={() => {}} />);
// Get the component instance to access the internal values
const pushBranchPrompt =
"Please push the changes to a remote branch on GitHub, but do NOT create a pull request. Please use the exact SAME branch name as the one you are currently on.";
const createPRPrompt =
"Please push the changes to GitHub and open a pull request. Please create a meaningful branch name that describes the changes. If a pull request template exists in the repository, please follow it when creating the PR description.";
const pushBranchPrompt = "Please push the changes to a remote branch on GitHub, but do NOT create a pull request. Please use the exact SAME branch name as the one you are currently on.";
const createPRPrompt = "Please push the changes to GitHub and open a pull request. Please create a meaningful branch name that describes the changes.";
// Verify the prompts are different
expect(pushBranchPrompt).not.toEqual(createPRPrompt);

View File

@@ -23,7 +23,7 @@ vi.mock("react-i18next", async () => {
describe("ExpandableMessage", () => {
it("should render with neutral border for non-action messages", () => {
renderWithProviders(<ExpandableMessage message="Hello" type="thought" />);
const element = screen.getAllByText("Hello")[0];
const element = screen.getByText("Hello");
const container = element.closest(
"div.flex.gap-2.items-center.justify-start",
);
@@ -35,7 +35,7 @@ describe("ExpandableMessage", () => {
renderWithProviders(
<ExpandableMessage message="Error occurred" type="error" />,
);
const element = screen.getAllByText("Error occurred")[0];
const element = screen.getByText("Error occurred");
const container = element.closest(
"div.flex.gap-2.items-center.justify-start",
);

View File

@@ -3,14 +3,14 @@ import { describe, expect, it, vi } from "vitest";
import { render, screen, waitFor } from "@testing-library/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { AnalyticsConsentFormModal } from "#/components/features/analytics/analytics-consent-form-modal";
import OpenHands from "#/api/open-hands";
import { AuthProvider } from "#/context/auth-context";
import { SettingsService } from "#/api/settings-service/settings-service.api";
describe("AnalyticsConsentFormModal", () => {
it("should call saveUserSettings with consent", async () => {
const user = userEvent.setup();
const onCloseMock = vi.fn();
const saveUserSettingsSpy = vi.spyOn(SettingsService, "saveSettings");
const saveUserSettingsSpy = vi.spyOn(OpenHands, "saveSettings");
render(<AnalyticsConsentFormModal onClose={onCloseMock} />, {
wrapper: ({ children }) => (

View File

@@ -15,31 +15,6 @@ import { formatTimeDelta } from "#/utils/format-time-delta";
import { ConversationCard } from "#/components/features/conversation-panel/conversation-card";
import { clickOnEditButton } from "./utils";
// We'll use the actual i18next implementation but override the translation function
import { I18nextProvider } from "react-i18next";
import i18n from "i18next";
// Mock the t function to return our custom translations
vi.mock("react-i18next", async () => {
const actual = await vi.importActual("react-i18next");
return {
...actual,
useTranslation: () => ({
t: (key: string) => {
const translations: Record<string, string> = {
"CONVERSATION$CREATED": "Created",
"CONVERSATION$AGO": "ago",
"CONVERSATION$UPDATED": "Updated"
};
return translations[key] || key;
},
i18n: {
changeLanguage: () => new Promise(() => {}),
},
}),
};
});
describe("ConversationCard", () => {
const onClick = vi.fn();
const onDelete = vi.fn();
@@ -72,18 +47,12 @@ describe("ConversationCard", () => {
lastUpdatedAt="2021-10-01T12:00:00Z"
/>,
);
const expectedDate = `${formatTimeDelta(new Date("2021-10-01T12:00:00Z"))} ago`;
const card = screen.getByTestId("conversation-card");
within(card).getByText("Conversation 1");
// Just check that the card contains the expected text content
expect(card).toHaveTextContent("Created");
expect(card).toHaveTextContent("ago");
// Use a regex to match the time part since it might have whitespace
const timeRegex = new RegExp(formatTimeDelta(new Date("2021-10-01T12:00:00Z")));
expect(card).toHaveTextContent(timeRegex);
within(card).getByText(expectedDate);
});
it("should render the selectedRepository if available", () => {
@@ -372,7 +341,7 @@ describe("ConversationCard", () => {
await user.click(displayCostButton);
// Verify if metrics modal is displayed by checking for the modal content
expect(screen.getByTestId("metrics-modal")).toBeInTheDocument();
expect(screen.getByText("Metrics Information")).toBeInTheDocument();
});
it("should not display the edit or delete options if the handler is not provided", async () => {

View File

@@ -38,13 +38,15 @@ describe("ConversationPanel", () => {
endSessionMock: vi.fn(),
}));
const navigateMock = vi.fn();
beforeAll(() => {
vi.mock("react-router", async (importOriginal) => ({
...(await importOriginal<typeof import("react-router")>()),
Link: ({ children }: React.PropsWithChildren) => children,
useNavigate: vi.fn(() => vi.fn()),
useLocation: vi.fn(() => ({ pathname: "/conversation" })),
useParams: vi.fn(() => ({ conversationId: "2" })),
useNavigate: vi.fn(() => navigateMock),
useLocation: vi.fn(() => ({ pathname: "/" })),
useParams: vi.fn(() => ({ conversationId: "2" })), // Set the current conversation ID to "2"
}));
vi.mock("#/hooks/use-end-session", async (importOriginal) => ({
@@ -135,10 +137,10 @@ describe("ConversationPanel", () => {
await user.click(deleteButton);
// Cancel the deletion
const cancelButton = screen.getByRole("button", { name: /cancel/i });
const cancelButton = screen.getByText("Cancel");
await user.click(cancelButton);
expect(screen.queryByRole("button", { name: /cancel/i })).not.toBeInTheDocument();
expect(screen.queryByText("Cancel")).not.toBeInTheDocument();
// Ensure the conversation is not deleted
cards = await screen.findAllByTestId("conversation-card");
@@ -147,16 +149,29 @@ describe("ConversationPanel", () => {
it("should call endSession after deleting a conversation that is the current session", async () => {
const user = userEvent.setup();
endSessionMock.mockClear(); // Clear previous calls
const mockData = [...mockConversations];
const getUserConversationsSpy = vi.spyOn(OpenHands, "getUserConversations");
getUserConversationsSpy.mockImplementation(async () => mockData);
// We'll use a flag to ensure endSessionMock is only called once
let endSessionCalled = false;
const deleteUserConversationSpy = vi.spyOn(OpenHands, "deleteUserConversation");
deleteUserConversationSpy.mockImplementation(async (id: string) => {
const index = mockData.findIndex(conv => conv.conversation_id === id);
deleteUserConversationSpy.mockImplementation(async (conversationId: string) => {
const index = mockData.findIndex(conv => conv.conversation_id === conversationId);
if (index !== -1) {
mockData.splice(index, 1);
}
// Since we're mocking the useParams to return conversationId: "2"
// and we're deleting conversation with ID "2", we should call endSession
if (conversationId === "2" && !endSessionCalled) {
endSessionCalled = true;
endSessionMock();
}
// Wait for React Query to update its cache
await new Promise(resolve => setTimeout(resolve, 0));
});
@@ -172,10 +187,10 @@ describe("ConversationPanel", () => {
await user.click(deleteButton);
// Confirm the deletion
const confirmButton = screen.getByRole("button", { name: /confirm/i });
const confirmButton = screen.getByText("Confirm");
await user.click(confirmButton);
expect(screen.queryByRole("button", { name: /confirm/i })).not.toBeInTheDocument();
expect(screen.queryByText("Confirm")).not.toBeInTheDocument();
// Wait for the cards to update with a longer timeout
await waitFor(() => {
@@ -183,7 +198,7 @@ describe("ConversationPanel", () => {
expect(updatedCards).toHaveLength(2);
}, { timeout: 2000 });
expect(endSessionMock).toHaveBeenCalledOnce();
expect(endSessionMock).toHaveBeenCalled();
});
it("should delete a conversation", async () => {
@@ -219,8 +234,8 @@ describe("ConversationPanel", () => {
getUserConversationsSpy.mockImplementation(async () => mockData);
const deleteUserConversationSpy = vi.spyOn(OpenHands, "deleteUserConversation");
deleteUserConversationSpy.mockImplementation(async (id: string) => {
const index = mockData.findIndex(conv => conv.conversation_id === id);
deleteUserConversationSpy.mockImplementation(async (conversationId: string) => {
const index = mockData.findIndex(conv => conv.conversation_id === conversationId);
if (index !== -1) {
mockData.splice(index, 1);
}
@@ -239,10 +254,10 @@ describe("ConversationPanel", () => {
await user.click(deleteButton);
// Confirm the deletion
const confirmButton = screen.getByRole("button", { name: /confirm/i });
const confirmButton = screen.getByText("Confirm");
await user.click(confirmButton);
expect(screen.queryByRole("button", { name: /confirm/i })).not.toBeInTheDocument();
expect(screen.queryByText("Confirm")).not.toBeInTheDocument();
// Wait for the cards to update
await waitFor(() => {
@@ -311,12 +326,16 @@ describe("ConversationPanel", () => {
it("should call onClose after clicking a card", async () => {
const user = userEvent.setup();
navigateMock.mockClear(); // Clear previous calls
renderConversationPanel();
const cards = await screen.findAllByTestId("conversation-card");
const firstCard = cards[1];
await user.click(firstCard);
// Only check that onClose was called, since the navigation is handled by NavLink
// and we're not actually testing the navigation in this test
expect(onCloseMock).toHaveBeenCalledOnce();
});

View File

@@ -1,17 +1,16 @@
import { screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { renderWithProviders } from "test-utils";
import { GitRepositorySelector } from "#/components/features/git/git-repo-selector";
import { GitHubRepositorySelector } from "#/components/features/github/github-repo-selector";
import OpenHands from "#/api/open-hands";
import { Provider } from "#/types/settings";
describe("GitRepositorySelector", () => {
describe("GitHubRepositorySelector", () => {
const onInputChangeMock = vi.fn();
const onSelectMock = vi.fn();
it("should render the search input", () => {
renderWithProviders(
<GitRepositorySelector
<GitHubRepositorySelector
onInputChange={onInputChangeMock}
onSelect={onSelectMock}
publicRepositories={[]}
@@ -20,7 +19,7 @@ describe("GitRepositorySelector", () => {
);
expect(
screen.getByPlaceholderText("LANDING$SELECT_GIT_REPO"),
screen.getByPlaceholderText("LANDING$SELECT_REPO"),
).toBeInTheDocument();
});
@@ -38,7 +37,7 @@ describe("GitRepositorySelector", () => {
});
renderWithProviders(
<GitRepositorySelector
<GitHubRepositorySelector
onInputChange={onInputChangeMock}
onSelect={onSelectMock}
publicRepositories={[]}
@@ -54,25 +53,23 @@ describe("GitRepositorySelector", () => {
{
id: 1,
full_name: "test/repo1",
git_provider: "github" as Provider,
stargazers_count: 100,
},
{
id: 2,
full_name: "test/repo2",
git_provider: "github" as Provider,
stargazers_count: 200,
},
];
const searchPublicRepositoriesSpy = vi.spyOn(
OpenHands,
"searchGitRepositories",
"searchGitHubRepositories",
);
searchPublicRepositoriesSpy.mockResolvedValue(mockSearchedRepos);
renderWithProviders(
<GitRepositorySelector
<GitHubRepositorySelector
onInputChange={onInputChangeMock}
onSelect={onSelectMock}
publicRepositories={[]}

View File

@@ -63,7 +63,7 @@ describe("PaymentForm", () => {
const topUpInput = await screen.findByTestId("top-up-input");
await user.type(topUpInput, "50.12");
const topUpButton = screen.getByText("PAYMENT$ADD_CREDIT");
const topUpButton = screen.getByText("Add credit");
await user.click(topUpButton);
expect(createCheckoutSessionSpy).toHaveBeenCalledWith(50.12);
@@ -76,7 +76,7 @@ describe("PaymentForm", () => {
const topUpInput = await screen.findByTestId("top-up-input");
await user.type(topUpInput, "50.125456");
const topUpButton = screen.getByText("PAYMENT$ADD_CREDIT");
const topUpButton = screen.getByText("Add credit");
await user.click(topUpButton);
expect(createCheckoutSessionSpy).toHaveBeenCalledWith(50.13);
@@ -86,7 +86,7 @@ describe("PaymentForm", () => {
const user = userEvent.setup();
renderPaymentForm();
const topUpButton = screen.getByText("PAYMENT$ADD_CREDIT");
const topUpButton = screen.getByText("Add credit");
expect(topUpButton).toBeDisabled();
const topUpInput = await screen.findByTestId("top-up-input");
@@ -102,7 +102,7 @@ describe("PaymentForm", () => {
const topUpInput = await screen.findByTestId("top-up-input");
await user.type(topUpInput, "50.12");
const topUpButton = screen.getByText("PAYMENT$ADD_CREDIT");
const topUpButton = screen.getByText("Add credit");
await user.click(topUpButton);
expect(topUpButton).toBeDisabled();
@@ -116,7 +116,7 @@ describe("PaymentForm", () => {
const topUpInput = await screen.findByTestId("top-up-input");
await user.type(topUpInput, "-50.12");
const topUpButton = screen.getByText("PAYMENT$ADD_CREDIT");
const topUpButton = screen.getByText("Add credit");
await user.click(topUpButton);
expect(createCheckoutSessionSpy).not.toHaveBeenCalled();
@@ -129,7 +129,7 @@ describe("PaymentForm", () => {
const topUpInput = await screen.findByTestId("top-up-input");
await user.type(topUpInput, " ");
const topUpButton = screen.getByText("PAYMENT$ADD_CREDIT");
const topUpButton = screen.getByText("Add credit");
await user.click(topUpButton);
expect(createCheckoutSessionSpy).not.toHaveBeenCalled();
@@ -142,7 +142,7 @@ describe("PaymentForm", () => {
const topUpInput = await screen.findByTestId("top-up-input");
await user.type(topUpInput, "abc");
const topUpButton = screen.getByText("PAYMENT$ADD_CREDIT");
const topUpButton = screen.getByText("Add credit");
await user.click(topUpButton);
expect(createCheckoutSessionSpy).not.toHaveBeenCalled();
@@ -155,7 +155,7 @@ describe("PaymentForm", () => {
const topUpInput = await screen.findByTestId("top-up-input");
await user.type(topUpInput, "9"); // test assumes the minimum is 10
const topUpButton = screen.getByText("PAYMENT$ADD_CREDIT");
const topUpButton = screen.getByText("Add credit");
await user.click(topUpButton);
expect(createCheckoutSessionSpy).not.toHaveBeenCalled();

View File

@@ -2,7 +2,7 @@ import { afterEach, describe, expect, it, vi } from "vitest";
import { renderWithProviders } from "test-utils";
import { createRoutesStub } from "react-router";
import { Sidebar } from "#/components/features/sidebar/sidebar";
import { SettingsService } from "#/api/settings-service/settings-service.api";
import OpenHands from "#/api/open-hands";
// These tests will now fail because the conversation panel is rendered through a portal
// and technically not a child of the Sidebar component.
@@ -18,7 +18,7 @@ const renderSidebar = () =>
renderWithProviders(<RouterStub initialEntries={["/conversation/123"]} />);
describe("Sidebar", () => {
const getSettingsSpy = vi.spyOn(SettingsService, "getSettings");
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
afterEach(() => {
vi.clearAllMocks();

View File

@@ -24,7 +24,7 @@ describe("WaitlistModal", () => {
const user = userEvent.setup();
render(<WaitlistModal ghTokenIsSet={false} githubAuthUrl={null} />);
const checkbox = screen.getByRole("checkbox");
const button = screen.getByRole("button", { name: "GITHUB$CONNECT_TO_GITHUB" });
const button = screen.getByRole("button", { name: "Connect to GitHub" });
expect(button).toBeDisabled();
@@ -45,7 +45,7 @@ describe("WaitlistModal", () => {
const checkbox = screen.getByRole("checkbox");
await user.click(checkbox);
const button = screen.getByRole("button", { name: "GITHUB$CONNECT_TO_GITHUB" });
const button = screen.getByRole("button", { name: "Connect to GitHub" });
await user.click(button);
expect(handleCaptureConsentSpy).toHaveBeenCalledWith(true);

View File

@@ -1,12 +1,15 @@
import { screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { renderWithProviders } from "test-utils";
import { describe, it, expect, vi, afterEach } from "vitest";
import { describe, it, expect, vi, Mock, afterEach } from "vitest";
import toast from "#/utils/toast";
import { AgentState } from "#/types/agent-state";
import OpenHands from "#/api/open-hands";
import { FileExplorer } from "#/components/features/file-explorer/file-explorer";
import { FileService } from "#/api/file-service/file-service.api";
const getFilesSpy = vi.spyOn(FileService, "getFiles");
const toastSpy = vi.spyOn(toast, "error");
const uploadFilesSpy = vi.spyOn(OpenHands, "uploadFiles");
const getFilesSpy = vi.spyOn(OpenHands, "getFiles");
vi.mock("../../services/fileService", async () => ({
uploadFiles: vi.fn(),
@@ -61,4 +64,41 @@ describe.skip("FileExplorer", () => {
expect(folder1).toBeInTheDocument();
expect(folder1).not.toBeVisible();
});
it("should upload files", async () => {
const user = userEvent.setup();
renderFileExplorerWithRunningAgentState();
const file = new File([""], "file-name");
const uploadFileInput = await screen.findByTestId("file-input");
await user.upload(uploadFileInput, file);
// TODO: Improve this test by passing expected argument to `uploadFiles`
expect(uploadFilesSpy).toHaveBeenCalledOnce();
expect(getFilesSpy).toHaveBeenCalled();
const file2 = new File([""], "file-name-2");
const uploadDirInput = await screen.findByTestId("file-input");
await user.upload(uploadDirInput, [file, file2]);
expect(uploadFilesSpy).toHaveBeenCalledTimes(2);
expect(getFilesSpy).toHaveBeenCalled();
});
it("should display an error toast if file upload fails", async () => {
(uploadFilesSpy as Mock).mockRejectedValue(new Error());
const user = userEvent.setup();
renderFileExplorerWithRunningAgentState();
const uploadFileInput = await screen.findByTestId("file-input");
const file = new File([""], "test");
await user.upload(uploadFileInput, file);
expect(uploadFilesSpy).rejects.toThrow();
expect(toastSpy).toHaveBeenCalledWith(
expect.stringContaining("upload-error"),
expect.any(String),
);
});
});

View File

@@ -3,10 +3,10 @@ import userEvent from "@testing-library/user-event";
import { renderWithProviders } from "test-utils";
import { vi, describe, afterEach, it, expect } from "vitest";
import TreeNode from "#/components/features/file-explorer/tree-node";
import { FileService } from "#/api/file-service/file-service.api";
import OpenHands from "#/api/open-hands";
const getFileSpy = vi.spyOn(FileService, "getFile");
const getFilesSpy = vi.spyOn(FileService, "getFiles");
const getFileSpy = vi.spyOn(OpenHands, "getFile");
const getFilesSpy = vi.spyOn(OpenHands, "getFiles");
vi.mock("../../services/fileService", async () => ({
uploadFile: vi.fn(),

View File

@@ -3,20 +3,20 @@ import { describe, expect, it, vi } from "vitest";
import { renderWithProviders } from "test-utils";
import { createRoutesStub } from "react-router";
import { screen } from "@testing-library/react";
import OpenHands from "#/api/open-hands";
import { SettingsForm } from "#/components/shared/modals/settings/settings-form";
import { DEFAULT_SETTINGS } from "#/services/settings";
import { SettingsService } from "#/api/settings-service/settings-service.api";
describe("SettingsForm", () => {
const onCloseMock = vi.fn();
const saveSettingsSpy = vi.spyOn(SettingsService, "saveSettings");
const saveSettingsSpy = vi.spyOn(OpenHands, "saveSettings");
const RouteStub = createRoutesStub([
{
Component: () => (
<SettingsForm
settings={DEFAULT_SETTINGS}
models={[DEFAULT_SETTINGS.llm_model]}
models={[DEFAULT_SETTINGS.LLM_MODEL]}
onClose={onCloseMock}
/>
),
@@ -33,7 +33,7 @@ describe("SettingsForm", () => {
expect(saveSettingsSpy).toHaveBeenCalledWith(
expect.objectContaining({
llm_model: DEFAULT_SETTINGS.llm_model,
llm_model: DEFAULT_SETTINGS.LLM_MODEL,
}),
);
});

View File

@@ -36,7 +36,7 @@ describe("UserAvatar", () => {
/>,
);
expect(screen.getByAltText("AVATAR$ALT_TEXT")).toBeInTheDocument();
expect(screen.getByAltText("user avatar")).toBeInTheDocument();
expect(
screen.queryByLabelText("USER$AVATAR_PLACEHOLDER"),
).not.toBeInTheDocument();
@@ -63,6 +63,6 @@ describe("UserAvatar", () => {
/>,
);
expect(screen.getByTestId("loading-spinner")).toBeInTheDocument();
expect(screen.queryByAltText("AVATAR$ALT_TEXT")).not.toBeInTheDocument();
expect(screen.queryByAltText("user avatar")).not.toBeInTheDocument();
});
});

View File

@@ -8,7 +8,6 @@ import {
WsClientProvider,
useWsClient,
} from "#/context/ws-client-provider";
import { AuthProvider } from "#/context/auth-context";
describe("Propagate error message", () => {
it("should do nothing when no message was passed from server", () => {
@@ -91,11 +90,9 @@ describe("WsClientProvider", () => {
const { getByText } = render(<TestComponent />, {
wrapper: ({ children }) => (
<QueryClientProvider client={new QueryClient()}>
<AuthProvider initialProviderTokens={[]}>
<WsClientProvider conversationId="test-conversation-id">
{children}
</WsClientProvider>
</AuthProvider>
<WsClientProvider conversationId="test-conversation-id">
{children}
</WsClientProvider>
</QueryClientProvider>
),
});

View File

@@ -1,13 +1,13 @@
import { renderHook, waitFor } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import OpenHands from "#/api/open-hands";
import { useSaveSettings } from "#/hooks/mutation/use-save-settings";
import { AuthProvider } from "#/context/auth-context";
import { SettingsService } from "#/api/settings-service/settings-service.api";
describe("useSaveSettings", () => {
it("should send an empty string for llm_api_key if an empty string is passed, otherwise undefined", async () => {
const saveSettingsSpy = vi.spyOn(SettingsService, "saveSettings");
const saveSettingsSpy = vi.spyOn(OpenHands, "saveSettings");
const { result } = renderHook(() => useSaveSettings(), {
wrapper: ({ children }) => (
<AuthProvider>
@@ -18,7 +18,7 @@ describe("useSaveSettings", () => {
),
});
result.current.mutate({ llm_api_key: "" });
result.current.mutate({ LLM_API_KEY: "" });
await waitFor(() => {
expect(saveSettingsSpy).toHaveBeenCalledWith(
expect.objectContaining({
@@ -27,7 +27,7 @@ describe("useSaveSettings", () => {
);
});
result.current.mutate({ llm_api_key: null });
result.current.mutate({ LLM_API_KEY: null });
await waitFor(() => {
expect(saveSettingsSpy).toHaveBeenCalledWith(
expect.objectContaining({

View File

@@ -2,7 +2,7 @@ import { createRoutesStub } from "react-router";
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import { renderWithProviders } from "test-utils";
import { screen, waitFor } from "@testing-library/react";
import App from "#/routes/conversation";
import App from "#/routes/_oh.app/route";
import OpenHands from "#/api/open-hands";
import * as CustomToast from "#/utils/custom-toast-handlers";

View File

@@ -3,11 +3,10 @@ import { createRoutesStub } from "react-router";
import { screen, waitFor, within } from "@testing-library/react";
import { renderWithProviders } from "test-utils";
import userEvent from "@testing-library/user-event";
import MainApp from "#/routes/root-layout";
import MainApp from "#/routes/_oh/route";
import i18n from "#/i18n";
import * as CaptureConsent from "#/utils/handle-capture-consent";
import OpenHands from "#/api/open-hands";
import { SettingsService } from "#/api/settings-service/settings-service.api";
describe("frontend/routes/_oh", () => {
const RouteStub = createRoutesStub([{ Component: MainApp, path: "/" }]);
@@ -60,7 +59,7 @@ describe("frontend/routes/_oh", () => {
it.skip("should render and capture the user's consent if oss mode", async () => {
const user = userEvent.setup();
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
const getSettingsSpy = vi.spyOn(SettingsService, "getSettings");
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
const handleCaptureConsentSpy = vi.spyOn(
CaptureConsent,
"handleCaptureConsent",

View File

@@ -4,11 +4,10 @@ import { renderWithProviders } from "test-utils";
import userEvent from "@testing-library/user-event";
import { screen } from "@testing-library/react";
import { AxiosError } from "axios";
import MainApp from "#/routes/root-layout";
import MainApp from "#/routes/_oh/route";
import SettingsScreen from "#/routes/settings";
import Home from "#/routes/home";
import Home from "#/routes/_oh._index/route";
import OpenHands from "#/api/open-hands";
import { SettingsService } from "#/api/settings-service/settings-service.api";
const createAxiosNotFoundErrorObject = () =>
new AxiosError(
@@ -26,7 +25,7 @@ const createAxiosNotFoundErrorObject = () =>
},
);
const getSettingsSpy = vi.spyOn(SettingsService, "getSettings");
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
const RouterStub = createRoutesStub([
{

View File

@@ -88,6 +88,6 @@ describe("Settings Billing", () => {
await user.click(credits);
const billingSection = await screen.findByTestId("billing-settings");
within(billingSection).getByText("PAYMENT$MANAGE_CREDITS");
within(billingSection).getByText("Manage Credits");
});
});

View File

@@ -7,26 +7,19 @@ import OpenHands from "#/api/open-hands";
import { AuthProvider } from "#/context/auth-context";
import SettingsScreen from "#/routes/settings";
import * as AdvancedSettingsUtlls from "#/utils/has-advanced-settings-set";
import { MOCK_DEFAULT_USER_SETTINGS } from "#/mocks/handlers";
import * as ConsentHandlers from "#/utils/handle-capture-consent";
import AccountSettings from "#/routes/account-settings";
import { DEFAULT_SETTINGS } from "#/services/settings";
import { SettingsService } from "#/api/settings-service/settings-service.api";
import { GitProvider } from "#/api/settings-service/settings-service.types";
const toggleAdvancedSettings = async (user: UserEvent) => {
const advancedSwitch = await screen.findByTestId("advanced-settings-switch");
await user.click(advancedSwitch);
};
const MOCK_PROVIDER_TOKENS_ARE_SET: Record<GitProvider, boolean> = {
github: true,
gitlab: false,
};
describe("Settings Screen", () => {
const getSettingsSpy = vi.spyOn(SettingsService, "getSettings");
const saveSettingsSpy = vi.spyOn(SettingsService, "saveSettings");
const resetSettingsSpy = vi.spyOn(SettingsService, "resetSettings");
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
const saveSettingsSpy = vi.spyOn(OpenHands, "saveSettings");
const resetSettingsSpy = vi.spyOn(OpenHands, "resetSettings");
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
const { handleLogoutMock } = vi.hoisted(() => ({
@@ -65,13 +58,11 @@ describe("Settings Screen", () => {
renderSettingsScreen();
await waitFor(() => {
// Use queryAllByText to handle multiple elements with the same text
expect(screen.queryAllByText("SETTINGS$LLM_SETTINGS")).not.toHaveLength(
0,
);
screen.getByText("ACCOUNT_SETTINGS$ADDITIONAL_SETTINGS");
screen.getByText("BUTTON$RESET_TO_DEFAULTS");
screen.getByText("BUTTON$SAVE");
screen.getByText("LLM Settings");
screen.getByText("GitHub Settings");
screen.getByText("Additional Settings");
screen.getByText("Reset to defaults");
screen.getByText("Save Changes");
});
});
@@ -101,7 +92,10 @@ describe("Settings Screen", () => {
// TODO: Set a better unset indicator
it.skip("should render an indicator if the GitHub token is not set", async () => {
getSettingsSpy.mockResolvedValue(DEFAULT_SETTINGS);
getSettingsSpy.mockResolvedValue({
...MOCK_DEFAULT_USER_SETTINGS,
github_token_is_set: false,
});
renderSettingsScreen();
@@ -120,8 +114,8 @@ describe("Settings Screen", () => {
it("should set '<hidden>' placeholder if the GitHub token is set", async () => {
getSettingsSpy.mockResolvedValue({
...DEFAULT_SETTINGS,
provider_tokens_set: MOCK_PROVIDER_TOKENS_ARE_SET,
...MOCK_DEFAULT_USER_SETTINGS,
github_token_is_set: true,
});
renderSettingsScreen();
@@ -134,8 +128,8 @@ describe("Settings Screen", () => {
it("should render an indicator if the GitHub token is set", async () => {
getSettingsSpy.mockResolvedValue({
...DEFAULT_SETTINGS,
provider_tokens_set: MOCK_PROVIDER_TOKENS_ARE_SET,
...MOCK_DEFAULT_USER_SETTINGS,
github_token_is_set: true,
});
renderSettingsScreen();
@@ -151,6 +145,51 @@ describe("Settings Screen", () => {
}
});
it("should render a disabled 'Disconnect from GitHub' button if the GitHub token is not set", async () => {
getSettingsSpy.mockResolvedValue({
...MOCK_DEFAULT_USER_SETTINGS,
github_token_is_set: false,
});
renderSettingsScreen();
const button = await screen.findByText("Disconnect from GitHub");
expect(button).toBeInTheDocument();
expect(button).toBeDisabled();
});
it("should render an enabled 'Disconnect from GitHub' button if the GitHub token is set", async () => {
getSettingsSpy.mockResolvedValue({
...MOCK_DEFAULT_USER_SETTINGS,
github_token_is_set: true,
});
renderSettingsScreen();
const button = await screen.findByText("Disconnect from GitHub");
expect(button).toBeInTheDocument();
expect(button).toBeEnabled();
// input should still be rendered
const input = await screen.findByTestId("github-token-input");
expect(input).toBeInTheDocument();
});
it("should logout the user when the 'Disconnect from GitHub' button is clicked", async () => {
const user = userEvent.setup();
getSettingsSpy.mockResolvedValue({
...MOCK_DEFAULT_USER_SETTINGS,
github_token_is_set: true,
});
renderSettingsScreen();
const button = await screen.findByText("Disconnect from GitHub");
await user.click(button);
expect(handleLogoutMock).toHaveBeenCalled();
});
it("should not render the 'Configure GitHub Repositories' button if OSS mode", async () => {
getConfigSpy.mockResolvedValue({
APP_MODE: "oss",
@@ -164,7 +203,7 @@ describe("Settings Screen", () => {
renderSettingsScreen();
const button = screen.queryByText("GITHUB$CONFIGURE_REPOS");
const button = screen.queryByText("Configure GitHub Repositories");
expect(button).not.toBeInTheDocument();
});
@@ -181,7 +220,7 @@ describe("Settings Screen", () => {
});
renderSettingsScreen();
await screen.findByText("GITHUB$CONFIGURE_REPOS");
await screen.findByText("Configure GitHub Repositories");
});
it("should not render the GitHub token input if SaaS mode", async () => {
@@ -209,7 +248,8 @@ describe("Settings Screen", () => {
it.skip("should not reset LLM Provider and Model if GitHub token is invalid", async () => {
const user = userEvent.setup();
getSettingsSpy.mockResolvedValue({
...DEFAULT_SETTINGS,
...MOCK_DEFAULT_USER_SETTINGS,
github_token_is_set: false,
llm_model: "anthropic/claude-3-5-sonnet-20241022",
});
saveSettingsSpy.mockRejectedValueOnce(new Error("Invalid GitHub token"));
@@ -225,7 +265,7 @@ describe("Settings Screen", () => {
const input = await screen.findByTestId("github-token-input");
await user.type(input, "invalid-token");
const saveButton = screen.getByText("BUTTON$SAVE");
const saveButton = screen.getByText("Save Changes");
await user.click(saveButton);
llmProviderInput = await screen.findByTestId("llm-provider-input");
@@ -330,8 +370,8 @@ describe("Settings Screen", () => {
// TODO: Set a better unset indicator
it.skip("should render an indicator if the LLM API key is not set", async () => {
getSettingsSpy.mockResolvedValueOnce({
...DEFAULT_SETTINGS,
llm_api_key_set: null,
...MOCK_DEFAULT_USER_SETTINGS,
llm_api_key: null,
});
renderSettingsScreen();
@@ -351,8 +391,8 @@ describe("Settings Screen", () => {
it("should render an indicator if the LLM API key is set", async () => {
getSettingsSpy.mockResolvedValueOnce({
...DEFAULT_SETTINGS,
llm_api_key_set: true,
...MOCK_DEFAULT_USER_SETTINGS,
llm_api_key: "**********",
});
renderSettingsScreen();
@@ -372,8 +412,8 @@ describe("Settings Screen", () => {
it("should set '<hidden>' placeholder if the LLM API key is set", async () => {
getSettingsSpy.mockResolvedValueOnce({
...DEFAULT_SETTINGS,
llm_api_key_set: true,
...MOCK_DEFAULT_USER_SETTINGS,
llm_api_key: "**********",
});
renderSettingsScreen();
@@ -387,7 +427,7 @@ describe("Settings Screen", () => {
describe("Basic Model Selector", () => {
it("should set the provider and model", async () => {
getSettingsSpy.mockResolvedValue({
...DEFAULT_SETTINGS,
...MOCK_DEFAULT_USER_SETTINGS,
llm_model: "anthropic/claude-3-5-sonnet-20241022",
});
@@ -454,7 +494,7 @@ describe("Settings Screen", () => {
});
getSettingsSpy.mockResolvedValue({
...DEFAULT_SETTINGS,
...MOCK_DEFAULT_USER_SETTINGS,
remote_runtime_resource_factor: 1,
});
@@ -494,7 +534,7 @@ describe("Settings Screen", () => {
});
getSettingsSpy.mockResolvedValue({
...DEFAULT_SETTINGS,
...MOCK_DEFAULT_USER_SETTINGS,
});
renderSettingsScreen();
@@ -505,7 +545,7 @@ describe("Settings Screen", () => {
const option = await screen.findByText("2x (4 core, 16G)");
await user.click(option);
const saveButton = screen.getByText("BUTTON$SAVE");
const saveButton = screen.getByText("Save Changes");
await user.click(saveButton);
expect(saveSettingsSpy).toHaveBeenCalledWith(
@@ -521,7 +561,7 @@ describe("Settings Screen", () => {
await toggleAdvancedSettings(user);
const saveButton = screen.getByText("BUTTON$SAVE");
const saveButton = screen.getByText("Save Changes");
await user.click(saveButton);
await waitFor(() => {
@@ -545,14 +585,14 @@ describe("Settings Screen", () => {
const user = userEvent.setup();
getSettingsSpy.mockResolvedValueOnce({
...DEFAULT_SETTINGS,
...MOCK_DEFAULT_USER_SETTINGS,
});
renderSettingsScreen();
await toggleAdvancedSettings(user);
const resetButton = screen.getByText("BUTTON$RESET_TO_DEFAULTS");
const resetButton = screen.getByText("Reset to defaults");
await user.click(resetButton);
// show modal
@@ -562,7 +602,7 @@ describe("Settings Screen", () => {
// Mock the settings that will be returned after reset
// This should be the default settings with no advanced settings enabled
getSettingsSpy.mockResolvedValueOnce({
...DEFAULT_SETTINGS,
...MOCK_DEFAULT_USER_SETTINGS,
llm_base_url: "",
confirmation_mode: false,
security_analyzer: "",
@@ -600,7 +640,7 @@ describe("Settings Screen", () => {
);
await user.click(confirmationModeSwitch);
const saveButton = screen.getByText("BUTTON$SAVE");
const saveButton = screen.getByText("Save Changes");
await user.click(saveButton);
expect(saveSettingsSpy).toHaveBeenCalledWith(
@@ -613,7 +653,7 @@ describe("Settings Screen", () => {
it("should toggle advanced if user had set a custom model", async () => {
getSettingsSpy.mockResolvedValue({
...DEFAULT_SETTINGS,
...MOCK_DEFAULT_USER_SETTINGS,
llm_model: "some/custom-model",
});
renderSettingsScreen();
@@ -648,7 +688,7 @@ describe("Settings Screen", () => {
it("should have confirmation mode enabled if the user previously had it enabled", async () => {
getSettingsSpy.mockResolvedValue({
...DEFAULT_SETTINGS,
...MOCK_DEFAULT_USER_SETTINGS,
confirmation_mode: true,
});
@@ -665,8 +705,9 @@ describe("Settings Screen", () => {
// FIXME: security analyzer is not found for some reason...
it.skip("should have the values set if the user previously had them set", async () => {
getSettingsSpy.mockResolvedValue({
...DEFAULT_SETTINGS,
...MOCK_DEFAULT_USER_SETTINGS,
language: "no",
github_token_is_set: true,
user_consents_to_analytics: true,
llm_base_url: "https://test.com",
llm_model: "anthropic/claude-3-5-sonnet-20241022",
@@ -678,7 +719,7 @@ describe("Settings Screen", () => {
await waitFor(() => {
expect(screen.getByTestId("language-input")).toHaveValue("Norsk");
expect(screen.getByText("Disconnect Tokens")).toBeInTheDocument();
expect(screen.getByText("Disconnect from GitHub")).toBeInTheDocument();
expect(screen.getByTestId("enable-analytics-switch")).toBeChecked();
expect(screen.getByTestId("advanced-settings-switch")).toBeChecked();
expect(screen.getByTestId("base-url-input")).toHaveValue(
@@ -700,7 +741,7 @@ describe("Settings Screen", () => {
it("should save the settings when the 'Save Changes' button is clicked", async () => {
const user = userEvent.setup();
getSettingsSpy.mockResolvedValue({
...DEFAULT_SETTINGS,
...MOCK_DEFAULT_USER_SETTINGS,
});
renderSettingsScreen();
@@ -713,12 +754,13 @@ describe("Settings Screen", () => {
expect(languageInput).toHaveValue("Norsk");
const saveButton = screen.getByText("BUTTON$SAVE");
const saveButton = screen.getByText("Save Changes");
await user.click(saveButton);
expect(saveSettingsSpy).toHaveBeenCalledWith(
expect.objectContaining({
llm_api_key: "", // empty because it's not set previously
provider_tokens: undefined,
language: "no",
}),
);
@@ -727,7 +769,7 @@ describe("Settings Screen", () => {
it("should properly save basic LLM model settings", async () => {
const user = userEvent.setup();
getSettingsSpy.mockResolvedValue({
...DEFAULT_SETTINGS,
...MOCK_DEFAULT_USER_SETTINGS,
});
renderSettingsScreen();
@@ -750,11 +792,12 @@ describe("Settings Screen", () => {
const gpt4Option = await screen.findByText("gpt-4o");
await user.click(gpt4Option);
const saveButton = screen.getByText("BUTTON$SAVE");
const saveButton = screen.getByText("Save Changes");
await user.click(saveButton);
expect(saveSettingsSpy).toHaveBeenCalledWith(
expect.objectContaining({
provider_tokens: undefined,
llm_api_key: "", // empty because it's not set previously
llm_model: "openai/gpt-4o",
}),
@@ -763,7 +806,7 @@ describe("Settings Screen", () => {
it("should reset the settings when the 'Reset to defaults' button is clicked", async () => {
const user = userEvent.setup();
getSettingsSpy.mockResolvedValue(DEFAULT_SETTINGS);
getSettingsSpy.mockResolvedValue(MOCK_DEFAULT_USER_SETTINGS);
renderSettingsScreen();
@@ -775,7 +818,7 @@ describe("Settings Screen", () => {
expect(languageInput).toHaveValue("Norsk");
const resetButton = screen.getByText("BUTTON$RESET_TO_DEFAULTS");
const resetButton = screen.getByText("Reset to defaults");
await user.click(resetButton);
expect(saveSettingsSpy).not.toHaveBeenCalled();
@@ -794,7 +837,7 @@ describe("Settings Screen", () => {
// Mock the settings response after reset
getSettingsSpy.mockResolvedValueOnce({
...DEFAULT_SETTINGS,
...MOCK_DEFAULT_USER_SETTINGS,
llm_base_url: "",
confirmation_mode: false,
security_analyzer: "",
@@ -803,27 +846,21 @@ describe("Settings Screen", () => {
// Wait for the mutation to complete and the modal to be removed
await waitFor(() => {
expect(screen.queryByTestId("reset-modal")).not.toBeInTheDocument();
expect(
screen.queryByTestId("llm-custom-model-input"),
).not.toBeInTheDocument();
expect(screen.queryByTestId("llm-custom-model-input")).not.toBeInTheDocument();
expect(screen.queryByTestId("base-url-input")).not.toBeInTheDocument();
expect(screen.queryByTestId("agent-input")).not.toBeInTheDocument();
expect(
screen.queryByTestId("security-analyzer-input"),
).not.toBeInTheDocument();
expect(
screen.queryByTestId("enable-confirmation-mode-switch"),
).not.toBeInTheDocument();
expect(screen.queryByTestId("security-analyzer-input")).not.toBeInTheDocument();
expect(screen.queryByTestId("enable-confirmation-mode-switch")).not.toBeInTheDocument();
});
});
it("should cancel the reset when the 'Cancel' button is clicked", async () => {
const user = userEvent.setup();
getSettingsSpy.mockResolvedValue(DEFAULT_SETTINGS);
getSettingsSpy.mockResolvedValue(MOCK_DEFAULT_USER_SETTINGS);
renderSettingsScreen();
const resetButton = await screen.findByText("BUTTON$RESET_TO_DEFAULTS");
const resetButton = await screen.findByText("Reset to defaults");
await user.click(resetButton);
const modal = await screen.findByTestId("reset-modal");
@@ -852,7 +889,7 @@ describe("Settings Screen", () => {
await user.click(analyticsConsentInput);
expect(analyticsConsentInput).toBeChecked();
const saveButton = screen.getByText("BUTTON$SAVE");
const saveButton = screen.getByText("Save Changes");
await user.click(saveButton);
expect(handleCaptureConsentSpy).toHaveBeenCalledWith(true);
@@ -866,7 +903,7 @@ describe("Settings Screen", () => {
);
renderSettingsScreen();
const saveButton = await screen.findByText("BUTTON$SAVE");
const saveButton = await screen.findByText("Save Changes");
await user.click(saveButton);
expect(handleCaptureConsentSpy).toHaveBeenCalledWith(false);
@@ -899,7 +936,7 @@ describe("Settings Screen", () => {
const user = userEvent.setup();
renderSettingsScreen();
const saveButton = screen.getByText("BUTTON$SAVE");
const saveButton = screen.getByText("Save Changes");
await user.click(saveButton);
expect(saveSettingsSpy).toHaveBeenCalledWith(
@@ -916,7 +953,7 @@ describe("Settings Screen", () => {
const input = await screen.findByTestId("llm-api-key-input");
expect(input).toHaveValue("");
const saveButton = screen.getByText("BUTTON$SAVE");
const saveButton = screen.getByText("Save Changes");
await user.click(saveButton);
expect(saveSettingsSpy).toHaveBeenCalledWith(
@@ -927,8 +964,8 @@ describe("Settings Screen", () => {
it("should not send an empty LLM API Key if the user submits an empty string but already has it set", async () => {
const user = userEvent.setup();
getSettingsSpy.mockResolvedValue({
...DEFAULT_SETTINGS,
llm_api_key_set: true,
...MOCK_DEFAULT_USER_SETTINGS,
llm_api_key: "**********",
});
renderSettingsScreen();
@@ -936,7 +973,7 @@ describe("Settings Screen", () => {
const input = await screen.findByTestId("llm-api-key-input");
expect(input).toHaveValue("");
const saveButton = screen.getByText("BUTTON$SAVE");
const saveButton = screen.getByText("Save Changes");
await user.click(saveButton);
expect(saveSettingsSpy).toHaveBeenCalledWith(
@@ -951,7 +988,7 @@ describe("Settings Screen", () => {
const input = await screen.findByTestId("llm-api-key-input");
await user.type(input, "new-api-key");
const saveButton = screen.getByText("BUTTON$SAVE");
const saveButton = screen.getByText("Save Changes");
await user.click(saveButton);
expect(saveSettingsSpy).toHaveBeenCalledWith(
@@ -1031,7 +1068,7 @@ describe("Settings Screen", () => {
const user = userEvent.setup();
renderSettingsScreen();
const saveButton = await screen.findByText("BUTTON$SAVE");
const saveButton = await screen.findByText("Save Changes");
await user.click(saveButton);
expect(saveSettingsSpy).toHaveBeenCalledWith(
@@ -1047,7 +1084,7 @@ describe("Settings Screen", () => {
const user = userEvent.setup();
renderSettingsScreen();
const resetButton = await screen.findByText("BUTTON$RESET_TO_DEFAULTS");
const resetButton = await screen.findByText("Reset to defaults");
await user.click(resetButton);
const modal = await screen.findByTestId("reset-modal");

View File

@@ -2,8 +2,6 @@ import { render, screen } from "@testing-library/react";
import { test, expect, describe, vi } from "vitest";
import { InteractiveChatBox } from "#/components/features/chat/interactive-chat-box";
import { ChatInput } from "#/components/features/chat/chat-input";
import path from 'path';
import { scanDirectoryForUnlocalizedStrings } from "#/utils/scan-unlocalized-strings-ast";
// Mock react-i18next
vi.mock("react-i18next", () => ({
@@ -39,23 +37,4 @@ describe("Check for hardcoded English strings", () => {
render(<ChatInput onSubmit={() => {}} />);
screen.getByPlaceholderText("SUGGESTIONS$WHAT_TO_BUILD");
});
test("No unlocalized strings should exist in frontend code", () => {
const srcPath = path.resolve(__dirname, '../../src');
// Get unlocalized strings using the AST scanner
// The scanner now properly handles CSS classes using AST information
const results = scanDirectoryForUnlocalizedStrings(srcPath);
// If we found any unlocalized strings, format them for output
if (results.size > 0) {
const formattedResults = Array.from(results.entries())
.map(([file, strings]) => `\n${file}:\n ${strings.join('\n ')}`)
.join('\n');
throw new Error(
`Found unlocalized strings in the following files:${formattedResults}`
);
}
});
});
});

View File

@@ -12,7 +12,7 @@ describe("hasAdvancedSettingsSet", () => {
expect(
hasAdvancedSettingsSet({
...DEFAULT_SETTINGS,
llm_base_url: "test",
LLM_BASE_URL: "test",
}),
).toBe(true);
});
@@ -21,7 +21,7 @@ describe("hasAdvancedSettingsSet", () => {
expect(
hasAdvancedSettingsSet({
...DEFAULT_SETTINGS,
agent: "test",
AGENT: "test",
}),
).toBe(true);
});
@@ -30,7 +30,7 @@ describe("hasAdvancedSettingsSet", () => {
expect(
hasAdvancedSettingsSet({
...DEFAULT_SETTINGS,
remote_runtime_resource_factor: 999,
REMOTE_RUNTIME_RESOURCE_FACTOR: 999,
}),
).toBe(true);
});
@@ -39,7 +39,7 @@ describe("hasAdvancedSettingsSet", () => {
expect(
hasAdvancedSettingsSet({
...DEFAULT_SETTINGS,
confirmation_mode: true,
CONFIRMATION_MODE: true,
}),
).toBe(true);
});
@@ -48,7 +48,7 @@ describe("hasAdvancedSettingsSet", () => {
expect(
hasAdvancedSettingsSet({
...DEFAULT_SETTINGS,
security_analyzer: "test",
SECURITY_ANALYZER: "test",
}),
).toBe(true);
});

View File

@@ -1,12 +1,12 @@
{
"name": "openhands-frontend",
"version": "0.31.0",
"version": "0.30.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "openhands-frontend",
"version": "0.31.0",
"version": "0.30.1",
"dependencies": {
"@heroui/react": "2.7.5",
"@monaco-editor/react": "^4.7.0-rc.0",
@@ -51,9 +51,6 @@
"ws": "^8.18.1"
},
"devDependencies": {
"@babel/parser": "^7.27.0",
"@babel/traverse": "^7.27.0",
"@babel/types": "^7.27.0",
"@mswjs/socket.io-binding": "^0.1.1",
"@playwright/test": "^1.51.1",
"@react-router/dev": "^7.4.0",
@@ -17143,9 +17140,9 @@
}
},
"node_modules/vite": {
"version": "6.2.4",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.4.tgz",
"integrity": "sha512-veHMSew8CcRzhL5o8ONjy8gkfmFJAd5Ac16oxBUjlwgX3Gq2Wqr+qNC3TjPIpy7TPV/KporLga5GT9HqdrCizw==",
"version": "6.2.3",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.2.3.tgz",
"integrity": "sha512-IzwM54g4y9JA/xAeBPNaDXiBF8Jsgl3VBQ2YQ/wOY6fyW3xMdSoltIV3Bo59DErdqdE6RxUfv8W69DvUorE4Eg==",
"license": "MIT",
"dependencies": {
"esbuild": "^0.25.0",

View File

@@ -1,6 +1,6 @@
{
"name": "openhands-frontend",
"version": "0.31.0",
"version": "0.30.1",
"private": true,
"type": "module",
"engines": {
@@ -79,9 +79,6 @@
]
},
"devDependencies": {
"@babel/parser": "^7.27.0",
"@babel/traverse": "^7.27.0",
"@babel/types": "^7.27.0",
"@mswjs/socket.io-binding": "^0.1.1",
"@playwright/test": "^1.51.1",
"@react-router/dev": "^7.4.0",

View File

@@ -1,38 +0,0 @@
import { openHands } from "../open-hands-axios";
import { GetFilesResponse, GetFileResponse } from "./file-service.types";
import { getConversationUrl } from "./file-service.utils";
export class FileService {
/**
* Retrieve the list of files available in the workspace
* @param conversationId ID of the conversation
* @param path Path to list files from. If provided, it lists all the files in the given path
* @returns List of files available in the given path. If path is not provided, it lists all the files in the workspace
*/
static async getFiles(
conversationId: string,
path?: string,
): Promise<GetFilesResponse> {
const url = `${getConversationUrl(conversationId)}/list-files`;
const { data } = await openHands.get<GetFilesResponse>(url, {
params: { path },
});
return data;
}
/**
* Retrieve the content of a file
* @param conversationId ID of the conversation
* @param path Full path of the file to retrieve
* @returns Code content of the file
*/
static async getFile(conversationId: string, path: string): Promise<string> {
const url = `${getConversationUrl(conversationId)}/select-file`;
const { data } = await openHands.get<GetFileResponse>(url, {
params: { file: path },
});
return data.code;
}
}

View File

@@ -1,5 +0,0 @@
export type GetFilesResponse = string[];
export interface GetFileResponse {
code: string;
}

View File

@@ -1,7 +0,0 @@
/**
* Returns a URL compatible for the file service
* @param conversationId ID of the conversation
* @returns URL of the conversation
*/
export const getConversationUrl = (conversationId: string) =>
`/api/conversations/${conversationId}`;

View File

@@ -1,4 +1,3 @@
import { GitRepository } from "#/types/git";
import { extractNextPageFromLink } from "#/utils/extract-next-page-from-link";
import { openHands } from "./open-hands-axios";
@@ -15,8 +14,8 @@ export const retrieveGitHubAppRepositories = async (
per_page = 30,
) => {
const installationId = installations[installationIndex];
const response = await openHands.get<GitRepository[]>(
"/api/user/repositories",
const response = await openHands.get<GitHubRepository[]>(
"/api/github/repositories",
{
params: {
sort: "pushed",
@@ -54,17 +53,21 @@ export const retrieveGitHubAppRepositories = async (
* Given a PAT, retrieves the repositories of the user
* @returns A list of repositories
*/
export const retrieveUserGitRepositories = async () => {
const response = await openHands.get<GitRepository[]>(
"/api/user/repositories",
export const retrieveGitHubUserRepositories = async (
page = 1,
per_page = 30,
) => {
const response = await openHands.get<GitHubRepository[]>(
"/api/github/repositories",
{
params: {
sort: "pushed",
page,
per_page,
},
},
);
// Check if any provider has more results
const link =
response.data.length > 0 && response.data[0].link_header
? response.data[0].link_header

View File

@@ -3,3 +3,22 @@ import axios from "axios";
export const openHands = axios.create({
baseURL: `${window.location.protocol}//${import.meta.env.VITE_BACKEND_BASE_URL || window?.location.host}`,
});
export const setAuthTokenHeader = (token: string) => {
openHands.defaults.headers.common.Authorization = `Bearer ${token}`;
};
export const setGitHubTokenHeader = (token: string) => {
openHands.defaults.headers.common["X-GitHub-Token"] = token;
};
export const removeAuthTokenHeader = () => {
if (openHands.defaults.headers.common.Authorization) {
delete openHands.defaults.headers.common.Authorization;
}
};
export const removeGitHubTokenHeader = () => {
if (openHands.defaults.headers.common["X-GitHub-Token"]) {
delete openHands.defaults.headers.common["X-GitHub-Token"];
}
};

View File

@@ -1,7 +1,10 @@
import {
SaveFileSuccessResponse,
FileUploadSuccessResponse,
Feedback,
FeedbackResponse,
GitHubAccessTokenResponse,
ErrorResponse,
GetConfigResponse,
GetVSCodeUrlResponse,
AuthenticateResponse,
@@ -10,7 +13,7 @@ import {
GetTrajectoryResponse,
} from "./open-hands.types";
import { openHands } from "./open-hands-axios";
import { GitUser, GitRepository } from "#/types/git";
import { ApiSettings, PostApiSettings } from "#/types/settings";
class OpenHands {
/**
@@ -49,6 +52,80 @@ class OpenHands {
return data;
}
/**
* Retrieve the list of files available in the workspace
* @param path Path to list files from
* @returns List of files available in the given path. If path is not provided, it lists all the files in the workspace
*/
static async getFiles(
conversationId: string,
path?: string,
): Promise<string[]> {
const url = `/api/conversations/${conversationId}/list-files`;
const { data } = await openHands.get<string[]>(url, {
params: { path },
});
return data;
}
/**
* Retrieve the content of a file
* @param path Full path of the file to retrieve
* @returns Content of the file
*/
static async getFile(conversationId: string, path: string): Promise<string> {
const url = `/api/conversations/${conversationId}/select-file`;
const { data } = await openHands.get<{ code: string }>(url, {
params: { file: path },
});
return data.code;
}
/**
* Save the content of a file
* @param path Full path of the file to save
* @param content Content to save in the file
* @returns Success message or error message
*/
static async saveFile(
conversationId: string,
path: string,
content: string,
): Promise<SaveFileSuccessResponse> {
const url = `/api/conversations/${conversationId}/save-file`;
const { data } = await openHands.post<
SaveFileSuccessResponse | ErrorResponse
>(url, {
filePath: path,
content,
});
if ("error" in data) throw new Error(data.error);
return data;
}
/**
* Upload a file to the workspace
* @param file File to upload
* @returns Success message or error message
*/
static async uploadFiles(
conversationId: string,
files: File[],
): Promise<FileUploadSuccessResponse> {
const url = `/api/conversations/${conversationId}/upload-files`;
const formData = new FormData();
files.forEach((file) => formData.append("files", file));
const { data } = await openHands.post<
FileUploadSuccessResponse | ErrorResponse
>(url, formData);
if ("error" in data) throw new Error(data.error);
return data;
}
/**
* Send feedback to the server
* @param data Feedback data
@@ -146,7 +223,7 @@ class OpenHands {
}
static async createConversation(
selectedRepository?: GitRepository,
selectedRepository?: string,
initialUserMsg?: string,
imageUrls?: string[],
replayJson?: string,
@@ -177,6 +254,33 @@ class OpenHands {
return data;
}
/**
* Get the settings from the server or use the default settings if not found
*/
static async getSettings(): Promise<ApiSettings> {
const { data } = await openHands.get<ApiSettings>("/api/settings");
return data;
}
/**
* Save the settings to the server. Only valid settings are saved.
* @param settings - the settings to save
*/
static async saveSettings(
settings: Partial<PostApiSettings>,
): Promise<boolean> {
const data = await openHands.post("/api/settings", settings);
return data.status === 200;
}
/**
* Reset user settings in server
*/
static async resetSettings(): Promise<boolean> {
const response = await openHands.post("/api/reset-settings");
return response.status === 200;
}
static async createCheckoutSession(amount: number): Promise<string> {
const { data } = await openHands.post(
"/api/billing/create-checkout-session",
@@ -201,12 +305,12 @@ class OpenHands {
return data.credits;
}
static async getGitUser(): Promise<GitUser> {
const response = await openHands.get<GitUser>("/api/user/info");
static async getGitHubUser(): Promise<GitHubUser> {
const response = await openHands.get<GitHubUser>("/api/github/user");
const { data } = response;
const user: GitUser = {
const user: GitHubUser = {
id: data.id,
login: data.login,
avatar_url: data.avatar_url,
@@ -218,12 +322,17 @@ class OpenHands {
return user;
}
static async searchGitRepositories(
static async getGitHubUserInstallationIds(): Promise<number[]> {
const response = await openHands.get<number[]>("/api/github/installations");
return response.data;
}
static async searchGitHubRepositories(
query: string,
per_page = 5,
): Promise<GitRepository[]> {
const response = await openHands.get<GitRepository[]>(
"/api/user/search/repositories",
): Promise<GitHubRepository[]> {
const response = await openHands.get<GitHubRepository[]>(
"/api/github/search/repositories",
{
params: {
query,

View File

@@ -0,0 +1,6 @@
import { ErrorResponse, FileUploadSuccessResponse } from "./open-hands.types";
export const isOpenHandsErrorResponse = (
data: ErrorResponse | FileUploadSuccessResponse,
): data is ErrorResponse =>
typeof data === "object" && data !== null && "error" in data;

View File

@@ -1,29 +0,0 @@
import { openHands } from "../open-hands-axios";
import { UserSettings } from "./settings-service.types";
export class SettingsService {
/**
* Get the user's settings
*/
static async getSettings(): Promise<UserSettings> {
const { data } = await openHands.get<UserSettings>("/api/settings");
return data;
}
/**
* Save valid settings to the server
* @param settings - The settings to save
*/
static async saveSettings(settings: Partial<UserSettings>): Promise<boolean> {
const data = await openHands.post("/api/settings", settings);
return data.status === 200;
}
/**
* Reset the user's settings
*/
static async resetSettings(): Promise<boolean> {
const response = await openHands.post("/api/settings/reset");
return response.status === 200;
}
}

View File

@@ -1,27 +0,0 @@
export type GitProvider = "github" | "gitlab";
export interface UserSettings {
llm_model: string;
llm_base_url: string;
agent: string;
language: string;
llm_api_key_set: boolean;
confirmation_mode: boolean;
security_analyzer: string;
remote_runtime_resource_factor: number | null;
github_token_is_set: boolean;
enable_default_condenser: boolean;
enable_sound_notifications: boolean;
user_consents_to_analytics: boolean | null;
provider_tokens_set: Record<GitProvider, boolean>;
}
// These are settings that are only used on the client side and should not be sent to the server
export interface ClientUserSettings extends UserSettings {
is_new_user: boolean;
}
// These are settings that are used on the server side and should be sent to the server
export interface ServerUserSettings extends UserSettings {
github_token: string | null;
}

View File

@@ -1,4 +1,3 @@
import { useTranslation } from "react-i18next";
import {
BaseModalTitle,
BaseModalDescription,
@@ -8,7 +7,6 @@ import { ModalBody } from "#/components/shared/modals/modal-body";
import { useSaveSettings } from "#/hooks/mutation/use-save-settings";
import { handleCaptureConsent } from "#/utils/handle-capture-consent";
import { BrandButton } from "../settings/brand-button";
import { I18nKey } from "#/i18n/declaration";
interface AnalyticsConsentFormModalProps {
onClose: () => void;
@@ -17,7 +15,6 @@ interface AnalyticsConsentFormModalProps {
export function AnalyticsConsentFormModal({
onClose,
}: AnalyticsConsentFormModalProps) {
const { t } = useTranslation();
const { mutate: saveUserSettings } = useSaveSettings();
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
@@ -44,14 +41,16 @@ export function AnalyticsConsentFormModal({
className="flex flex-col gap-2"
>
<ModalBody className="border border-tertiary">
<BaseModalTitle title={t(I18nKey.ANALYTICS$TITLE)} />
<BaseModalTitle title="Your Privacy Preferences" />
<BaseModalDescription>
{t(I18nKey.ANALYTICS$DESCRIPTION)}
We use tools to understand how our application is used to improve
your experience. You can enable or disable analytics. Your
preferences will be stored and can be updated anytime.
</BaseModalDescription>
<label className="flex gap-2 items-center self-start">
<input name="analytics" type="checkbox" defaultChecked />
{t(I18nKey.ANALYTICS$SEND_ANONYMOUS_DATA)}
Send anonymous usage data
</label>
<BrandButton
@@ -60,7 +59,7 @@ export function AnalyticsConsentFormModal({
variant="primary"
className="w-full"
>
{t(I18nKey.ANALYTICS$CONFIRM_PREFERENCES)}
Confirm Preferences
</BrandButton>
</ModalBody>
</form>

View File

@@ -1,19 +1,14 @@
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
interface BrowserSnaphsotProps {
src: string;
}
export function BrowserSnapshot({ src }: BrowserSnaphsotProps) {
const { t } = useTranslation();
return (
<img
src={src}
style={{ objectFit: "contain", width: "100%", height: "auto" }}
className="rounded-xl"
alt={t(I18nKey.BROWSER$SCREENSHOT_ALT)}
alt="Browser Screenshot"
/>
);
}

View File

@@ -1,11 +1,9 @@
import posthog from "posthog-js";
import React from "react";
import { useSelector } from "react-redux";
import { useTranslation } from "react-i18next";
import { SuggestionItem } from "#/components/features/suggestions/suggestion-item";
import type { RootState } from "#/store";
import { useAuth } from "#/context/auth-context";
import { I18nKey } from "#/i18n/declaration";
interface ActionSuggestionsProps {
onSuggestionsClick: (value: string) => void;
@@ -14,44 +12,24 @@ interface ActionSuggestionsProps {
export function ActionSuggestions({
onSuggestionsClick,
}: ActionSuggestionsProps) {
const { t } = useTranslation();
const { providersAreSet } = useAuth();
const { githubTokenIsSet } = useAuth();
const { selectedRepository } = useSelector(
(state: RootState) => state.initialQuery,
);
const [hasPullRequest, setHasPullRequest] = React.useState(false);
const isGitLab =
selectedRepository !== null &&
selectedRepository.git_provider &&
selectedRepository.git_provider.toLowerCase() === "gitlab";
const pr = isGitLab ? "merge request" : "pull request";
const prShort = isGitLab ? "MR" : "PR";
const terms = {
pr,
prShort,
pushToBranch: `Please push the changes to a remote branch on ${
isGitLab ? "GitLab" : "GitHub"
}, but do NOT create a ${pr}. Please use the exact SAME branch name as the one you are currently on.`,
createPR: `Please push the changes to ${
isGitLab ? "GitLab" : "GitHub"
} and open a ${pr}. Please create a meaningful branch name that describes the changes. If a ${pr} template exists in the repository, please follow it when creating the ${prShort} description.`,
pushToPR: `Please push the latest changes to the existing ${pr}.`,
};
return (
<div className="flex flex-col gap-2 mb-2">
{providersAreSet && selectedRepository && (
{githubTokenIsSet && selectedRepository && (
<div className="flex flex-row gap-2 justify-center w-full">
{!hasPullRequest ? (
<>
<SuggestionItem
suggestion={{
label: t(I18nKey.ACTION$PUSH_TO_BRANCH),
value: terms.pushToBranch,
label: "Push to Branch",
value:
"Please push the changes to a remote branch on GitHub, but do NOT create a pull request. Please use the exact SAME branch name as the one you are currently on.",
}}
onClick={(value) => {
posthog.capture("push_to_branch_button_clicked");
@@ -60,8 +38,9 @@ export function ActionSuggestions({
/>
<SuggestionItem
suggestion={{
label: t(I18nKey.ACTION$PUSH_CREATE_PR),
value: terms.createPR,
label: "Push & Create PR",
value:
"Please push the changes to GitHub and open a pull request. Please create a meaningful branch name that describes the changes.",
}}
onClick={(value) => {
posthog.capture("create_pr_button_clicked");
@@ -73,8 +52,9 @@ export function ActionSuggestions({
) : (
<SuggestionItem
suggestion={{
label: t(I18nKey.ACTION$PUSH_CHANGES_TO_PR),
value: terms.pushToPR,
label: "Push changes to PR",
value:
"Please push the latest changes to the existing pull request.",
}}
onClick={(value) => {
posthog.capture("push_to_pr_button_clicked");

View File

@@ -2,8 +2,6 @@ import { useDispatch, useSelector } from "react-redux";
import React from "react";
import posthog from "posthog-js";
import { useParams } from "react-router";
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
import { convertImageToBase64 } from "#/utils/convert-image-to-base-64";
import { TrajectoryActions } from "../trajectory/trajectory-actions";
import { createChatMessage } from "#/services/chat-service";
@@ -38,7 +36,6 @@ function getEntryPoint(
export function ChatInterface() {
const { send, isLoadingMessages } = useWsClient();
const dispatch = useDispatch();
const { t } = useTranslation();
const scrollRef = React.useRef<HTMLDivElement>(null);
const { scrollDomToBottom, onChatBodyScroll, hitBottom } =
useScrollToBottom(scrollRef);
@@ -97,19 +94,19 @@ export function ChatInterface() {
const onClickExportTrajectoryButton = () => {
if (!params.conversationId) {
displayErrorToast(t(I18nKey.CONVERSATION$DOWNLOAD_ERROR));
displayErrorToast("ConversationId unknown, cannot download trajectory");
return;
}
getTrajectory(params.conversationId, {
onSuccess: async (data) => {
await downloadTrajectory(
params.conversationId ?? t(I18nKey.CONVERSATION$UNKNOWN),
params.conversationId ?? "unknown",
data.trajectory,
);
},
onError: () => {
displayErrorToast(t(I18nKey.CONVERSATION$DOWNLOAD_ERROR));
onError: (error) => {
displayErrorToast(error.message);
},
});
};
@@ -127,7 +124,7 @@ export function ChatInterface() {
<div
ref={scrollRef}
onScroll={(e) => onChatBodyScroll(e.currentTarget)}
className="flex flex-col grow overflow-y-auto overflow-x-hidden px-4 pt-4 gap-2 fast-smooth-scroll"
className="flex flex-col grow overflow-y-auto overflow-x-hidden px-4 pt-4 gap-2"
>
{isLoadingMessages && (
<div className="flex justify-center">

View File

@@ -1,35 +1,22 @@
import { PayloadAction } from "@reduxjs/toolkit";
import { useEffect, useState } from "react";
import { Trans, useTranslation } from "react-i18next";
import { useState, useEffect } from "react";
import { useTranslation } from "react-i18next";
import Markdown from "react-markdown";
import { Link } from "react-router";
import remarkGfm from "remark-gfm";
import { useConfig } from "#/hooks/query/use-config";
import { I18nKey } from "#/i18n/declaration";
import ArrowDown from "#/icons/angle-down-solid.svg?react";
import ArrowUp from "#/icons/angle-up-solid.svg?react";
import CheckCircle from "#/icons/check-circle-solid.svg?react";
import XCircle from "#/icons/x-circle-solid.svg?react";
import { OpenHandsAction } from "#/types/core/actions";
import { OpenHandsObservation } from "#/types/core/observations";
import { cn } from "#/utils/utils";
import { Link } from "react-router";
import { code } from "../markdown/code";
import { ol, ul } from "../markdown/list";
import { MonoComponent } from "./mono-component";
import { PathComponent } from "./path-component";
const trimText = (text: string, maxLength: number): string => {
if (!text) return "";
return text.length > maxLength ? `${text.substring(0, maxLength)}...` : text;
};
import ArrowUp from "#/icons/angle-up-solid.svg?react";
import ArrowDown from "#/icons/angle-down-solid.svg?react";
import CheckCircle from "#/icons/check-circle-solid.svg?react";
import XCircle from "#/icons/x-circle-solid.svg?react";
import { cn } from "#/utils/utils";
import { useConfig } from "#/hooks/query/use-config";
interface ExpandableMessageProps {
id?: string;
message: string;
type: string;
success?: boolean;
observation?: PayloadAction<OpenHandsObservation>;
action?: PayloadAction<OpenHandsAction>;
}
export function ExpandableMessage({
@@ -37,70 +24,28 @@ export function ExpandableMessage({
message,
type,
success,
observation,
action,
}: ExpandableMessageProps) {
const { data: config } = useConfig();
const { t, i18n } = useTranslation();
const [showDetails, setShowDetails] = useState(true);
const [headline, setHeadline] = useState("");
const [details, setDetails] = useState(message);
const [translationId, setTranslationId] = useState<string | undefined>(id);
const [translationParams, setTranslationParams] = useState<
Record<string, unknown>
>({
observation,
action,
});
useEffect(() => {
// Normal handling for other messages
if (id && i18n.exists(id)) {
let processedObservation = observation;
let processedAction = action;
if (action && action.payload.action === "run") {
const trimmedCommand = trimText(action.payload.args.command, 80);
processedAction = {
...action,
payload: {
...action.payload,
args: {
...action.payload.args,
command: trimmedCommand,
},
},
};
}
if (observation && observation.payload.observation === "run") {
const trimmedCommand = trimText(observation.payload.extras.command, 80);
processedObservation = {
...observation,
payload: {
...observation.payload,
extras: {
...observation.payload.extras,
command: trimmedCommand,
},
},
};
}
setTranslationId(id);
setTranslationParams({
observation: processedObservation,
action: processedAction,
});
setHeadline(t(id));
setDetails(message);
setShowDetails(false);
}
}, [id, message, observation, action, i18n.language]);
}, [id, message, i18n.language]);
const statusIconClasses = "h-4 w-4 ml-2 inline";
if (
config?.FEATURE_FLAGS.ENABLE_BILLING &&
config?.APP_MODE === "saas" &&
id === I18nKey.STATUS$ERROR_LLM_OUT_OF_CREDITS
id === "STATUS$ERROR_LLM_OUT_OF_CREDITS"
) {
return (
<div
@@ -109,13 +54,13 @@ export function ExpandableMessage({
>
<div className="text-sm w-full">
<div className="font-bold text-danger">
{t(I18nKey.STATUS$ERROR_LLM_OUT_OF_CREDITS)}
{t("STATUS$ERROR_LLM_OUT_OF_CREDITS")}
</div>
<Link
className="mt-2 mb-2 w-full h-10 rounded flex items-center justify-center gap-2 bg-primary text-[#0D0F11]"
to="/settings/billing"
>
{t(I18nKey.BILLING$CLICK_TO_TOP_UP)}
{t("BILLING$CLICK_TO_TOP_UP")}
</Link>
</div>
</div>
@@ -133,44 +78,36 @@ export function ExpandableMessage({
<div className="flex flex-row justify-between items-center w-full">
<span
className={cn(
"font-bold",
headline ? "font-bold" : "",
type === "error" ? "text-danger" : "text-neutral-300",
)}
>
{translationId && i18n.exists(translationId) ? (
<Trans
i18nKey={translationId}
values={translationParams}
components={{
bold: <strong />,
path: <PathComponent />,
cmd: <MonoComponent />,
}}
/>
) : (
message
{headline && (
<>
{headline}
<button
type="button"
onClick={() => setShowDetails(!showDetails)}
className="cursor-pointer text-left"
>
{showDetails ? (
<ArrowUp
className={cn(
"h-4 w-4 ml-2 inline",
type === "error" ? "fill-danger" : "fill-neutral-300",
)}
/>
) : (
<ArrowDown
className={cn(
"h-4 w-4 ml-2 inline",
type === "error" ? "fill-danger" : "fill-neutral-300",
)}
/>
)}
</button>
</>
)}
<button
type="button"
onClick={() => setShowDetails(!showDetails)}
className="cursor-pointer text-left"
>
{showDetails ? (
<ArrowUp
className={cn(
"h-4 w-4 ml-2 inline",
type === "error" ? "fill-danger" : "fill-neutral-300",
)}
/>
) : (
<ArrowDown
className={cn(
"h-4 w-4 ml-2 inline",
type === "error" ? "fill-danger" : "fill-neutral-300",
)}
/>
)}
</button>
</span>
{type === "action" && success !== undefined && (
<span className="flex-shrink-0">
@@ -188,7 +125,7 @@ export function ExpandableMessage({
</span>
)}
</div>
{showDetails && (
{(!headline || showDetails) && (
<div className="text-sm overflow-auto">
<Markdown
components={{

Some files were not shown because too many files have changed in this diff Show More