Compare commits

..

60 Commits

Author SHA1 Message Date
openhands
39fd5cdab6 fix: Use runtime to load microagents
- Update CodeActAgent to use runtime.get_microagents_from_selected_repo
- Add test to verify microagent loading from runtime
- Fix #6304
2025-01-16 14:35:20 +00:00
dependabot[bot]
6e089619e0 chore(deps-dev): bump chromadb from 0.6.2 to 0.6.3 in the chromadb group (#6289)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-16 00:37:42 +01:00
Xingyao Wang
179a89a211 Fix microagent loading with trailing slashes and nested directories (#6239)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-01-15 17:07:40 +00:00
tofarr
8795ee6c6e Fix closing sessions (#6114) 2025-01-15 10:04:22 -07:00
Engel Nyst
97e938d545 Fix French doc (#6283) 2025-01-15 04:25:47 +00:00
Engel Nyst
b9a70c8d5c Delegation fixes (#6165) 2025-01-15 03:24:39 +00:00
Ray Myers
082d0b25c5 Send status message on runtime restart (#6275) 2025-01-15 03:21:06 +01:00
Engel Nyst
c5797d1d5a Fix llm_config fallback (#4415)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-01-15 01:17:37 +00:00
Xingyao Wang
7ce1fb85ff chore: remove repo info from initial query for #6057 (#6279) 2025-01-15 00:40:54 +00:00
Robert Brennan
fa6792e5a6 Add GitHub repository information to system prompt (#6057)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-01-15 08:02:07 +08:00
dependabot[bot]
3d9b4c4af6 chore(deps): bump the version-all group with 4 updates (#6267)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-14 21:30:56 +01:00
tofarr
e21cbf67ee Feat: User id should be a str (Because it will probably be a UUID) (#6251) 2025-01-14 12:39:51 -07:00
Xingyao Wang
6b2e3f938f fix: prevent runtime size deselection (#6119)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: mamoodi <mamoodiha@gmail.com>
2025-01-14 17:53:51 +00:00
Rohit Malhotra
580d7b938c Fix: Don't refresh github token on local (#5880) 2025-01-14 17:48:33 +00:00
mamoodi
28178a2940 Remove extra optional for github token (#6270) 2025-01-14 17:44:28 +00:00
sp.wack
04382b2b19 hotfix(backend): Remove GH header token middleware (#6269) 2025-01-14 12:07:13 -05:00
Robert Brennan
4da812c781 Better handling of stack traces and exc_info (#6253) 2025-01-14 10:22:39 -05:00
mamoodi
37b7173481 Update landing page examples (#6254) 2025-01-14 15:09:30 +00:00
Graham Neubig
f0ebf3eba8 Improve i18n support and add missing translations (#6070)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-01-14 13:46:22 +00:00
Boxuan Li
92b8d55c2d Rename trajectories_path config to save_trajectory_path (#6216)
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-01-14 04:32:45 +00:00
dependabot[bot]
a125b6cd43 chore(deps): bump the version-all group across 1 directory with 6 updates (#6248)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-13 23:33:09 +01:00
tofarr
01ac207b92 Fix remove dead code (#6249) 2025-01-13 14:15:13 -07:00
Ray Myers
6d015a5dca Don't start conversation runtime without valid API key (#6181) 2025-01-13 22:03:37 +01:00
dependabot[bot]
275512305d chore(deps): bump docker/setup-qemu-action from 3.2.0 to 3.3.0 (#6229)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-13 21:31:01 +01:00
mamoodi
3a4bc10b29 Release 0.20.0 (#6234)
Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com>
2025-01-13 14:58:20 -05:00
sp.wack
bbd31b32f3 chore: Move GH requests to the server (#6217) 2025-01-13 23:12:50 +04:00
Joseph O'Connor
295c6fd629 fix(issue_definitions, issue-success-check.jinja): pass git-patch to issue-success-check (#6243)
Co-authored-by: Graham Neubig <neubig@gmail.com>
2025-01-13 18:40:15 +00:00
tofarr
5a809c9b53 Feature: User id propagation (#6233) 2025-01-13 18:10:45 +00:00
sp.wack
0b74fd71d9 fix(frontend): Prevent from send a SET API key (#6235) 2025-01-13 17:50:37 +00:00
tofarr
4fa5c329d6 Fix : minor updates to log messages (#6232) 2025-01-13 17:19:51 +00:00
tofarr
5b1dcf83a6 Fix for issue where S3FileStore does not delete directory objects (#6231) 2025-01-13 16:50:58 +00:00
tofarr
b9884f7609 Fixes for minor cases where FDs were not closed (#6228) 2025-01-13 09:15:23 -07:00
dependabot[bot]
99eda0e571 chore(deps-dev): bump the eslint group in /frontend with 2 updates (#6227)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-13 19:44:10 +04:00
Ryan H. Tran
5832463088 Revert openhands-resolver.yml change in #5972 (#6222) 2025-01-13 15:39:54 +00:00
tofarr
045ec2b95d Fix: Timezone should be UTC (#6225) 2025-01-13 08:24:26 -07:00
tofarr
23473070b9 Revert "Config objects as Pydantic BaseModels (#6176)" (#6214) 2025-01-13 07:36:25 -07:00
mamoodi
63133c0ba9 Document changes for Micro-Agents and some formatting changes (#6155)
Co-authored-by: Robert Brennan <accounts@rbren.io>
2025-01-13 09:24:10 -05:00
dependabot[bot]
2023fb767f chore(deps): bump the version-all group in /frontend with 2 updates (#6192)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com>
2025-01-13 11:09:11 +00:00
Ryan H. Tran
23f40a1c01 Enable runtime image build for resolver's experimental feature (#5972) 2025-01-12 17:21:34 -05:00
Calvin Smith
873dddb4e8 Config objects as Pydantic BaseModels (#6176)
Co-authored-by: Calvin Smith <calvin@all-hands.dev>
Co-authored-by: Graham Neubig <neubig@gmail.com>
2025-01-12 15:09:45 -05:00
Ryan H. Tran
fe50cd1f9f Upgrade openhands-aci to 0.1.8 (#6123) 2025-01-12 07:26:57 +01:00
Boxuan Li
516e2da520 Custom runtime builder: fix NoEmptyContinuation error (#6211) 2025-01-11 15:58:08 -08:00
jmtatsch
1dd6f544bc Fix #6056 (#6203) 2025-01-11 23:32:12 +01:00
Graham Neubig
40c52feb5b fix: Handle empty lines in patch parser (#6208)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-01-12 06:43:08 +09:00
Xingyao Wang
f31ccad48b feat: misc bash improvements, set max value for action-exec timeout, retry on requests.ConnectionError (#6175) 2025-01-11 04:36:12 +08:00
Xingyao Wang
828d169b82 refactor: consolidate runtime startup command into an util function (#6199) 2025-01-11 04:27:13 +08:00
dependabot[bot]
a622d27016 chore(deps-dev): bump llama-index-embeddings-huggingface from 0.4.0 to 0.5.0 in the llama group (#6194)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-10 19:59:54 +01:00
sp.wack
5507b131fe hotfix(frontend): Add beta tag to new app tab (#6198) 2025-01-10 17:31:42 +00:00
sp.wack
0f102e4c71 hotfix(frontend): Get bottom right conversation card details even when multi convo is disabled (#6197) 2025-01-10 17:19:37 +00:00
sp.wack
157a1a24f6 fix(frontend): Wait for fetched settings instead of loading default ones (#6193) 2025-01-10 16:54:31 +00:00
dependabot[bot]
fcfbcb64d4 chore(deps): bump the version-all group in /frontend with 5 updates (#6170)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: amanape <83104063+amanape@users.noreply.github.com>
2025-01-10 13:51:03 +00:00
Xingyao Wang
931792e87a fix: UI terminal output incorrect newline (#6182) 2025-01-10 17:11:06 +04:00
Robert Brennan
ee701eacc2 fix: prevent race condition in session manager during disconnect (#6053)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-01-09 17:26:53 -07:00
Ray Myers
8907fed78e Provide a clearer error message when settings are missing midsession (#6158) 2025-01-09 19:09:34 +00:00
Robert Brennan
3cc20a2576 remove timeouts on remote runtime (#6171) 2025-01-09 12:39:40 -05:00
dependabot[bot]
01cf0d433c chore(deps): bump the version-all group with 5 updates (#6169)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-09 17:45:16 +01:00
sp.wack
f6bed82ae2 Add port mappings support (#5577)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: tofarr <tofarr@gmail.com>
Co-authored-by: Robert Brennan <accounts@rbren.io>
Co-authored-by: Robert Brennan <contact@rbren.io>
2025-01-09 15:02:56 +00:00
sp.wack
3eae2e2aca chore(frontend): Optimize requests made to the backend (#6168) 2025-01-09 15:00:26 +00:00
sp.wack
b45fc522c7 feat(frontend): Display current conversation info in the bottom right (#6143) 2025-01-09 14:55:33 +00:00
sp.wack
0d409c8c24 fix(frontend): Prevent saving empty custom model (#6149) 2025-01-09 13:43:39 +00:00
207 changed files with 9060 additions and 4132 deletions

View File

@@ -56,7 +56,7 @@ jobs:
docker-images: false
swap-storage: true
- name: Set up QEMU
uses: docker/setup-qemu-action@v3.2.0
uses: docker/setup-qemu-action@v3.3.0
with:
image: tonistiigi/binfmt:latest
- name: Login to GHCR
@@ -119,7 +119,7 @@ jobs:
docker-images: false
swap-storage: true
- name: Set up QEMU
uses: docker/setup-qemu-action@v3.2.0
uses: docker/setup-qemu-action@v3.3.0
with:
image: tonistiigi/binfmt:latest
- name: Login to GHCR

View File

@@ -56,6 +56,7 @@ jobs:
LLM_MODEL: "litellm_proxy/claude-3-5-haiku-20241022"
LLM_API_KEY: ${{ secrets.LLM_API_KEY }}
LLM_BASE_URL: ${{ secrets.LLM_BASE_URL }}
MAX_ITERATIONS: 10
run: |
echo "[llm.eval]" > config.toml
echo "model = \"$LLM_MODEL\"" >> config.toml
@@ -70,7 +71,7 @@ jobs:
env:
SANDBOX_FORCE_REBUILD_RUNTIME: True
run: |
poetry run ./evaluation/integration_tests/scripts/run_infer.sh llm.eval HEAD CodeActAgent '' $N_PROCESSES '' 'haiku_run'
poetry run ./evaluation/integration_tests/scripts/run_infer.sh llm.eval HEAD CodeActAgent '' 10 $N_PROCESSES '' 'haiku_run'
# get integration tests report
REPORT_FILE_HAIKU=$(find evaluation/evaluation_outputs/outputs/integration_tests/CodeActAgent/*haiku*_maxiter_10_N* -name "report.md" -type f | head -n 1)
@@ -88,6 +89,7 @@ jobs:
LLM_MODEL: "litellm_proxy/deepseek-chat"
LLM_API_KEY: ${{ secrets.LLM_API_KEY }}
LLM_BASE_URL: ${{ secrets.LLM_BASE_URL }}
MAX_ITERATIONS: 10
run: |
echo "[llm.eval]" > config.toml
echo "model = \"$LLM_MODEL\"" >> config.toml
@@ -99,7 +101,7 @@ jobs:
env:
SANDBOX_FORCE_REBUILD_RUNTIME: True
run: |
poetry run ./evaluation/integration_tests/scripts/run_infer.sh llm.eval HEAD CodeActAgent '' $N_PROCESSES '' 'deepseek_run'
poetry run ./evaluation/integration_tests/scripts/run_infer.sh llm.eval HEAD CodeActAgent '' 10 $N_PROCESSES '' 'deepseek_run'
# get integration tests report
REPORT_FILE_DEEPSEEK=$(find evaluation/evaluation_outputs/outputs/integration_tests/CodeActAgent/deepseek*_maxiter_10_N* -name "report.md" -type f | head -n 1)
@@ -109,11 +111,75 @@ jobs:
echo >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
# -------------------------------------------------------------
# Run DelegatorAgent tests for Haiku, limited to t01 and t02
- name: Wait a little bit (again)
run: sleep 5
- name: Configure config.toml for testing DelegatorAgent (Haiku)
env:
LLM_MODEL: "litellm_proxy/claude-3-5-haiku-20241022"
LLM_API_KEY: ${{ secrets.LLM_API_KEY }}
LLM_BASE_URL: ${{ secrets.LLM_BASE_URL }}
MAX_ITERATIONS: 30
run: |
echo "[llm.eval]" > config.toml
echo "model = \"$LLM_MODEL\"" >> config.toml
echo "api_key = \"$LLM_API_KEY\"" >> config.toml
echo "base_url = \"$LLM_BASE_URL\"" >> config.toml
echo "temperature = 0.0" >> config.toml
- name: Run integration test evaluation for DelegatorAgent (Haiku)
env:
SANDBOX_FORCE_REBUILD_RUNTIME: True
run: |
poetry run ./evaluation/integration_tests/scripts/run_infer.sh llm.eval HEAD DelegatorAgent '' 30 $N_PROCESSES "t01_fix_simple_typo,t02_add_bash_hello" 'delegator_haiku_run'
# Find and export the delegator test results
REPORT_FILE_DELEGATOR_HAIKU=$(find evaluation/evaluation_outputs/outputs/integration_tests/DelegatorAgent/*haiku*_maxiter_30_N* -name "report.md" -type f | head -n 1)
echo "REPORT_FILE_DELEGATOR_HAIKU: $REPORT_FILE_DELEGATOR_HAIKU"
echo "INTEGRATION_TEST_REPORT_DELEGATOR_HAIKU<<EOF" >> $GITHUB_ENV
cat $REPORT_FILE_DELEGATOR_HAIKU >> $GITHUB_ENV
echo >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
# -------------------------------------------------------------
# Run DelegatorAgent tests for DeepSeek, limited to t01 and t02
- name: Wait a little bit (again)
run: sleep 5
- name: Configure config.toml for testing DelegatorAgent (DeepSeek)
env:
LLM_MODEL: "litellm_proxy/deepseek-chat"
LLM_API_KEY: ${{ secrets.LLM_API_KEY }}
LLM_BASE_URL: ${{ secrets.LLM_BASE_URL }}
MAX_ITERATIONS: 30
run: |
echo "[llm.eval]" > config.toml
echo "model = \"$LLM_MODEL\"" >> config.toml
echo "api_key = \"$LLM_API_KEY\"" >> config.toml
echo "base_url = \"$LLM_BASE_URL\"" >> config.toml
echo "temperature = 0.0" >> config.toml
- name: Run integration test evaluation for DelegatorAgent (DeepSeek)
env:
SANDBOX_FORCE_REBUILD_RUNTIME: True
run: |
poetry run ./evaluation/integration_tests/scripts/run_infer.sh llm.eval HEAD DelegatorAgent '' 30 $N_PROCESSES "t01_fix_simple_typo,t02_add_bash_hello" 'delegator_deepseek_run'
# Find and export the delegator test results
REPORT_FILE_DELEGATOR_DEEPSEEK=$(find evaluation/evaluation_outputs/outputs/integration_tests/DelegatorAgent/deepseek*_maxiter_30_N* -name "report.md" -type f | head -n 1)
echo "REPORT_FILE_DELEGATOR_DEEPSEEK: $REPORT_FILE_DELEGATOR_DEEPSEEK"
echo "INTEGRATION_TEST_REPORT_DELEGATOR_DEEPSEEK<<EOF" >> $GITHUB_ENV
cat $REPORT_FILE_DELEGATOR_DEEPSEEK >> $GITHUB_ENV
echo >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
- name: Create archive of evaluation outputs
run: |
TIMESTAMP=$(date +'%y-%m-%d-%H-%M')
cd evaluation/evaluation_outputs/outputs # Change to the outputs directory
tar -czvf ../../../integration_tests_${TIMESTAMP}.tar.gz integration_tests/CodeActAgent/* # Only include the actual result directories
tar -czvf ../../../integration_tests_${TIMESTAMP}.tar.gz integration_tests/CodeActAgent/* integration_tests/DelegatorAgent/* # Only include the actual result directories
- name: Upload evaluation results as artifact
uses: actions/upload-artifact@v4
@@ -154,5 +220,11 @@ jobs:
**Integration Tests Report (DeepSeek)**
DeepSeek LLM Test Results:
${{ env.INTEGRATION_TEST_REPORT_DEEPSEEK }}
---
**Integration Tests Report Delegator (Haiku)**
${{ env.INTEGRATION_TEST_REPORT_DELEGATOR_HAIKU }}
---
**Integration Tests Report Delegator (DeepSeek)**
${{ env.INTEGRATION_TEST_REPORT_DELEGATOR_DEEPSEEK }}
---
Download testing outputs (includes both Haiku and DeepSeek results): [Download](${{ steps.upload_results_artifact.outputs.artifact-url }})

View File

@@ -259,19 +259,16 @@ jobs:
LLM_BASE_URL: ${{ secrets.LLM_BASE_URL }}
PYTHONPATH: ""
run: |
cd /tmp && BASE_COMMIT=$(cd repo && git rev-parse HEAD) && \
if [ "${{ steps.check_result.outputs.RESOLUTION_SUCCESS }}" == "true" ]; then
python -m openhands.resolver.send_pull_request \
cd /tmp && python -m openhands.resolver.send_pull_request \
--issue-number ${{ env.ISSUE_NUMBER }} \
--pr-type draft \
--reviewer ${{ github.actor }} \
--base-commit "$BASE_COMMIT" | tee pr_result.txt && \
--reviewer ${{ github.actor }} | tee pr_result.txt && \
grep "draft created" pr_result.txt | sed 's/.*\///g' > pr_number.txt
else
python -m openhands.resolver.send_pull_request \
cd /tmp && python -m openhands.resolver.send_pull_request \
--issue-number ${{ env.ISSUE_NUMBER }} \
--pr-type branch \
--base-commit "$BASE_COMMIT" \
--send-on-failure | tee branch_result.txt && \
grep "branch created" branch_result.txt | sed 's/.*\///g; s/.expand=1//g' > branch_name.txt
fi

View File

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

View File

@@ -43,17 +43,17 @@ See the [Installation](https://docs.all-hands.dev/modules/usage/installation) gu
system requirements and more information.
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.19-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.20-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.19-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.20-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.19
docker.all-hands.dev/all-hands-ai/openhands:0.20
```
You'll find OpenHands running at [http://localhost:3000](http://localhost:3000)!

View File

@@ -34,7 +34,7 @@ workspace_base = "./workspace"
# Path to store trajectories, can be a folder or a file
# If it's a folder, the session id will be used as the file name
#trajectories_path="./trajectories"
#save_trajectory_path="./trajectories"
# File store path
#file_store_path = "/tmp/file_store"

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.19-nikolaik}
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.20-nikolaik}
- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234}
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
ports:

View File

@@ -1,4 +1,4 @@
#
services:
openhands:
build:
@@ -7,8 +7,8 @@ services:
image: openhands:latest
container_name: openhands-app-${DATE:-}
environment:
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.19-nikolaik}
- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234}
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-docker.all-hands.dev/all-hands-ai/runtime:0.20-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:
- "3000:3000"
@@ -16,6 +16,7 @@ services:
- "host.docker.internal:host-gateway"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ~/.openhands-state:/.openhands-state
- ${WORKSPACE_BASE:-$PWD/workspace}:/opt/workspace_base
pull_policy: build
stdin_open: true

View File

@@ -1,5 +1,3 @@
# Options de configuration
Ce guide détaille toutes les options de configuration disponibles pour OpenHands, vous aidant à personnaliser son comportement et à l'intégrer avec d'autres services.
@@ -94,7 +92,7 @@ Les options de configuration de base sont définies dans la section `[core]` du
- Description : Désactiver la couleur dans la sortie du terminal
**Trajectoires**
- `trajectories_path`
- `save_trajectory_path`
- Type : `str`
- Valeur par défaut : `"./trajectories"`
- Description : Chemin pour stocker les trajectoires (peut être un dossier ou un fichier). Si c'est un dossier, les trajectoires seront enregistrées dans un fichier nommé avec l'ID de session et l'extension .json, dans ce dossier.
@@ -184,6 +182,10 @@ Les options de configuration LLM (Large Language Model) sont définies dans la s
Pour les utiliser avec la commande docker, passez `-e LLM_<option>`. Exemple : `-e LLM_NUM_RETRIES`.
:::note
Pour les configurations de développement, vous pouvez également définir des configurations LLM personnalisées. Voir [Configurations LLM personnalisées](./llms/custom-llm-configs) pour plus de détails.
:::
**Informations d'identification AWS**
- `aws_access_key_id`
- Type : `str`
@@ -368,4 +370,26 @@ Les options de configuration de l'agent sont définies dans les sections `[agent
- `codeact_enable_llm_editor`
- Type : `bool`
- Valeur par défaut : `false`
- Description : Si l'éditeur LLM est activé dans l'espace d'action (foncti
- Description : Si l'éditeur LLM est activé dans l'espace d'action (fonctionne uniquement avec l'appel de fonction)
**Utilisation du micro-agent**
- `use_microagents`
- Type : `bool`
- Valeur par défaut : `true`
- Description : Indique si l'utilisation des micro-agents est activée ou non
- `disabled_microagents`
- Type : `list of str`
- Valeur par défaut : `None`
- Description : Liste des micro-agents à désactiver
### Exécution
- `timeout`
- Type : `int`
- Valeur par défaut : `120`
- Description : Délai d'expiration du bac à sable, en secondes
- `user_id`
- Type : `int`
- Valeur par défaut : `1000`
- Description : ID de l'utilisateur du bac à sable

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.19-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.20-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.19 \
docker.all-hands.dev/all-hands-ai/openhands:0.20 \
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.19-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.20-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.19 \
docker.all-hands.dev/all-hands-ai/openhands:0.20 \
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.19-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.20-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.19-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.20-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.19
docker.all-hands.dev/all-hands-ai/openhands:0.20
```
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

@@ -0,0 +1,106 @@
# Configurations LLM personnalisées
OpenHands permet de définir plusieurs configurations LLM nommées dans votre fichier `config.toml`. Cette fonctionnalité vous permet d'utiliser différentes configurations LLM pour différents usages, comme utiliser un modèle moins coûteux pour les tâches qui ne nécessitent pas de réponses de haute qualité, ou utiliser différents modèles avec différents paramètres pour des agents spécifiques.
## Comment ça fonctionne
Les configurations LLM nommées sont définies dans le fichier `config.toml` en utilisant des sections qui commencent par `llm.`. Par exemple :
```toml
# Configuration LLM par défaut
[llm]
model = "gpt-4"
api_key = "votre-clé-api"
temperature = 0.0
# Configuration LLM personnalisée pour un modèle moins coûteux
[llm.gpt3]
model = "gpt-3.5-turbo"
api_key = "votre-clé-api"
temperature = 0.2
# Une autre configuration personnalisée avec des paramètres différents
[llm.haute-creativite]
model = "gpt-4"
api_key = "votre-clé-api"
temperature = 0.8
top_p = 0.9
```
Chaque configuration nommée hérite de tous les paramètres de la section `[llm]` par défaut et peut remplacer n'importe lequel de ces paramètres. Vous pouvez définir autant de configurations personnalisées que nécessaire.
## Utilisation des configurations personnalisées
### Avec les agents
Vous pouvez spécifier quelle configuration LLM un agent doit utiliser en définissant le paramètre `llm_config` dans la section de configuration de l'agent :
```toml
[agent.RepoExplorerAgent]
# Utiliser la configuration GPT-3 moins coûteuse pour cet agent
llm_config = 'gpt3'
[agent.CodeWriterAgent]
# Utiliser la configuration haute créativité pour cet agent
llm_config = 'haute-creativite'
```
### Options de configuration
Chaque configuration LLM nommée prend en charge toutes les mêmes options que la configuration LLM par défaut. Celles-ci incluent :
- Sélection du modèle (`model`)
- Configuration de l'API (`api_key`, `base_url`, etc.)
- Paramètres du modèle (`temperature`, `top_p`, etc.)
- Paramètres de nouvelle tentative (`num_retries`, `retry_multiplier`, etc.)
- Limites de jetons (`max_input_tokens`, `max_output_tokens`)
- Et toutes les autres options de configuration LLM
Pour une liste complète des options disponibles, consultez la section Configuration LLM dans la documentation des [Options de configuration](../configuration-options).
## Cas d'utilisation
Les configurations LLM personnalisées sont particulièrement utiles dans plusieurs scénarios :
- **Optimisation des coûts** : Utiliser des modèles moins coûteux pour les tâches qui ne nécessitent pas de réponses de haute qualité, comme l'exploration de dépôt ou les opérations simples sur les fichiers.
- **Réglage spécifique aux tâches** : Configurer différentes valeurs de température et de top_p pour les tâches qui nécessitent différents niveaux de créativité ou de déterminisme.
- **Différents fournisseurs** : Utiliser différents fournisseurs LLM ou points d'accès API pour différentes tâches.
- **Tests et développement** : Basculer facilement entre différentes configurations de modèles pendant le développement et les tests.
## Exemple : Optimisation des coûts
Un exemple pratique d'utilisation des configurations LLM personnalisées pour optimiser les coûts :
```toml
# Configuration par défaut utilisant GPT-4 pour des réponses de haute qualité
[llm]
model = "gpt-4"
api_key = "votre-clé-api"
temperature = 0.0
# Configuration moins coûteuse pour l'exploration de dépôt
[llm.repo-explorer]
model = "gpt-3.5-turbo"
temperature = 0.2
# Configuration pour la génération de code
[llm.code-gen]
model = "gpt-4"
temperature = 0.0
max_output_tokens = 2000
[agent.RepoExplorerAgent]
llm_config = 'repo-explorer'
[agent.CodeWriterAgent]
llm_config = 'code-gen'
```
Dans cet exemple :
- L'exploration de dépôt utilise un modèle moins coûteux car il s'agit principalement de comprendre et de naviguer dans le code
- La génération de code utilise GPT-4 avec une limite de jetons plus élevée pour générer des blocs de code plus importants
- La configuration par défaut reste disponible pour les autres tâches
:::note
Les configurations LLM personnalisées ne sont disponibles que lors de l'utilisation d'OpenHands en mode développement, via `main.py` ou `cli.py`. Lors de l'exécution via `docker run`, veuillez utiliser les options de configuration standard.
:::

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.19-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.20-nikolaik \
-v /var/run/docker.sock:/var/run/docker.sock \
# ...
```

View File

@@ -91,7 +91,7 @@
- 描述: 禁用终端输出中的颜色
**轨迹**
- `trajectories_path`
- `save_trajectory_path`
- 类型: `str`
- 默认值: `"./trajectories"`
- 描述: 存储轨迹的路径(可以是文件夹或文件)。如果是文件夹,轨迹将保存在该文件夹中以会话 ID 命名的 .json 文件中。

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.19-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.20-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.19 \
docker.all-hands.dev/all-hands-ai/openhands:0.20 \
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.19-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.20-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.19 \
docker.all-hands.dev/all-hands-ai/openhands:0.20 \
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.19-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.20-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.19-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.20-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.19
docker.all-hands.dev/all-hands-ai/openhands:0.20
```
你也可以在可脚本化的[无头模式](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.19-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.20-nikolaik \
-v /var/run/docker.sock:/var/run/docker.sock \
# ...
```

View File

@@ -7,53 +7,11 @@ If you are running in [GUI Mode](https://docs.all-hands.dev/modules/usage/how-to
take precedence.
:::
---
# Table of Contents
- [Core Configuration](#core-configuration)
- [API Keys](#api-keys)
- [Workspace](#workspace)
- [Debugging and Logging](#debugging-and-logging)
- [Session Management](#session-management)
- [Trajectories](#trajectories)
- [File Store](#file-store)
- [Task Management](#task-management)
- [Sandbox Configuration](#sandbox-configuration)
- [Miscellaneous](#miscellaneous)
- [LLM Configuration](#llm-configuration)
- [AWS Credentials](#aws-credentials)
- [API Configuration](#api-configuration)
- [Custom LLM Provider](#custom-llm-provider)
- [Embeddings](#embeddings)
- [Message Handling](#message-handling)
- [Model Selection](#model-selection)
- [Retrying](#retrying)
- [Advanced Options](#advanced-options)
- [Agent Configuration](#agent-configuration)
- [Microagent Configuration](#microagent-configuration)
- [Memory Configuration](#memory-configuration)
- [LLM Configuration](#llm-configuration-2)
- [ActionSpace Configuration](#actionspace-configuration)
- [Microagent Usage](#microagent-usage)
- [Sandbox Configuration](#sandbox-configuration)
- [Execution](#execution)
- [Container Image](#container-image)
- [Networking](#networking)
- [Linting and Plugins](#linting-and-plugins)
- [Dependencies and Environment](#dependencies-and-environment)
- [Evaluation](#evaluation)
- [Security Configuration](#security-configuration)
- [Confirmation Mode](#confirmation-mode)
- [Security Analyzer](#security-analyzer)
---
## Core Configuration
The core configuration options are defined in the `[core]` section of the `config.toml` file.
**API Keys**
### API Keys
- `e2b_api_key`
- Type: `str`
- Default: `""`
@@ -69,7 +27,7 @@ The core configuration options are defined in the `[core]` section of the `confi
- Default: `""`
- Description: API token secret for Modal
**Workspace**
### Workspace
- `workspace_base`
- Type: `str`
- Default: `"./workspace"`
@@ -80,7 +38,7 @@ The core configuration options are defined in the `[core]` section of the `confi
- Default: `"/tmp/cache"`
- Description: Cache directory path
**Debugging and Logging**
### Debugging and Logging
- `debug`
- Type: `bool`
- Default: `false`
@@ -91,13 +49,13 @@ The core configuration options are defined in the `[core]` section of the `confi
- Default: `false`
- Description: Disable color in terminal output
**Trajectories**
- `trajectories_path`
### Trajectories
- `save_trajectory_path`
- Type: `str`
- Default: `"./trajectories"`
- Description: Path to store trajectories (can be a folder or a file). If it's a folder, the trajectories will be saved in a file named with the session id name and .json extension, in that folder.
**File Store**
### File Store
- `file_store_path`
- Type: `str`
- Default: `"/tmp/file_store"`
@@ -128,7 +86,7 @@ The core configuration options are defined in the `[core]` section of the `confi
- Default: `[".*"]`
- Description: List of allowed file extensions for uploads
**Task Management**
### Task Management
- `max_budget_per_task`
- Type: `float`
- Default: `0.0`
@@ -139,7 +97,7 @@ The core configuration options are defined in the `[core]` section of the `confi
- Default: `100`
- Description: Maximum number of iterations
**Sandbox Configuration**
### Sandbox Configuration
- `workspace_mount_path_in_sandbox`
- Type: `str`
- Default: `"/workspace"`
@@ -155,7 +113,7 @@ The core configuration options are defined in the `[core]` section of the `confi
- Default: `""`
- Description: Path to rewrite the workspace mount path to. You can usually ignore this, it refers to special cases of running inside another container.
**Miscellaneous**
### Miscellaneous
- `run_as_openhands`
- Type: `bool`
- Default: `true`
@@ -182,6 +140,10 @@ The LLM (Large Language Model) configuration options are defined in the `[llm]`
To use these with the docker command, pass in `-e LLM_<option>`. Example: `-e LLM_NUM_RETRIES`.
:::note
For development setups, you can also define custom named LLM configurations. See [Custom LLM Configurations](./llms/custom-llm-configs) for details.
:::
**AWS Credentials**
- `aws_access_key_id`
- Type: `str`
@@ -198,7 +160,7 @@ To use these with the docker command, pass in `-e LLM_<option>`. Example: `-e LL
- Default: `""`
- Description: AWS secret access key
**API Configuration**
### API Configuration
- `api_key`
- Type: `str`
- Default: `None`
@@ -224,13 +186,13 @@ To use these with the docker command, pass in `-e LLM_<option>`. Example: `-e LL
- Default: `0.0`
- Description: Cost per output token
**Custom LLM Provider**
### Custom LLM Provider
- `custom_llm_provider`
- Type: `str`
- Default: `""`
- Description: Custom LLM provider
**Embeddings**
### Embeddings
- `embedding_base_url`
- Type: `str`
- Default: `""`
@@ -246,7 +208,7 @@ To use these with the docker command, pass in `-e LLM_<option>`. Example: `-e LL
- Default: `"local"`
- Description: Embedding model to use
**Message Handling**
### Message Handling
- `max_message_chars`
- Type: `int`
- Default: `30000`
@@ -262,13 +224,13 @@ To use these with the docker command, pass in `-e LLM_<option>`. Example: `-e LL
- Default: `0`
- Description: Maximum number of output tokens
**Model Selection**
### Model Selection
- `model`
- Type: `str`
- Default: `"claude-3-5-sonnet-20241022"`
- Description: Model to use
**Retrying**
### Retrying
- `num_retries`
- Type: `int`
- Default: `8`
@@ -289,7 +251,7 @@ To use these with the docker command, pass in `-e LLM_<option>`. Example: `-e LL
- Default: `2.0`
- Description: Multiplier for exponential backoff calculation
**Advanced Options**
### Advanced Options
- `drop_params`
- Type: `bool`
- Default: `false`
@@ -329,13 +291,13 @@ To use these with the docker command, pass in `-e LLM_<option>`. Example: `-e LL
The agent configuration options are defined in the `[agent]` and `[agent.<agent_name>]` sections of the `config.toml` file.
**Microagent Configuration**
### Microagent Configuration
- `micro_agent_name`
- Type: `str`
- Default: `""`
- Description: Name of the micro agent to use for this agent
**Memory Configuration**
### Memory Configuration
- `memory_enabled`
- Type: `bool`
- Default: `false`
@@ -346,13 +308,13 @@ The agent configuration options are defined in the `[agent]` and `[agent.<agent_
- Default: `3`
- Description: The maximum number of threads indexing at the same time for embeddings
**LLM Configuration**
### LLM Configuration
- `llm_config`
- Type: `str`
- Default: `'your-llm-config-group'`
- Description: The name of the LLM config to use
**ActionSpace Configuration**
### ActionSpace Configuration
- `function_calling`
- Type: `bool`
- Default: `true`
@@ -373,7 +335,7 @@ The agent configuration options are defined in the `[agent]` and `[agent.<agent_
- Default: `false`
- Description: Whether Jupyter is enabled in the action space
**Microagent Usage**
### Microagent Usage
- `use_microagents`
- Type: `bool`
- Default: `true`
@@ -390,7 +352,7 @@ The sandbox configuration options are defined in the `[sandbox]` section of the
To use these with the docker command, pass in `-e SANDBOX_<option>`. Example: `-e SANDBOX_TIMEOUT`.
**Execution**
### Execution
- `timeout`
- Type: `int`
- Default: `120`
@@ -401,19 +363,19 @@ To use these with the docker command, pass in `-e SANDBOX_<option>`. Example: `-
- Default: `1000`
- Description: Sandbox user ID
**Container Image**
### Container Image
- `base_container_image`
- Type: `str`
- Default: `"nikolaik/python-nodejs:python3.12-nodejs22"`
- Description: Container image to use for the sandbox
**Networking**
### Networking
- `use_host_network`
- Type: `bool`
- Default: `false`
- Description: Use host network
**Linting and Plugins**
### Linting and Plugins
- `enable_auto_lint`
- Type: `bool`
- Default: `false`
@@ -424,7 +386,7 @@ To use these with the docker command, pass in `-e SANDBOX_<option>`. Example: `-
- Default: `true`
- Description: Whether to initialize plugins
**Dependencies and Environment**
### Dependencies and Environment
- `runtime_extra_deps`
- Type: `str`
- Default: `""`
@@ -435,7 +397,7 @@ To use these with the docker command, pass in `-e SANDBOX_<option>`. Example: `-
- Default: `{}`
- Description: Environment variables to set at the launch of the runtime
**Evaluation**
### Evaluation
- `browsergym_eval_env`
- Type: `str`
- Default: `""`
@@ -447,13 +409,13 @@ The security configuration options are defined in the `[security]` section of th
To use these with the docker command, pass in `-e SECURITY_<option>`. Example: `-e SECURITY_CONFIRMATION_MODE`.
**Confirmation Mode**
### Confirmation Mode
- `confirmation_mode`
- Type: `bool`
- Default: `false`
- Description: Enable confirmation mode
**Security Analyzer**
### Security Analyzer
- `security_analyzer`
- Type: `str`
- Default: `""`

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.19-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.20-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.19 \
docker.all-hands.dev/all-hands-ai/openhands:0.20 \
python -m openhands.core.cli
```
@@ -58,7 +58,7 @@ Here are some examples of CLI commands and their expected outputs:
### Example 1: Simple Task
```bash
How can I help? >> Write a Python script that prints "Hello, World!"
>> Write a Python script that prints "Hello, World!"
```
Expected Output:
@@ -72,7 +72,7 @@ Expected Output:
### Example 2: Bash Command
```bash
How can I help? >> Create a directory named "test_dir"
>> Create a directory named "test_dir"
```
Expected Output:
@@ -86,7 +86,7 @@ Expected Output:
### Example 3: Error Handling
```bash
How can I help? >> Delete a non-existent file
>> Delete a non-existent file
```
Expected Output:

View File

@@ -76,18 +76,18 @@ When using OpenHands in online mode, the GitHub OAuth flow:
Common issues and solutions:
1. **Token Not Recognized**:
- **Token Not Recognized**:
- Ensure the token is properly saved in settings.
- Check that the token hasn't expired.
- Verify the token has the required scopes.
- Try regenerating the token.
2. **Organization Access Denied**:
- **Organization Access Denied**:
- Check if SSO is required but not enabled.
- Verify organization membership.
- Contact organization admin if token policies are blocking access.
3. **Verifying Token Works**:
- **Verifying Token Works**:
- The app will show a green checkmark if the token is valid.
- Try accessing a repository to confirm permissions.
- Check the browser console for any error messages.

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.19-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.20-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.19 \
docker.all-hands.dev/all-hands-ai/openhands:0.20 \
python -m openhands.core.main -t "write a bash script that prints hi"
```

View File

@@ -1,16 +0,0 @@
# Persisting Session Data
Using the standard Development Workflow, the session data is stored in memory. Currently, if OpenHands' service is restarted,
previous sessions become invalid (a new secret is generated) and thus not recoverable.
## How to Persist Session Data
### Development Workflow
In the `config.toml` file, specify the following:
```
[core]
...
file_store="local"
file_store_path="/absolute/path/to/openhands/cache/directory"
jwt_secret="secretpass"
```

View File

@@ -11,17 +11,17 @@
The easiest way to run OpenHands is in Docker.
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.19-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.20-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.19-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.20-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.19
docker.all-hands.dev/all-hands-ai/openhands:0.20
```
You'll find OpenHands running at http://localhost:3000!

View File

@@ -0,0 +1,106 @@
# Custom LLM Configurations
OpenHands supports defining multiple named LLM configurations in your `config.toml` file. This feature allows you to use different LLM configurations for different purposes, such as using a cheaper model for tasks that don't require high-quality responses, or using different models with different parameters for specific agents.
## How It Works
Named LLM configurations are defined in the `config.toml` file using sections that start with `llm.`. For example:
```toml
# Default LLM configuration
[llm]
model = "gpt-4"
api_key = "your-api-key"
temperature = 0.0
# Custom LLM configuration for a cheaper model
[llm.gpt3]
model = "gpt-3.5-turbo"
api_key = "your-api-key"
temperature = 0.2
# Another custom configuration with different parameters
[llm.high-creativity]
model = "gpt-4"
api_key = "your-api-key"
temperature = 0.8
top_p = 0.9
```
Each named configuration inherits all settings from the default `[llm]` section and can override any of those settings. You can define as many custom configurations as needed.
## Using Custom Configurations
### With Agents
You can specify which LLM configuration an agent should use by setting the `llm_config` parameter in the agent's configuration section:
```toml
[agent.RepoExplorerAgent]
# Use the cheaper GPT-3 configuration for this agent
llm_config = 'gpt3'
[agent.CodeWriterAgent]
# Use the high creativity configuration for this agent
llm_config = 'high-creativity'
```
### Configuration Options
Each named LLM configuration supports all the same options as the default LLM configuration. These include:
- Model selection (`model`)
- API configuration (`api_key`, `base_url`, etc.)
- Model parameters (`temperature`, `top_p`, etc.)
- Retry settings (`num_retries`, `retry_multiplier`, etc.)
- Token limits (`max_input_tokens`, `max_output_tokens`)
- And all other LLM configuration options
For a complete list of available options, see the LLM Configuration section in the [Configuration Options](../configuration-options) documentation.
## Use Cases
Custom LLM configurations are particularly useful in several scenarios:
- **Cost Optimization**: Use cheaper models for tasks that don't require high-quality responses, like repository exploration or simple file operations.
- **Task-Specific Tuning**: Configure different temperature and top_p values for tasks that require different levels of creativity or determinism.
- **Different Providers**: Use different LLM providers or API endpoints for different tasks.
- **Testing and Development**: Easily switch between different model configurations during development and testing.
## Example: Cost Optimization
A practical example of using custom LLM configurations to optimize costs:
```toml
# Default configuration using GPT-4 for high-quality responses
[llm]
model = "gpt-4"
api_key = "your-api-key"
temperature = 0.0
# Cheaper configuration for repository exploration
[llm.repo-explorer]
model = "gpt-3.5-turbo"
temperature = 0.2
# Configuration for code generation
[llm.code-gen]
model = "gpt-4"
temperature = 0.0
max_output_tokens = 2000
[agent.RepoExplorerAgent]
llm_config = 'repo-explorer'
[agent.CodeWriterAgent]
llm_config = 'code-gen'
```
In this example:
- Repository exploration uses a cheaper model since it mainly involves understanding and navigating code
- Code generation uses GPT-4 with a higher token limit for generating larger code blocks
- The default configuration remains available for other tasks
:::note
Custom LLM configurations are only available when using OpenHands in development mode, via `main.py` or `cli.py`. When running via `docker run`, please use the standard configuration options.
:::

View File

@@ -0,0 +1,36 @@
# Microagents Overview
Microagents are specialized prompts that enhance OpenHands with domain-specific knowledge, repository-specific context
and task-specific workflows. They help by providing expert guidance, automating common tasks, and ensuring
consistent practices across projects.
## Microagent Types
Currently OpenHands supports the following types of microagents:
* [Repository Microagents](./microagents-repo): Repository-specific context and guidelines for OpenHands.
* [Public Microagents](./microagents-public): General guidelines triggered by keywords for all OpenHands users.
When OpenHands works with a repository, it:
1. Loads repository-specific instructions from `.openhands/microagents/` if present in the repository.
2. Loads general guidelines triggered by keywords in conversations.
See current [Public Microagents](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/knowledge).
## Microagent Format
All microagents use markdown files with YAML frontmatter that have special instructions to help OpenHands accomplish
tasks:
```
---
name: <Name of the microagent>
type: <MicroAgent type>
version: <MicroAgent version>
agent: <The agent type (Typically CodeActAgent)>
triggers:
- <Optional keywords triggering the microagent. If triggers are removed, it will always be included>
---
<Markdown with any special guidelines, instructions, and prompts that OpenHands should follow.
Check out the specific documentation for each microagent on best practices for more information.>
```

View File

@@ -1,31 +1,21 @@
# Public Micro-Agents
OpenHands uses specialized micro-agents to handle specific tasks and contexts efficiently. These micro-agents are small,
focused components that provide specialized behavior and knowledge for particular scenarios.
# Public Microagents
## Overview
Public micro-agents are defined in markdown files under the
Public microagents are specialized guidelines triggered by keywords for all OpenHands users.
They are defined in markdown files under the
[`microagents/knowledge/`](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/knowledge) directory.
Each micro-agent is configured with:
- A unique name.
- The agent type (typically CodeActAgent).
- Trigger keywords that activate the agent.
- Specific instructions and capabilities.
### Integration
Public micro-agents are automatically integrated into OpenHands' workflow. They:
Public microagents:
- Monitor incoming commands for their trigger words.
- Activate when relevant triggers are detected.
- Apply their specialized knowledge and capabilities.
- Follow their specific guidelines and restrictions.
## Available Public Micro-Agents
## Current Public Microagents
For more information about specific micro-agents, refer to their individual documentation files in
the [`micro-agents`](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents) directory.
For more information about specific microagents, refer to their individual documentation files in
the [`microagents/knowledge/`](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/knowledge/) directory.
### GitHub Agent
**File**: `github.md`
@@ -66,105 +56,54 @@ Usage Example:
yes | npm install package-name
```
### Custom Public Micro-Agents
## Contributing a Public Microagent
You can create your own public micro-agents by adding new markdown files to the `microagents/knowledge/` directory.
Each file should follow this structure:
You can create your own public microagents by adding new markdown files to the
[`microagents/knowledge/`](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/knowledge/) directory.
```markdown
---
name: agent_name
agent: CodeActAgent
triggers:
- trigger_word1
- trigger_word2
---
### Public Microagents Best Practices
Instructions and capabilities for the micro-agent...
```
## Working With Public Micro-Agents
When working with public micro-agents:
- **Use Appropriate Triggers**: Ensure your commands include the relevant trigger words to activate the correct micro-agent.
- **Follow Agent Guidelines**: Each agent has specific instructions and limitations. Respect these for optimal results.
- **API-First Approach**: When available, use API endpoints rather than web interfaces.
- **Automation Friendly**: Design commands that work well in non-interactive environments.
## Contributing a Public Micro-Agent
Best practices for creating public micro-agents:
- **Clear Scope**: Keep the micro-agent focused on a specific domain or task.
- **Clear Scope**: Keep the microagent focused on a specific domain or task.
- **Explicit Instructions**: Provide clear, unambiguous guidelines.
- **Useful Examples**: Include practical examples of common use cases.
- **Safety First**: Include necessary warnings and constraints.
- **Integration Awareness**: Consider how the micro-agent interacts with other components.
- **Integration Awareness**: Consider how the microagent interacts with other components.
To contribute a new micro-agent to OpenHands:
### Steps to Contribute a Public Microagent
### 1. Plan the Public Micro-Agent
#### 1. Plan the Public Microagent
Before creating a public micro-agent, consider:
Before creating a public microagent, consider:
- What specific problem or use case will it address?
- What unique capabilities or knowledge should it have?
- What trigger words make sense for activating it?
- What constraints or guidelines should it follow?
### 2. File Structure
#### 2. Create File
Create a new markdown file in `microagents/knowledge/` with a descriptive name (e.g., `docker.md` for a Docker-focused agent).
Create a new markdown file in [`microagents/knowledge/`](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/knowledge/)
with a descriptive name (e.g., `docker.md` for a Docker-focused agent).
### 3. Required Components
Update the file with the required frontmatter [according to the required format](./microagents-overview#microagent-format)
and the required specialized guidelines while following the [best practices above](#public-microagents-best-practices).
The micro-agent file must include:
#### 3. Testing the Public Microagent
- **Front Matter**: YAML metadata at the start of the file:
```markdown
---
name: your_agent_name
agent: CodeActAgent
triggers:
- trigger_word1
- trigger_word2
---
```
- **Instructions**: Clear, specific guidelines for the agent's behavior:
```markdown
You are responsible for [specific task/domain].
Key responsibilities:
1. [Responsibility 1]
2. [Responsibility 2]
Guidelines:
- [Guideline 1]
- [Guideline 2]
Examples of usage:
[Example 1]
[Example 2]
```
### 4. Testing the Public Micro-Agent
Before submitting:
- Test the agent with various prompts.
- Verify trigger words activate the agent correctly.
- Ensure instructions are clear and comprehensive.
- Check for potential conflicts with existing agents.
### 5. Submission Process
#### 4. Submission Process
Submit a pull request with:
- The new micro-agent file.
- The new microagent file.
- Updated documentation if needed.
- Description of the agent's purpose and capabilities.
### Example Public Micro-Agent Implementation
### Example Public Microagent Implementation
Here's a template for a new micro-agent:
Here's a template for a new microagent:
```markdown
---
@@ -210,5 +149,5 @@ Remember to:
- Optimize for build time and image size
```
Remember that micro-agents are a powerful way to extend OpenHands' capabilities in specific domains. Well-designed
agents can significantly improve the system's ability to handle specialized tasks.
See the [current public micro-agents](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/knowledge) for
more examples.

View File

@@ -1,25 +1,51 @@
# Repository Micro-Agents
# Repository Microagents
## Overview
OpenHands can be customized to work more effectively with specific repositories by providing repository-specific context
and guidelines. This section explains how to optimize OpenHands for your project.
## Repository Configuration
## Creating a Repository Micro-Agent
You can customize OpenHands' behavior for your repository by creating a `.openhands/microagents/` directory in your repository's root.
At minimum, it should contain the file
At minimum it should contain the file
`.openhands/microagents/repo.md`, which includes instructions that will
be given to the agent every time it works with this repository.
We suggest including the following information:
### Repository Microagents Best Practices
- **Keep Instructions Updated**: Regularly update your `.openhands/microagents/` directory as your project evolves.
- **Be Specific**: Include specific paths, patterns, and requirements unique to your project.
- **Document Dependencies**: List all tools and dependencies required for development.
- **Include Examples**: Provide examples of good code patterns from your project.
- **Specify Conventions**: Document naming conventions, file organization, and code style preferences.
### Steps to Create a Repository Microagent
#### 1. Plan the Repository Microagent
When creating a repository-specific micro-agent, we suggest including the following information:
- **Repository Overview**: A brief description of your project's purpose and architecture.
- **Directory Structure**: Key directories and their purposes.
- **Development Guidelines**: Project-specific coding standards and practices.
- **Testing Requirements**: How to run tests and what types of tests are required.
- **Setup Instructions**: Steps needed to build and run the project.
### Example Repository Configuration
Example `.openhands/microagents/repo.md` file:
#### 2. Create File
Create a file in your repository under `.openhands/microagents/` (Example: `.openhands/microagents/repo.md`)
Update the file with the required frontmatter [according to the required format](./microagents-overview#microagent-format)
and the required specialized guidelines for your repository.
### Example Repository Microagent
```
---
name: repo
type: repo
agent: CodeActAgent
---
Repository: MyProject
Description: A web application for task management
@@ -37,30 +63,6 @@ Guidelines:
- Follow ESLint configuration
- Write tests for all new features
- Use TypeScript for new code
If adding a new component in src/components, always add appropriate unit tests in tests/components/.
```
### Customizing Prompts
You may also add customized prompts to the `.openhands/microagents/repo.md` file when working with a repository.
These could:
- **Reference Project Standards**: Mention specific coding standards or patterns used in your project.
- **Include Context**: Reference relevant documentation or existing implementations.
- **Specify Testing Requirements**: Include project-specific testing requirements in your prompts.
Example customized prompt:
```
Add a new task completion feature to src/components/TaskList.tsx following our existing component patterns.
Include unit tests in tests/components/ and update the documentation in docs/features/.
The component should use our shared styling from src/styles/components.
```
### Best Practices for Repository Customization
- **Keep Instructions Updated**: Regularly update your `.openhands/microagents/` directory as your project evolves.
- **Be Specific**: Include specific paths, patterns, and requirements unique to your project.
- **Document Dependencies**: List all tools and dependencies required for development.
- **Include Examples**: Provide examples of good code patterns from your project.
- **Specify Conventions**: Document naming conventions, file organization, and code style preferences.
By customizing OpenHands for your repository, you'll get more accurate and consistent results that align with your project's standards and requirements.

View File

@@ -16,7 +16,7 @@ some flags being passed to `docker run` that make this possible:
```
docker run # ...
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.19-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.20-nikolaik \
-v /var/run/docker.sock:/var/run/docker.sock \
# ...
```

View File

@@ -24,18 +24,23 @@ const sidebars: SidebarsConfig = {
},
{
type: 'category',
label: 'Micro-Agents',
label: 'Microagents',
items: [
{
type: 'doc',
label: 'Public',
id: 'usage/prompting/microagents-public',
label: 'Overview',
id: 'usage/prompting/microagents-overview',
},
{
type: 'doc',
label: 'Repository',
id: 'usage/prompting/microagents-repo',
},
{
type: 'doc',
label: 'Public',
id: 'usage/prompting/microagents-public',
},
],
}
],
@@ -132,11 +137,6 @@ const sidebars: SidebarsConfig = {
label: 'Custom Sandbox',
id: 'usage/how-to/custom-sandbox-guide',
},
{
type: 'doc',
label: 'Persist Session Data',
id: 'usage/how-to/persist-session-data',
},
],
},
{

View File

@@ -39,7 +39,7 @@ def get_config(
run_as_openhands=False,
max_budget_per_task=4,
max_iterations=100,
trajectories_path=os.path.join(
save_trajectory_path=os.path.join(
mount_path_on_host, f'traj_{task_short_name}.json'
),
sandbox=SandboxConfig(

View File

@@ -8,13 +8,15 @@ from evaluation.integration_tests.tests.base import BaseIntegrationTest, TestRes
from evaluation.utils.shared import (
EvalMetadata,
EvalOutput,
codeact_user_response,
make_metadata,
prepare_dataset,
reset_logger_for_multiprocessing,
run_evaluation,
update_llm_config_for_completions_logging,
)
from evaluation.utils.shared import (
codeact_user_response as fake_user_response,
)
from openhands.controller.state.state import State
from openhands.core.config import (
AgentConfig,
@@ -31,7 +33,8 @@ from openhands.runtime.base import Runtime
from openhands.utils.async_utils import call_async_from_sync
FAKE_RESPONSES = {
'CodeActAgent': codeact_user_response,
'CodeActAgent': fake_user_response,
'DelegatorAgent': fake_user_response,
}
@@ -219,7 +222,7 @@ if __name__ == '__main__':
df = pd.read_json(output_file, lines=True, orient='records')
# record success and reason for failure for the final report
# record success and reason
df['success'] = df['test_result'].apply(lambda x: x['success'])
df['reason'] = df['test_result'].apply(lambda x: x['reason'])
logger.info('-' * 100)
@@ -234,15 +237,27 @@ if __name__ == '__main__':
logger.info('-' * 100)
# record cost for each instance, with 3 decimal places
df['cost'] = df['metrics'].apply(lambda x: round(x['accumulated_cost'], 3))
# we sum up all the "costs" from the metrics array
df['cost'] = df['metrics'].apply(
lambda m: round(sum(c['cost'] for c in m['costs']), 3)
if m and 'costs' in m
else 0.0
)
# capture the top-level error if present, per instance
df['error_message'] = df.get('error', None)
logger.info(f'Total cost: USD {df["cost"].sum():.2f}')
report_file = os.path.join(metadata.eval_output_dir, 'report.md')
with open(report_file, 'w') as f:
f.write(
f'Success rate: {df["success"].mean():.2%} ({df["success"].sum()}/{len(df)})\n'
f'Success rate: {df["success"].mean():.2%}'
f' ({df["success"].sum()}/{len(df)})\n'
)
f.write(f'\nTotal cost: USD {df["cost"].sum():.2f}\n')
f.write(
df[['instance_id', 'success', 'reason', 'cost']].to_markdown(index=False)
df[
['instance_id', 'success', 'reason', 'cost', 'error_message']
].to_markdown(index=False)
)

View File

@@ -7,8 +7,9 @@ MODEL_CONFIG=$1
COMMIT_HASH=$2
AGENT=$3
EVAL_LIMIT=$4
NUM_WORKERS=$5
EVAL_IDS=$6
MAX_ITERATIONS=$5
NUM_WORKERS=$6
EVAL_IDS=$7
if [ -z "$NUM_WORKERS" ]; then
NUM_WORKERS=1
@@ -43,7 +44,7 @@ fi
COMMAND="poetry run python evaluation/integration_tests/run_infer.py \
--agent-cls $AGENT \
--llm-config $MODEL_CONFIG \
--max-iterations 10 \
--max-iterations ${MAX_ITERATIONS:-10} \
--eval-num-workers $NUM_WORKERS \
--eval-note $EVAL_NOTE"

View File

@@ -1,47 +0,0 @@
import { describe, expect, it, vi } from "vitest";
import { retrieveLatestGitHubCommit } from "../../src/api/github";
describe("retrieveLatestGitHubCommit", () => {
const { githubGetMock } = vi.hoisted(() => ({
githubGetMock: vi.fn(),
}));
vi.mock("../../src/api/github-axios-instance", () => ({
github: {
get: githubGetMock,
},
}));
it("should return the latest commit when repository has commits", async () => {
const mockCommit = {
sha: "123abc",
commit: {
message: "Initial commit",
},
};
githubGetMock.mockResolvedValueOnce({
data: [mockCommit],
});
const result = await retrieveLatestGitHubCommit("user/repo");
expect(result).toEqual(mockCommit);
});
it("should return null when repository is empty", async () => {
const error = new Error("Repository is empty");
(error as any).response = { status: 409 };
githubGetMock.mockRejectedValueOnce(error);
const result = await retrieveLatestGitHubCommit("user/empty-repo");
expect(result).toBeNull();
});
it("should throw error for other error cases", async () => {
const error = new Error("Network error");
(error as any).response = { status: 500 };
githubGetMock.mockRejectedValueOnce(error);
await expect(retrieveLatestGitHubCommit("user/repo")).rejects.toThrow();
});
});

View File

@@ -1,11 +1,10 @@
import { describe, it, expect, afterEach, vi } from "vitest";
import * as router from "react-router";
// Mock useParams before importing components
vi.mock("react-router", async () => {
const actual = await vi.importActual("react-router");
return {
...actual as object,
...(actual as object),
useParams: () => ({ conversationId: "test-conversation-id" }),
};
});
@@ -14,7 +13,7 @@ vi.mock("react-router", async () => {
vi.mock("react-i18next", async () => {
const actual = await vi.importActual("react-i18next");
return {
...actual as object,
...(actual as object),
useTranslation: () => ({
t: (key: string) => key,
i18n: {
@@ -28,7 +27,6 @@ import { screen } from "@testing-library/react";
import { renderWithProviders } from "../../test-utils";
import { BrowserPanel } from "#/components/features/browser/browser";
describe("Browser", () => {
afterEach(() => {
vi.clearAllMocks();
@@ -45,7 +43,7 @@ describe("Browser", () => {
});
// i18n empty message key
expect(screen.getByText("BROWSER$EMPTY_MESSAGE")).toBeInTheDocument();
expect(screen.getByText("BROWSER$NO_PAGE_LOADED")).toBeInTheDocument();
});
it("renders the url and a screenshot", () => {

View File

@@ -84,12 +84,10 @@ describe("ChatInput", () => {
expect(onSubmitMock).not.toHaveBeenCalled();
});
it("should render a placeholder", () => {
render(
<ChatInput placeholder="Enter your message" onSubmit={onSubmitMock} />,
);
it("should render a placeholder with translation key", () => {
render(<ChatInput onSubmit={onSubmitMock} />);
const textarea = screen.getByPlaceholderText("Enter your message");
const textarea = screen.getByPlaceholderText("SUGGESTIONS$WHAT_TO_BUILD");
expect(textarea).toBeInTheDocument();
});

View File

@@ -2,36 +2,42 @@ import { describe, expect, it } from "vitest";
import { screen } from "@testing-library/react";
import { renderWithProviders } from "test-utils";
import { ExpandableMessage } from "#/components/features/chat/expandable-message";
import { vi } from 'vitest';
import { vi } from "vitest";
vi.mock('react-i18next', async () => {
const actual = await vi.importActual('react-i18next');
vi.mock("react-i18next", async () => {
const actual = await vi.importActual("react-i18next");
return {
...actual,
useTranslation: () => ({
t: (key:string) => key,
t: (key: string) => key,
i18n: {
changeLanguage: () => new Promise(() => {}),
language: 'en',
language: "en",
exists: () => true,
},
}),
}
};
});
describe("ExpandableMessage", () => {
it("should render with neutral border for non-action messages", () => {
renderWithProviders(<ExpandableMessage message="Hello" type="thought" />);
const element = screen.getByText("Hello");
const container = element.closest("div.flex.gap-2.items-center.justify-start");
const container = element.closest(
"div.flex.gap-2.items-center.justify-start",
);
expect(container).toHaveClass("border-neutral-300");
expect(screen.queryByTestId("status-icon")).not.toBeInTheDocument();
});
it("should render with neutral border for error messages", () => {
renderWithProviders(<ExpandableMessage message="Error occurred" type="error" />);
renderWithProviders(
<ExpandableMessage message="Error occurred" type="error" />,
);
const element = screen.getByText("Error occurred");
const container = element.closest("div.flex.gap-2.items-center.justify-start");
const container = element.closest(
"div.flex.gap-2.items-center.justify-start",
);
expect(container).toHaveClass("border-danger");
expect(screen.queryByTestId("status-icon")).not.toBeInTheDocument();
});
@@ -43,10 +49,12 @@ describe("ExpandableMessage", () => {
message="Command executed successfully"
type="action"
success={true}
/>
/>,
);
const element = screen.getByText("OBSERVATION_MESSAGE$RUN");
const container = element.closest("div.flex.gap-2.items-center.justify-start");
const container = element.closest(
"div.flex.gap-2.items-center.justify-start",
);
expect(container).toHaveClass("border-neutral-300");
const icon = screen.getByTestId("status-icon");
expect(icon).toHaveClass("fill-success");
@@ -59,10 +67,12 @@ describe("ExpandableMessage", () => {
message="Command failed"
type="action"
success={false}
/>
/>,
);
const element = screen.getByText("OBSERVATION_MESSAGE$RUN");
const container = element.closest("div.flex.gap-2.items-center.justify-start");
const container = element.closest(
"div.flex.gap-2.items-center.justify-start",
);
expect(container).toHaveClass("border-neutral-300");
const icon = screen.getByTestId("status-icon");
expect(icon).toHaveClass("fill-danger");
@@ -74,10 +84,12 @@ describe("ExpandableMessage", () => {
id="OBSERVATION_MESSAGE$RUN"
message="Running command"
type="action"
/>
/>,
);
const element = screen.getByText("OBSERVATION_MESSAGE$RUN");
const container = element.closest("div.flex.gap-2.items-center.justify-start");
const container = element.closest(
"div.flex.gap-2.items-center.justify-start",
);
expect(container).toHaveClass("border-neutral-300");
expect(screen.queryByTestId("status-icon")).not.toBeInTheDocument();
});

View File

@@ -9,6 +9,7 @@ describe("ConversationCard", () => {
const onClick = vi.fn();
const onDelete = vi.fn();
const onChangeTitle = vi.fn();
const onDownloadWorkspace = vi.fn();
afterEach(() => {
vi.clearAllMocks();
@@ -233,6 +234,120 @@ describe("ConversationCard", () => {
expect(onClick).not.toHaveBeenCalled();
});
it("should call onDownloadWorkspace when the download button is clicked", async () => {
const user = userEvent.setup();
render(
<ConversationCard
onClick={onClick}
onDelete={onDelete}
onChangeTitle={onChangeTitle}
onDownloadWorkspace={onDownloadWorkspace}
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
/>,
);
const ellipsisButton = screen.getByTestId("ellipsis-button");
await user.click(ellipsisButton);
const menu = screen.getByTestId("context-menu");
const downloadButton = within(menu).getByTestId("download-button");
await user.click(downloadButton);
expect(onDownloadWorkspace).toHaveBeenCalled();
});
it("should not display the edit or delete options if the handler is not provided", async () => {
const user = userEvent.setup();
const { rerender } = render(
<ConversationCard
onClick={onClick}
onChangeTitle={onChangeTitle}
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
/>,
);
const ellipsisButton = screen.getByTestId("ellipsis-button");
await user.click(ellipsisButton);
expect(screen.queryByTestId("edit-button")).toBeInTheDocument();
expect(screen.queryByTestId("delete-button")).not.toBeInTheDocument();
// toggle to hide the context menu
await user.click(ellipsisButton);
rerender(
<ConversationCard
onClick={onClick}
onDelete={onDelete}
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
/>,
);
await user.click(ellipsisButton);
expect(screen.queryByTestId("edit-button")).not.toBeInTheDocument();
expect(screen.queryByTestId("delete-button")).toBeInTheDocument();
});
it("should not render the ellipsis button if there are no actions", () => {
const { rerender } = render(
<ConversationCard
onClick={onClick}
onDelete={onDelete}
onChangeTitle={onChangeTitle}
onDownloadWorkspace={onDownloadWorkspace}
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
/>,
);
expect(screen.getByTestId("ellipsis-button")).toBeInTheDocument();
rerender(
<ConversationCard
onClick={onClick}
onDelete={onDelete}
onDownloadWorkspace={onDownloadWorkspace}
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
/>,
);
expect(screen.getByTestId("ellipsis-button")).toBeInTheDocument();
rerender(
<ConversationCard
onClick={onClick}
onDownloadWorkspace={onDownloadWorkspace}
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
/>,
);
expect(screen.queryByTestId("ellipsis-button")).toBeInTheDocument();
rerender(
<ConversationCard
onClick={onClick}
title="Conversation 1"
selectedRepository={null}
lastUpdatedAt="2021-10-01T12:00:00Z"
/>,
);
expect(screen.queryByTestId("ellipsis-button")).not.toBeInTheDocument();
});
describe("state indicator", () => {
it("should render the 'STOPPED' indicator by default", () => {
render(

View File

@@ -71,7 +71,7 @@ describe("ConversationPanel", () => {
renderConversationPanel();
const emptyState = await screen.findByText("No conversations found");
const emptyState = await screen.findByText("CONVERSATION$NO_CONVERSATIONS");
expect(emptyState).toBeInTheDocument();
});

View File

@@ -3,7 +3,6 @@ import { describe, expect, it, vi } from "vitest";
import { renderWithProviders } from "test-utils";
import { GitHubRepositorySelector } from "#/components/features/github/github-repo-selector";
import OpenHands from "#/api/open-hands";
import * as GitHubAPI from "#/api/github";
describe("GitHubRepositorySelector", () => {
const onInputChangeMock = vi.fn();
@@ -20,7 +19,7 @@ describe("GitHubRepositorySelector", () => {
);
expect(
screen.getByPlaceholderText("Select a GitHub project"),
screen.getByPlaceholderText("LANDING$SELECT_REPO"),
).toBeInTheDocument();
});
@@ -60,8 +59,8 @@ describe("GitHubRepositorySelector", () => {
];
const searchPublicRepositoriesSpy = vi.spyOn(
GitHubAPI,
"searchPublicRepositories",
OpenHands,
"searchGitHubRepositories",
);
searchPublicRepositoriesSpy.mockResolvedValue(mockSearchedRepos);

View File

@@ -1,10 +1,12 @@
import { screen } from "@testing-library/react";
import { screen, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, expect, it } from "vitest";
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 { MULTI_CONVERSATION_UI } from "#/utils/feature-flags";
import OpenHands from "#/api/open-hands";
import { MOCK_USER_PREFERENCES } from "#/mocks/handlers";
const renderSidebar = () => {
const RouterStub = createRoutesStub([
@@ -43,4 +45,130 @@ describe("Sidebar", () => {
).not.toBeInTheDocument();
},
);
describe("Settings", () => {
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
const saveSettingsSpy = vi.spyOn(OpenHands, "saveSettings");
afterEach(() => {
vi.clearAllMocks();
});
it("should fetch settings data on mount", () => {
renderSidebar();
expect(getSettingsSpy).toHaveBeenCalledOnce();
});
it("should send all settings data when saving AI configuration", async () => {
const user = userEvent.setup();
renderSidebar();
const settingsButton = screen.getByTestId("settings-button");
await user.click(settingsButton);
const settingsModal = screen.getByTestId("ai-config-modal");
const saveButton = within(settingsModal).getByTestId(
"save-settings-button",
);
await user.click(saveButton);
expect(saveSettingsSpy).toHaveBeenCalledWith({
...MOCK_USER_PREFERENCES.settings,
// the actual values are falsey (null or "") but we're checking for undefined
llm_api_key: undefined,
llm_base_url: undefined,
security_analyzer: undefined,
});
});
it("should send all settings data when saving account settings", async () => {
const user = userEvent.setup();
renderSidebar();
const userAvatar = screen.getByTestId("user-avatar");
await user.click(userAvatar);
const menu = screen.getByTestId("account-settings-context-menu");
const accountSettingsButton = within(menu).getByTestId(
"account-settings-button",
);
await user.click(accountSettingsButton);
const accountSettingsModal = screen.getByTestId("account-settings-form");
const saveButton =
within(accountSettingsModal).getByTestId("save-settings");
await user.click(saveButton);
expect(saveSettingsSpy).toHaveBeenCalledWith({
...MOCK_USER_PREFERENCES.settings,
llm_api_key: undefined, // null or undefined
});
});
it("should not reset AI configuration when saving account settings", async () => {
const user = userEvent.setup();
renderSidebar();
const userAvatar = screen.getByTestId("user-avatar");
await user.click(userAvatar);
const menu = screen.getByTestId("account-settings-context-menu");
const accountSettingsButton = within(menu).getByTestId(
"account-settings-button",
);
await user.click(accountSettingsButton);
const accountSettingsModal = screen.getByTestId("account-settings-form");
const languageInput =
within(accountSettingsModal).getByLabelText(/language/i);
await user.click(languageInput);
const norskOption = screen.getByText(/norsk/i);
await user.click(norskOption);
const tokenInput =
within(accountSettingsModal).getByLabelText(/GITHUB\$TOKEN_LABEL/i);
await user.type(tokenInput, "new-token");
const saveButton =
within(accountSettingsModal).getByTestId("save-settings");
await user.click(saveButton);
expect(saveSettingsSpy).toHaveBeenCalledWith({
...MOCK_USER_PREFERENCES.settings,
language: "no",
llm_api_key: undefined, // null or undefined
});
});
it("should not send the api key if its SET", async () => {
const user = userEvent.setup();
renderSidebar();
const settingsButton = screen.getByTestId("settings-button");
await user.click(settingsButton);
const settingsModal = screen.getByTestId("ai-config-modal");
// Click the advanced options switch to show the API key input
const advancedOptionsSwitch = within(settingsModal).getByTestId("advanced-option-switch");
await user.click(advancedOptionsSwitch);
const apiKeyInput = within(settingsModal).getByLabelText(/API\$KEY/i);
await user.type(apiKeyInput, "SET");
const saveButton = within(settingsModal).getByTestId(
"save-settings-button",
);
await user.click(saveButton);
expect(saveSettingsSpy).toHaveBeenCalledWith({
...MOCK_USER_PREFERENCES.settings,
llm_api_key: undefined,
llm_base_url: "",
security_analyzer: undefined,
});
});
});
});

View File

@@ -1,11 +1,10 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import * as router from "react-router";
import { afterEach, describe, expect, it, vi } from "vitest";
// Mock useParams before importing components
vi.mock("react-router", async () => {
const actual = await vi.importActual("react-router");
return {
...actual as object,
...(actual as object),
useParams: () => ({ conversationId: "test-conversation-id" }),
};
});
@@ -14,6 +13,7 @@ import { screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { renderWithProviders } from "test-utils";
import { FeedbackForm } from "#/components/features/feedback/feedback-form";
import { I18nKey } from "#/i18n/declaration";
describe("FeedbackForm", () => {
const user = userEvent.setup();
@@ -28,20 +28,20 @@ describe("FeedbackForm", () => {
<FeedbackForm polarity="positive" onClose={onCloseMock} />,
);
screen.getByLabelText("Email");
screen.getByLabelText("Private");
screen.getByLabelText("Public");
screen.getByLabelText(I18nKey.FEEDBACK$EMAIL_LABEL);
screen.getByLabelText(I18nKey.FEEDBACK$PRIVATE_LABEL);
screen.getByLabelText(I18nKey.FEEDBACK$PUBLIC_LABEL);
screen.getByRole("button", { name: "Submit" });
screen.getByRole("button", { name: "Cancel" });
screen.getByRole("button", { name: I18nKey.FEEDBACK$CONTRIBUTE_LABEL });
screen.getByRole("button", { name: I18nKey.FEEDBACK$CANCEL_LABEL });
});
it("should switch between private and public permissions", async () => {
renderWithProviders(
<FeedbackForm polarity="positive" onClose={onCloseMock} />,
);
const privateRadio = screen.getByLabelText("Private");
const publicRadio = screen.getByLabelText("Public");
const privateRadio = screen.getByLabelText(I18nKey.FEEDBACK$PRIVATE_LABEL);
const publicRadio = screen.getByLabelText(I18nKey.FEEDBACK$PUBLIC_LABEL);
expect(privateRadio).toBeChecked(); // private is the default value
expect(publicRadio).not.toBeChecked();
@@ -59,7 +59,9 @@ describe("FeedbackForm", () => {
renderWithProviders(
<FeedbackForm polarity="positive" onClose={onCloseMock} />,
);
await user.click(screen.getByRole("button", { name: "Cancel" }));
await user.click(
screen.getByRole("button", { name: I18nKey.FEEDBACK$CANCEL_LABEL }),
);
expect(onCloseMock).toHaveBeenCalled();
});

View File

@@ -157,7 +157,7 @@ describe("InteractiveChatBox", () => {
expect(onChange).not.toHaveBeenCalledWith("");
// Submit the message with image
const submitButton = screen.getByRole("button", { name: "Send" });
const submitButton = screen.getByRole("button", { name: "BUTTON$SEND" });
await user.click(submitButton);
// Verify onSubmit was called with the message and image

View File

@@ -0,0 +1,190 @@
import { render, screen } from "@testing-library/react";
import { test, expect, describe, vi } from "vitest";
import { useTranslation } from "react-i18next";
import translations from "../../src/i18n/translation.json";
import { UserAvatar } from "../../src/components/features/sidebar/user-avatar";
vi.mock("@nextui-org/react", () => ({
Tooltip: ({ content, children }: { content: string; children: React.ReactNode }) => (
<div>
{children}
<div>{content}</div>
</div>
),
}));
const supportedLanguages = ['en', 'ja', 'zh-CN', 'zh-TW', 'ko-KR', 'de', 'no', 'it', 'pt', 'es', 'ar', 'fr', 'tr'];
// Helper function to check if a translation exists for all supported languages
function checkTranslationExists(key: string) {
const missingTranslations: string[] = [];
const translationEntry = (translations as Record<string, Record<string, string>>)[key];
if (!translationEntry) {
throw new Error(`Translation key "${key}" does not exist in translation.json`);
}
for (const lang of supportedLanguages) {
if (!translationEntry[lang]) {
missingTranslations.push(lang);
}
}
return missingTranslations;
}
// Helper function to find duplicate translation keys
function findDuplicateKeys(obj: Record<string, any>) {
const seen = new Set<string>();
const duplicates = new Set<string>();
// Only check top-level keys as these are our translation keys
for (const key in obj) {
if (seen.has(key)) {
duplicates.add(key);
} else {
seen.add(key);
}
}
return Array.from(duplicates);
}
vi.mock("react-i18next", () => ({
useTranslation: () => ({
t: (key: string) => {
const translationEntry = (translations as Record<string, Record<string, string>>)[key];
return translationEntry?.ja || key;
},
}),
}));
describe("Landing page translations", () => {
test("should render Japanese translations correctly", () => {
// Mock a simple component that uses the translations
const TestComponent = () => {
const { t } = useTranslation();
return (
<div>
<UserAvatar onClick={() => {}} />
<div data-testid="main-content">
<h1>{t("LANDING$TITLE")}</h1>
<button>{t("VSCODE$OPEN")}</button>
<button>{t("SUGGESTIONS$INCREASE_TEST_COVERAGE")}</button>
<button>{t("SUGGESTIONS$AUTO_MERGE_PRS")}</button>
<button>{t("SUGGESTIONS$FIX_README")}</button>
<button>{t("SUGGESTIONS$CLEAN_DEPENDENCIES")}</button>
</div>
<div data-testid="tabs">
<span>{t("WORKSPACE$TERMINAL_TAB_LABEL")}</span>
<span>{t("WORKSPACE$BROWSER_TAB_LABEL")}</span>
<span>{t("WORKSPACE$JUPYTER_TAB_LABEL")}</span>
<span>{t("WORKSPACE$CODE_EDITOR_TAB_LABEL")}</span>
</div>
<div data-testid="workspace-label">{t("WORKSPACE$TITLE")}</div>
<button data-testid="new-project">{t("PROJECT$NEW_PROJECT")}</button>
<div data-testid="status">
<span>{t("TERMINAL$WAITING_FOR_CLIENT")}</span>
<span>{t("STATUS$CONNECTED")}</span>
<span>{t("STATUS$CONNECTED_TO_SERVER")}</span>
</div>
<div data-testid="time">
<span>{`5 ${t("TIME$MINUTES_AGO")}`}</span>
<span>{`2 ${t("TIME$HOURS_AGO")}`}</span>
<span>{`3 ${t("TIME$DAYS_AGO")}`}</span>
</div>
</div>
);
};
render(<TestComponent />);
// Check main content translations
expect(screen.getByText("開発を始めましょう!")).toBeInTheDocument();
expect(screen.getByText("VS Codeで開く")).toBeInTheDocument();
expect(screen.getByText("テストカバレッジを向上させる")).toBeInTheDocument();
expect(screen.getByText("Dependabot PRを自動マージ")).toBeInTheDocument();
expect(screen.getByText("READMEを改善")).toBeInTheDocument();
expect(screen.getByText("依存関係を整理")).toBeInTheDocument();
// Check user avatar tooltip
const userAvatar = screen.getByTestId("user-avatar");
userAvatar.focus();
expect(screen.getByText("アカウント設定")).toBeInTheDocument();
// Check tab labels
const tabs = screen.getByTestId("tabs");
expect(tabs).toHaveTextContent("ターミナル");
expect(tabs).toHaveTextContent("ブラウザ");
expect(tabs).toHaveTextContent("Jupyter");
expect(tabs).toHaveTextContent("コードエディタ");
// Check workspace label and new project button
expect(screen.getByTestId("workspace-label")).toHaveTextContent("ワークスペース");
expect(screen.getByTestId("new-project")).toHaveTextContent("新規プロジェクト");
// Check status messages
const status = screen.getByTestId("status");
expect(status).toHaveTextContent("クライアントの準備を待機中");
expect(status).toHaveTextContent("接続済み");
expect(status).toHaveTextContent("サーバーに接続済み");
// Check account settings menu
expect(screen.getByText("アカウント設定")).toBeInTheDocument();
// Check time-related translations
const time = screen.getByTestId("time");
expect(time).toHaveTextContent("5 分前");
expect(time).toHaveTextContent("2 時間前");
expect(time).toHaveTextContent("3 日前");
});
test("all translation keys should have translations for all supported languages", () => {
// Test all translation keys used in the component
const translationKeys = [
"LANDING$TITLE",
"VSCODE$OPEN",
"SUGGESTIONS$INCREASE_TEST_COVERAGE",
"SUGGESTIONS$AUTO_MERGE_PRS",
"SUGGESTIONS$FIX_README",
"SUGGESTIONS$CLEAN_DEPENDENCIES",
"WORKSPACE$TERMINAL_TAB_LABEL",
"WORKSPACE$BROWSER_TAB_LABEL",
"WORKSPACE$JUPYTER_TAB_LABEL",
"WORKSPACE$CODE_EDITOR_TAB_LABEL",
"WORKSPACE$TITLE",
"PROJECT$NEW_PROJECT",
"TERMINAL$WAITING_FOR_CLIENT",
"STATUS$CONNECTED",
"STATUS$CONNECTED_TO_SERVER",
"TIME$MINUTES_AGO",
"TIME$HOURS_AGO",
"TIME$DAYS_AGO"
];
// Check all keys and collect missing translations
const missingTranslationsMap = new Map<string, string[]>();
translationKeys.forEach(key => {
const missing = checkTranslationExists(key);
if (missing.length > 0) {
missingTranslationsMap.set(key, missing);
}
});
// If any translations are missing, throw an error with all missing translations
if (missingTranslationsMap.size > 0) {
const errorMessage = Array.from(missingTranslationsMap.entries())
.map(([key, langs]) => `\n- "${key}" is missing translations for: ${langs.join(', ')}`)
.join('');
throw new Error(`Missing translations:${errorMessage}`);
}
});
test("translation file should not have duplicate keys", () => {
const duplicates = findDuplicateKeys(translations);
if (duplicates.length > 0) {
throw new Error(`Found duplicate translation keys: ${duplicates.join(', ')}`);
}
});
});

View File

@@ -1,7 +1,23 @@
import { describe, it, expect } from "vitest";
import { describe, it, expect, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { ModelSelector } from "#/components/shared/modals/settings/model-selector";
import { I18nKey } from "#/i18n/declaration";
// Mock react-i18next
vi.mock("react-i18next", () => ({
useTranslation: () => ({
t: (key: string) => {
const translations: { [key: string]: string } = {
LLM$PROVIDER: "LLM Provider",
LLM$MODEL: "LLM Model",
LLM$SELECT_PROVIDER_PLACEHOLDER: "Select a provider",
LLM$SELECT_MODEL_PLACEHOLDER: "Select a model",
};
return translations[key] || key;
},
}),
}));
describe("ModelSelector", () => {
const models = {

View File

@@ -1,13 +1,22 @@
import { screen, fireEvent } from "@testing-library/react";
import { describe, it, expect, vi } from "vitest";
import { describe, it, expect, vi, afterEach } from "vitest";
import { renderWithProviders } from "test-utils";
import { createRoutesStub } from "react-router";
import userEvent from "@testing-library/user-event";
import { DEFAULT_SETTINGS } from "#/services/settings";
import { SettingsForm } from "#/components/shared/modals/settings/settings-form";
import OpenHands from "#/api/open-hands";
describe("SettingsForm", () => {
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
const saveSettingsSpy = vi.spyOn(OpenHands, "saveSettings");
const onCloseMock = vi.fn();
afterEach(() => {
vi.clearAllMocks();
});
getConfigSpy.mockResolvedValue({
APP_MODE: "saas",
GITHUB_CLIENT_ID: "123",
@@ -19,10 +28,10 @@ describe("SettingsForm", () => {
Component: () => (
<SettingsForm
settings={DEFAULT_SETTINGS}
models={[]}
agents={[]}
securityAnalyzers={[]}
onClose={() => {}}
models={["anthropic/claude-3-5-sonnet-20241022", "model2"]}
agents={["CodeActAgent", "agent2"]}
securityAnalyzers={["analyzer1", "analyzer2"]}
onClose={onCloseMock}
/>
),
path: "/",
@@ -35,11 +44,33 @@ describe("SettingsForm", () => {
});
it("should show runtime size selector when advanced options are enabled", async () => {
const user = userEvent.setup();
renderWithProviders(<RouterStub />);
const advancedSwitch = screen.getByRole("switch", {
name: "SETTINGS_FORM$ADVANCED_OPTIONS_LABEL",
});
fireEvent.click(advancedSwitch);
await screen.findByText("SETTINGS_FORM$RUNTIME_SIZE_LABEL");
const toggleAdvancedMode = screen.getByTestId("advanced-option-switch");
await user.click(toggleAdvancedMode);
await screen.findByTestId("runtime-size");
});
it("should not submit the form if required fields are empty", async () => {
const user = userEvent.setup();
renderWithProviders(<RouterStub />);
expect(screen.queryByTestId("custom-model-input")).not.toBeInTheDocument();
const toggleAdvancedMode = screen.getByTestId("advanced-option-switch");
await user.click(toggleAdvancedMode);
const customModelInput = screen.getByTestId("custom-model-input");
expect(customModelInput).toBeInTheDocument();
await user.clear(customModelInput);
const saveButton = screen.getByTestId("save-settings-button");
await user.click(saveButton);
expect(saveSettingsSpy).not.toHaveBeenCalled();
expect(onCloseMock).not.toHaveBeenCalled();
});
});

View File

@@ -2,6 +2,20 @@ import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, describe, expect, it, vi } from "vitest";
import { SuggestionItem } from "#/components/features/suggestions/suggestion-item";
import { I18nKey } from "#/i18n/declaration";
vi.mock("react-i18next", () => ({
useTranslation: () => ({
t: (key: string) => {
const translations: Record<string, string> = {
"SUGGESTIONS$TODO_APP": "ToDoリストアプリを開発する",
"LANDING$BUILD_APP_BUTTON": "プルリクエストを表示するアプリを開発する",
"SUGGESTIONS$HACKER_NEWS": "Hacker Newsのトップ記事を表示するbashスクリプトを作成する",
};
return translations[key] || key;
},
}),
}));
describe("SuggestionItem", () => {
const suggestionItem = { label: "suggestion1", value: "a long text value" };
@@ -18,6 +32,19 @@ describe("SuggestionItem", () => {
expect(screen.getByText(/suggestion1/i)).toBeInTheDocument();
});
it("should render a translated suggestion when using I18nKey", async () => {
const translatedSuggestion = {
label: I18nKey.SUGGESTIONS$TODO_APP,
value: "todo app value",
};
const { container } = render(<SuggestionItem suggestion={translatedSuggestion} onClick={onClick} />);
console.log('Rendered HTML:', container.innerHTML);
expect(screen.getByText("ToDoリストアプリを開発する")).toBeInTheDocument();
});
it("should call onClick when clicking a suggestion", async () => {
const user = userEvent.setup();
render(<SuggestionItem suggestion={suggestionItem} onClick={onClick} />);

View File

@@ -14,7 +14,7 @@ describe("UserAvatar", () => {
render(<UserAvatar onClick={onClickMock} />);
expect(screen.getByTestId("user-avatar")).toBeInTheDocument();
expect(
screen.getByLabelText("user avatar placeholder"),
screen.getByLabelText("USER$AVATAR_PLACEHOLDER"),
).toBeInTheDocument();
});
@@ -38,7 +38,7 @@ describe("UserAvatar", () => {
expect(screen.getByAltText("user avatar")).toBeInTheDocument();
expect(
screen.queryByLabelText("user avatar placeholder"),
screen.queryByLabelText("USER$AVATAR_PLACEHOLDER"),
).not.toBeInTheDocument();
});
@@ -46,13 +46,13 @@ describe("UserAvatar", () => {
const { rerender } = render(<UserAvatar onClick={onClickMock} />);
expect(screen.queryByTestId("loading-spinner")).not.toBeInTheDocument();
expect(
screen.getByLabelText("user avatar placeholder"),
screen.getByLabelText("USER$AVATAR_PLACEHOLDER"),
).toBeInTheDocument();
rerender(<UserAvatar onClick={onClickMock} isLoading />);
expect(screen.getByTestId("loading-spinner")).toBeInTheDocument();
expect(
screen.queryByLabelText("user avatar placeholder"),
screen.queryByLabelText("USER$AVATAR_PLACEHOLDER"),
).not.toBeInTheDocument();
rerender(

View File

@@ -27,4 +27,17 @@ describe("Propagate error message", () => {
type: 'error'
});
});
it("should display error including translation id when present", () => {
const message = "We have a problem!"
const addErrorMessageSpy = vi.spyOn(ChatSlice, "addErrorMessage")
updateStatusWhenErrorMessagePresent({message, data: {msg_id: '..id..'}})
expect(addErrorMessageSpy).toHaveBeenCalledWith({
message,
id: '..id..',
status_update: true,
type: 'error'
});
});
});

View File

@@ -0,0 +1,76 @@
import { describe, expect, it } from 'vitest';
import fs from 'fs';
import path from 'path';
describe('translation.json', () => {
it('should not have duplicate translation keys', () => {
// Read the translation.json file
const translationPath = path.join(__dirname, '../../src/i18n/translation.json');
const translationContent = fs.readFileSync(translationPath, 'utf-8');
// First, let's check for exact string matches of key definitions
const keyRegex = /"([^"]+)": {/g;
const matches = translationContent.matchAll(keyRegex);
const keyOccurrences = new Map<string, number>();
const duplicateKeys: string[] = [];
for (const match of matches) {
const key = match[1];
const count = (keyOccurrences.get(key) || 0) + 1;
keyOccurrences.set(key, count);
if (count > 1) {
duplicateKeys.push(key);
}
}
// Remove duplicates from duplicateKeys array
const uniqueDuplicates = [...new Set(duplicateKeys)];
// If there are duplicates, create a helpful error message
if (uniqueDuplicates.length > 0) {
const errorMessage = `Found duplicate translation keys:\n${uniqueDuplicates
.map((key) => ` - "${key}" appears ${keyOccurrences.get(key)} times`)
.join('\n')}`;
throw new Error(errorMessage);
}
// Expect no duplicates (this will pass if we reach here)
expect(uniqueDuplicates).toHaveLength(0);
});
it('should have consistent translations for each key', () => {
// Read the translation.json file
const translationPath = path.join(__dirname, '../../src/i18n/translation.json');
const translationContent = fs.readFileSync(translationPath, 'utf-8');
const translations = JSON.parse(translationContent);
// Create a map to store English translations for each key
const englishTranslations = new Map<string, string>();
const inconsistentKeys: string[] = [];
// Check each key's English translation
Object.entries(translations).forEach(([key, value]: [string, any]) => {
if (typeof value === 'object' && value.en !== undefined) {
const currentEn = value.en.toLowerCase();
const existingEn = englishTranslations.get(key)?.toLowerCase();
if (existingEn !== undefined && existingEn !== currentEn) {
inconsistentKeys.push(key);
} else {
englishTranslations.set(key, value.en);
}
}
});
// If there are inconsistencies, create a helpful error message
if (inconsistentKeys.length > 0) {
const errorMessage = `Found inconsistent translations for keys:\n${inconsistentKeys
.map((key) => ` - "${key}" has multiple different English translations`)
.join('\n')}`;
throw new Error(errorMessage);
}
// Expect no inconsistencies
expect(inconsistentKeys).toHaveLength(0);
});
});

View File

@@ -0,0 +1,20 @@
import { screen } from '@testing-library/react';
import { describe, expect, it } from 'vitest';
import i18n from '../../src/i18n';
import { AccountSettingsContextMenu } from '../../src/components/features/context-menu/account-settings-context-menu';
import { renderWithProviders } from '../../test-utils';
describe('Translations', () => {
it('should render translated text', () => {
i18n.changeLanguage('en');
renderWithProviders(
<AccountSettingsContextMenu
onClickAccountSettings={() => {}}
onLogout={() => {}}
onClose={() => {}}
isLoggedIn={true}
/>
);
expect(screen.getByTestId('account-settings-context-menu')).toBeInTheDocument();
});
});

View File

@@ -1,5 +1,4 @@
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import * as router from "react-router";
import { createRoutesStub } from "react-router";
import { screen, waitFor, within } from "@testing-library/react";
import { renderWithProviders } from "test-utils";

View File

@@ -0,0 +1,40 @@
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";
// Mock react-i18next
vi.mock("react-i18next", () => ({
useTranslation: () => ({
t: (key: string) => key,
}),
}));
describe("Check for hardcoded English strings", () => {
test("InteractiveChatBox should not have hardcoded English strings", () => {
const { container } = render(
<InteractiveChatBox
onSubmit={() => {}}
onStop={() => {}}
/>
);
// Get all text content
const text = container.textContent;
// List of English strings that should be translated
const hardcodedStrings = [
"What do you want to build?",
];
// Check each string
hardcodedStrings.forEach(str => {
expect(text).not.toContain(str);
});
});
test("ChatInput should use translation key for placeholder", () => {
render(<ChatInput onSubmit={() => {}} />);
screen.getByPlaceholderText("SUGGESTIONS$WHAT_TO_BUILD");
});
});

View File

@@ -0,0 +1,29 @@
import { ReactNode } from "react";
import { I18nextProvider } from "react-i18next";
const mockI18n = {
language: "ja",
t: (key: string) => {
const translations: Record<string, string> = {
"SUGGESTIONS$TODO_APP": "ToDoリストアプリを開発する",
"LANDING$BUILD_APP_BUTTON": "プルリクエストを表示するアプリを開発する",
"SUGGESTIONS$HACKER_NEWS": "Hacker Newsのトップ記事を表示するbashスクリプトを作成する",
"LANDING$TITLE": "一緒に開発を始めましょう!",
"OPEN_IN_VSCODE": "VS Codeで開く",
"INCREASE_TEST_COVERAGE": "テストカバレッジを向上",
"AUTO_MERGE_PRS": "PRを自動マージ",
"FIX_README": "READMEを修正",
"CLEAN_DEPENDENCIES": "依存関係を整理"
};
return translations[key] || key;
},
exists: () => true,
changeLanguage: () => new Promise(() => {}),
use: () => mockI18n,
};
export function I18nTestProvider({ children }: { children: ReactNode }) {
return (
<I18nextProvider i18n={mockI18n as any}>{children}</I18nextProvider>
);
}

View File

@@ -0,0 +1,20 @@
import { vi } from "vitest";
import OpenHands from "#/api/open-hands";
export const setupTestConfig = () => {
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
getConfigSpy.mockResolvedValue({
APP_MODE: "oss",
GITHUB_CLIENT_ID: "test-id",
POSTHOG_CLIENT_KEY: "test-key",
});
};
export const setupSaasTestConfig = () => {
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
getConfigSpy.mockResolvedValue({
APP_MODE: "saas",
GITHUB_CLIENT_ID: "test-id",
POSTHOG_CLIENT_KEY: "test-key",
});
};

View File

@@ -1,12 +1,12 @@
{
"name": "openhands-frontend",
"version": "0.19.0",
"version": "0.20.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "openhands-frontend",
"version": "0.19.0",
"version": "0.20.0",
"dependencies": {
"@monaco-editor/react": "^4.7.0-rc.0",
"@nextui-org/react": "^2.6.11",
@@ -24,7 +24,7 @@
"i18next": "^24.2.1",
"i18next-browser-languagedetector": "^8.0.2",
"i18next-http-backend": "^3.0.1",
"isbot": "^5.1.20",
"isbot": "^5.1.21",
"jose": "^5.9.4",
"monaco-editor": "^0.52.2",
"posthog-js": "^1.205.0",
@@ -38,7 +38,7 @@
"react-redux": "^9.2.0",
"react-router": "^7.1.1",
"react-syntax-highlighter": "^15.6.1",
"react-textarea-autosize": "^8.5.4",
"react-textarea-autosize": "^8.5.7",
"remark-gfm": "^4.0.0",
"sirv-cli": "^3.0.0",
"socket.io-client": "^4.8.1",
@@ -57,7 +57,7 @@
"@testing-library/react": "^16.1.0",
"@testing-library/user-event": "^14.5.2",
"@types/node": "^22.10.5",
"@types/react": "^19.0.3",
"@types/react": "^19.0.4",
"@types/react-dom": "^19.0.2",
"@types/react-highlight": "^0.12.8",
"@types/react-syntax-highlighter": "^15.5.13",
@@ -70,20 +70,20 @@
"eslint": "^8.57.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-airbnb-typescript": "^18.0.0",
"eslint-config-prettier": "^9.1.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-react": "^7.37.3",
"eslint-plugin-react": "^7.37.4",
"eslint-plugin-react-hooks": "^4.6.2",
"husky": "^9.1.6",
"jsdom": "^25.0.1",
"jsdom": "^26.0.0",
"lint-staged": "^15.3.0",
"msw": "^2.6.6",
"postcss": "^8.4.47",
"prettier": "^3.4.2",
"tailwindcss": "^3.4.17",
"typescript": "^5.7.2",
"typescript": "^5.7.3",
"vite-plugin-svgr": "^4.2.0",
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^1.6.0"
@@ -124,6 +124,28 @@
"node": ">=6.0.0"
}
},
"node_modules/@asamuzakjp/css-color": {
"version": "2.8.2",
"resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-2.8.2.tgz",
"integrity": "sha512-RtWv9jFN2/bLExuZgFFZ0I3pWWeezAHGgrmjqGGWclATl1aDe3yhCUaI0Ilkp6OCk9zX7+FjvDasEX8Q9Rxc5w==",
"dev": true,
"dependencies": {
"@csstools/css-calc": "^2.1.1",
"@csstools/css-color-parser": "^3.0.7",
"@csstools/css-parser-algorithms": "^3.0.4",
"@csstools/css-tokenizer": "^3.0.3",
"lru-cache": "^11.0.2"
}
},
"node_modules/@asamuzakjp/css-color/node_modules/lru-cache": {
"version": "11.0.2",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.2.tgz",
"integrity": "sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA==",
"dev": true,
"engines": {
"node": "20 || >=22"
}
},
"node_modules/@babel/code-frame": {
"version": "7.26.2",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz",
@@ -691,6 +713,116 @@
"node": ">= 4.0.0"
}
},
"node_modules/@csstools/color-helpers": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.0.1.tgz",
"integrity": "sha512-MKtmkA0BX87PKaO1NFRTFH+UnkgnmySQOvNxJubsadusqPEC2aJ9MOQiMceZJJ6oitUl/i0L6u0M1IrmAOmgBA==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"engines": {
"node": ">=18"
}
},
"node_modules/@csstools/css-calc": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.1.tgz",
"integrity": "sha512-rL7kaUnTkL9K+Cvo2pnCieqNpTKgQzy5f+N+5Iuko9HAoasP+xgprVh7KN/MaJVvVL1l0EzQq2MoqBHKSrDrag==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@csstools/css-parser-algorithms": "^3.0.4",
"@csstools/css-tokenizer": "^3.0.3"
}
},
"node_modules/@csstools/css-color-parser": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.0.7.tgz",
"integrity": "sha512-nkMp2mTICw32uE5NN+EsJ4f5N+IGFeCFu4bGpiKgb2Pq/7J/MpyLBeQ5ry4KKtRFZaYs6sTmcMYrSRIyj5DFKA==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"dependencies": {
"@csstools/color-helpers": "^5.0.1",
"@csstools/css-calc": "^2.1.1"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@csstools/css-parser-algorithms": "^3.0.4",
"@csstools/css-tokenizer": "^3.0.3"
}
},
"node_modules/@csstools/css-parser-algorithms": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.4.tgz",
"integrity": "sha512-Up7rBoV77rv29d3uKHUIVubz1BTcgyUK72IvCQAbfbMv584xHcGKCKbWh7i8hPrRJ7qU4Y8IO3IY9m+iTB7P3A==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@csstools/css-tokenizer": "^3.0.3"
}
},
"node_modules/@csstools/css-tokenizer": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.3.tgz",
"integrity": "sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
@@ -5447,9 +5579,9 @@
}
},
"node_modules/@types/react": {
"version": "19.0.3",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.3.tgz",
"integrity": "sha512-UavfHguIjnnuq9O67uXfgy/h3SRJbidAYvNjLceB+2RIKVRBzVsh0QO+Pw6BCSQqFS9xwzKfwstXx0m6AbAREA==",
"version": "19.0.4",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.4.tgz",
"integrity": "sha512-3O4QisJDYr1uTUMZHA2YswiQZRq+Pd8D+GdVFYikTutYsTz+QZgWkAPnP7rx9txoI6EXKcPiluMqWPFV3tT9Wg==",
"dependencies": {
"csstype": "^3.0.2"
}
@@ -7448,13 +7580,13 @@
}
},
"node_modules/cssstyle": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.1.0.tgz",
"integrity": "sha512-h66W1URKpBS5YMI/V8PyXvTMFT8SupJ1IzoIV8IeBC/ji8WVmrO8dGlTi+2dh6whmdk6BiKJLD/ZBkhWbcg6nA==",
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.2.1.tgz",
"integrity": "sha512-9+vem03dMXG7gDmZ62uqmRiMRNtinIZ9ZyuF6BdxzfOD+FdN5hretzynkn0ReS2DO2GSw76RWHs0UmJPI2zUjw==",
"dev": true,
"license": "MIT",
"dependencies": {
"rrweb-cssom": "^0.7.1"
"@asamuzakjp/css-color": "^2.8.2",
"rrweb-cssom": "^0.8.0"
},
"engines": {
"node": ">=18"
@@ -8316,13 +8448,12 @@
}
},
"node_modules/eslint-config-prettier": {
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz",
"integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==",
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.0.1.tgz",
"integrity": "sha512-lZBts941cyJyeaooiKxAtzoPHTN+GbQTJFAIdQbRhA4/8whaAraEh47Whw/ZFfrjNSnlAxqfm9i0XVAEkULjCw==",
"dev": true,
"license": "MIT",
"bin": {
"eslint-config-prettier": "bin/cli.js"
"eslint-config-prettier": "build/bin/cli.js"
},
"peerDependencies": {
"eslint": ">=7.0.0"
@@ -8565,11 +8696,10 @@
}
},
"node_modules/eslint-plugin-react": {
"version": "7.37.3",
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.3.tgz",
"integrity": "sha512-DomWuTQPFYZwF/7c9W2fkKkStqZmBd3uugfqBYLdkZ3Hii23WzZuOLUskGxB8qkSKqftxEeGL1TB2kMhrce0jA==",
"version": "7.37.4",
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.4.tgz",
"integrity": "sha512-BGP0jRmfYyvOyvMoRX/uoUeW+GqNj9y16bPQzqAHf3AYII/tDs+jMN0dBVkl88/OZwNGwrVFxE7riHsXVfy/LQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"array-includes": "^3.1.8",
"array.prototype.findlast": "^1.2.5",
@@ -10670,9 +10800,9 @@
"license": "MIT"
},
"node_modules/isbot": {
"version": "5.1.20",
"resolved": "https://registry.npmjs.org/isbot/-/isbot-5.1.20.tgz",
"integrity": "sha512-cW535S5c05UBfx8bTAZHACjEXyY/p10bvAx5YeqoLEFoGC1HQ6A5n3ScpZRYd1zSwwNF8yYkEOq2F7WjFhX2ig==",
"version": "5.1.21",
"resolved": "https://registry.npmjs.org/isbot/-/isbot-5.1.21.tgz",
"integrity": "sha512-0q3naRVpENL0ReKHeNcwn/G7BDynp0DqZUckKyFtM9+hmpnPqgm8+8wbjiVZ0XNhq1wPQV28/Pb8Snh5adeUHA==",
"engines": {
"node": ">=18"
}
@@ -10808,23 +10938,22 @@
}
},
"node_modules/jsdom": {
"version": "25.0.1",
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-25.0.1.tgz",
"integrity": "sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==",
"version": "26.0.0",
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.0.0.tgz",
"integrity": "sha512-BZYDGVAIriBWTpIxYzrXjv3E/4u8+/pSG5bQdIYCbNCGOvsPkDQfTVLAIXAf9ETdCpduCVTkDe2NNZ8NIwUVzw==",
"dev": true,
"license": "MIT",
"dependencies": {
"cssstyle": "^4.1.0",
"cssstyle": "^4.2.1",
"data-urls": "^5.0.0",
"decimal.js": "^10.4.3",
"form-data": "^4.0.0",
"form-data": "^4.0.1",
"html-encoding-sniffer": "^4.0.0",
"http-proxy-agent": "^7.0.2",
"https-proxy-agent": "^7.0.5",
"https-proxy-agent": "^7.0.6",
"is-potential-custom-element-name": "^1.0.1",
"nwsapi": "^2.2.12",
"parse5": "^7.1.2",
"rrweb-cssom": "^0.7.1",
"nwsapi": "^2.2.16",
"parse5": "^7.2.1",
"rrweb-cssom": "^0.8.0",
"saxes": "^6.0.0",
"symbol-tree": "^3.2.4",
"tough-cookie": "^5.0.0",
@@ -10832,7 +10961,7 @@
"webidl-conversions": "^7.0.0",
"whatwg-encoding": "^3.1.1",
"whatwg-mimetype": "^4.0.0",
"whatwg-url": "^14.0.0",
"whatwg-url": "^14.1.0",
"ws": "^8.18.0",
"xml-name-validator": "^5.0.0"
},
@@ -10840,7 +10969,7 @@
"node": ">=18"
},
"peerDependencies": {
"canvas": "^2.11.2"
"canvas": "^3.0.0"
},
"peerDependenciesMeta": {
"canvas": {
@@ -14103,10 +14232,9 @@
}
},
"node_modules/react-textarea-autosize": {
"version": "8.5.6",
"resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-8.5.6.tgz",
"integrity": "sha512-aT3ioKXMa8f6zHYGebhbdMD2L00tKeRX1zuVuDx9YQK/JLLRSaSxq3ugECEmUB9z2kvk6bFSIoRHLkkUv0RJiw==",
"license": "MIT",
"version": "8.5.7",
"resolved": "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-8.5.7.tgz",
"integrity": "sha512-2MqJ3p0Jh69yt9ktFIaZmORHXw4c4bxSIhCeWiFwmJ9EYKgLmuNII3e9c9b2UO+ijl4StnpZdqpxNIhTdHvqtQ==",
"dependencies": {
"@babel/runtime": "^7.20.13",
"use-composed-ref": "^1.3.0",
@@ -14639,11 +14767,10 @@
}
},
"node_modules/rrweb-cssom": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz",
"integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==",
"dev": true,
"license": "MIT"
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz",
"integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==",
"dev": true
},
"node_modules/run-parallel": {
"version": "1.2.0",
@@ -16313,11 +16440,10 @@
}
},
"node_modules/typescript": {
"version": "5.7.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz",
"integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==",
"version": "5.7.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz",
"integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==",
"devOptional": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"

View File

@@ -1,6 +1,6 @@
{
"name": "openhands-frontend",
"version": "0.19.0",
"version": "0.20.0",
"private": true,
"type": "module",
"engines": {
@@ -23,7 +23,7 @@
"i18next": "^24.2.1",
"i18next-browser-languagedetector": "^8.0.2",
"i18next-http-backend": "^3.0.1",
"isbot": "^5.1.20",
"isbot": "^5.1.21",
"jose": "^5.9.4",
"monaco-editor": "^0.52.2",
"posthog-js": "^1.205.0",
@@ -37,7 +37,7 @@
"react-redux": "^9.2.0",
"react-router": "^7.1.1",
"react-syntax-highlighter": "^15.6.1",
"react-textarea-autosize": "^8.5.4",
"react-textarea-autosize": "^8.5.7",
"remark-gfm": "^4.0.0",
"sirv-cli": "^3.0.0",
"socket.io-client": "^4.8.1",
@@ -84,7 +84,7 @@
"@testing-library/react": "^16.1.0",
"@testing-library/user-event": "^14.5.2",
"@types/node": "^22.10.5",
"@types/react": "^19.0.3",
"@types/react": "^19.0.4",
"@types/react-dom": "^19.0.2",
"@types/react-highlight": "^0.12.8",
"@types/react-syntax-highlighter": "^15.5.13",
@@ -97,20 +97,20 @@
"eslint": "^8.57.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-airbnb-typescript": "^18.0.0",
"eslint-config-prettier": "^9.1.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-react": "^7.37.3",
"eslint-plugin-react": "^7.37.4",
"eslint-plugin-react-hooks": "^4.6.2",
"husky": "^9.1.6",
"jsdom": "^25.0.1",
"jsdom": "^26.0.0",
"lint-staged": "^15.3.0",
"msw": "^2.6.6",
"postcss": "^8.4.47",
"prettier": "^3.4.2",
"tailwindcss": "^3.4.17",
"typescript": "^5.7.2",
"typescript": "^5.7.3",
"vite-plugin-svgr": "^4.2.0",
"vite-tsconfig-paths": "^5.1.4",
"vitest": "^1.6.0"

View File

@@ -41,6 +41,7 @@ export const isGitHubErrorReponse = <T extends object | Array<unknown>>(
// Axios interceptor to handle token refresh
const setupAxiosInterceptors = (
appMode: string,
refreshToken: () => Promise<boolean>,
logout: () => void,
) => {
@@ -74,18 +75,21 @@ const setupAxiosInterceptors = (
!originalRequest._retry // Prevent infinite retry loops
) {
originalRequest._retry = true;
try {
const refreshed = await refreshToken();
if (refreshed) {
return await github(originalRequest);
}
logout();
return await Promise.reject(new Error("Failed to refresh token"));
} catch (refreshError) {
// If token refresh fails, evict the user
logout();
return Promise.reject(refreshError);
if (appMode === "saas") {
try {
const refreshed = await refreshToken();
if (refreshed) {
return await github(originalRequest);
}
logout();
return await Promise.reject(new Error("Failed to refresh token"));
} catch (refreshError) {
// If token refresh fails, evict the user
logout();
return Promise.reject(refreshError);
}
}
}

View File

@@ -1,19 +1,6 @@
import { extractNextPageFromLink } from "#/utils/extract-next-page-from-link";
import { github } from "./github-axios-instance";
import { openHands } from "./open-hands-axios";
/**
* Given the user, retrieves app installations IDs for OpenHands Github App
* Uses user access token for Github App
*/
export const retrieveGitHubAppInstallations = async (): Promise<number[]> => {
const response = await github.get<GithubAppInstallation>(
"/user/installations",
);
return response.data.installations.map((installation) => installation.id);
};
/**
* Retrieves repositories where OpenHands Github App has been installed
* @param installationIndex Pagination cursor position for app installation IDs
@@ -82,72 +69,3 @@ export const retrieveGitHubUserRepositories = async (
return { data: response.data, nextPage };
};
/**
* Given a GitHub token, retrieves the authenticated user
* @returns The authenticated user or an error response
*/
export const retrieveGitHubUser = async () => {
const response = await github.get<GitHubUser>("/user");
const { data } = response;
const user: GitHubUser = {
id: data.id,
login: data.login,
avatar_url: data.avatar_url,
company: data.company,
name: data.name,
email: data.email,
};
return user;
};
export const searchPublicRepositories = async (
query: string,
per_page = 5,
sort: "" | "updated" | "stars" | "forks" = "stars",
order: "desc" | "asc" = "desc",
): Promise<GitHubRepository[]> => {
const response = await github.get<{ items: GitHubRepository[] }>(
"/search/repositories",
{
params: {
q: query,
per_page,
sort,
order,
},
},
);
return response.data.items;
};
export const retrieveLatestGitHubCommit = async (
repository: string,
): Promise<GitHubCommit | null> => {
try {
const response = await github.get<GitHubCommit[]>(
`/repos/${repository}/commits`,
{
params: {
per_page: 1,
},
},
);
return response.data[0] || null;
} catch (error) {
if (!error || typeof error !== "object") {
throw new Error("Unknown error occurred");
}
const axiosError = error as { response?: { status: number } };
if (axiosError.response?.status === 409) {
// Repository is empty, no commits yet
return null;
}
throw new Error(
error instanceof Error ? error.message : "Unknown error occurred",
);
}
};

View File

@@ -315,6 +315,45 @@ class OpenHands {
const data = await openHands.post("/api/settings", settings);
return data.status === 200;
}
static async getGitHubUser(): Promise<GitHubUser> {
const response = await openHands.get<GitHubUser>("/api/github/user");
const { data } = response;
const user: GitHubUser = {
id: data.id,
login: data.login,
avatar_url: data.avatar_url,
company: data.company,
name: data.name,
email: data.email,
};
return user;
}
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<GitHubRepository[]> {
const response = await openHands.get<{ items: GitHubRepository[] }>(
"/api/github/search/repositories",
{
params: {
query,
per_page,
},
},
);
return response.data.items;
}
}
export default OpenHands;

View File

@@ -8,7 +8,7 @@ export function EmptyBrowserMessage() {
return (
<div className="flex flex-col items-center h-full justify-center">
<IoIosGlobe size={100} />
{t(I18nKey.BROWSER$EMPTY_MESSAGE)}
{t(I18nKey.BROWSER$NO_PAGE_LOADED)}
</div>
);
}

View File

@@ -1,5 +1,7 @@
import React from "react";
import TextareaAutosize from "react-textarea-autosize";
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
import { cn } from "#/utils/utils";
import { SubmitButton } from "#/components/shared/buttons/submit-button";
import { StopButton } from "#/components/shared/buttons/stop-button";
@@ -8,7 +10,6 @@ interface ChatInputProps {
name?: string;
button?: "submit" | "stop";
disabled?: boolean;
placeholder?: string;
showButton?: boolean;
value?: string;
maxRows?: number;
@@ -26,7 +27,6 @@ export function ChatInput({
name,
button = "submit",
disabled,
placeholder,
showButton = true,
value,
maxRows = 4,
@@ -39,6 +39,7 @@ export function ChatInput({
className,
buttonClassName,
}: ChatInputProps) {
const { t } = useTranslation();
const textareaRef = React.useRef<HTMLTextAreaElement>(null);
const [isDraggingOver, setIsDraggingOver] = React.useState(false);
@@ -117,7 +118,7 @@ export function ChatInput({
<TextareaAutosize
ref={textareaRef}
name={name}
placeholder={placeholder}
placeholder={t(I18nKey.SUGGESTIONS$WHAT_TO_BUILD)}
onKeyDown={handleKeyPress}
onChange={handleChange}
onFocus={onFocus}

View File

@@ -1,4 +1,6 @@
import { useTranslation } from "react-i18next";
import { Suggestions } from "#/components/features/suggestions/suggestions";
import { I18nKey } from "#/i18n/declaration";
import BuildIt from "#/icons/build-it.svg?react";
import { SUGGESTIONS } from "#/utils/suggestions";
@@ -7,12 +9,14 @@ interface ChatSuggestionsProps {
}
export function ChatSuggestions({ onSuggestionsClick }: ChatSuggestionsProps) {
const { t } = useTranslation();
return (
<div className="flex flex-col gap-6 h-full px-4 items-center justify-center">
<div className="flex flex-col items-center p-4 bg-neutral-700 rounded-xl w-full">
<BuildIt width={45} height={54} />
<span className="font-semibold text-[20px] leading-6 -tracking-[0.01em] gap-1">
Let&apos;s start building!
{t(I18nKey.LANDING$TITLE)}
</span>
</div>
<Suggestions

View File

@@ -68,7 +68,6 @@ export function InteractiveChatBox({
<ChatInput
disabled={isDisabled}
button={mode}
placeholder="What do you want to build?"
onChange={onChange}
onSubmit={handleSubmit}
onStop={onStop}

View File

@@ -27,7 +27,10 @@ export function AccountSettingsContextMenu({
ref={ref}
className="absolute left-full -top-1 z-10"
>
<ContextMenuListItem onClick={onClickAccountSettings}>
<ContextMenuListItem
testId="account-settings-button"
onClick={onClickAccountSettings}
>
{t(I18nKey.ACCOUNT_SETTINGS$SETTINGS)}
</ContextMenuListItem>
<ContextMenuSeparator />

View File

@@ -1,4 +1,6 @@
import { useSelector } from "react-redux";
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
import PauseIcon from "#/assets/pause";
import PlayIcon from "#/assets/play";
import { generateAgentStateChangeEvent } from "#/services/agent-state-service";
@@ -9,6 +11,7 @@ import { IGNORE_TASK_STATE_MAP } from "#/ignore-task-state-map.constant";
import { ActionButton } from "#/components/shared/buttons/action-button";
export function AgentControlBar() {
const { t } = useTranslation();
const { send } = useWsClient();
const { curAgentState } = useSelector((state: RootState) => state.agent);
@@ -27,8 +30,8 @@ export function AgentControlBar() {
}
content={
curAgentState === AgentState.PAUSED
? "Resume the agent task"
: "Pause the current task"
? t(I18nKey.AGENT$RESUME_TASK)
: t(I18nKey.AGENT$PAUSE_TASK)
}
action={
curAgentState === AgentState.PAUSED

View File

@@ -1,39 +1,30 @@
import { useParams } from "react-router";
import React from "react";
import { useSelector } from "react-redux";
import posthog from "posthog-js";
import { AgentControlBar } from "./agent-control-bar";
import { AgentStatusBar } from "./agent-status-bar";
import { ProjectMenuCard } from "../project-menu/ProjectMenuCard";
import { useAuth } from "#/context/auth-context";
import { RootState } from "#/store";
import { SecurityLock } from "./security-lock";
import { useUserConversation } from "#/hooks/query/use-user-conversation";
import { ConversationCard } from "../conversation-panel/conversation-card";
import { DownloadModal } from "#/components/shared/download-modal";
interface ControlsProps {
setSecurityOpen: (isOpen: boolean) => void;
showSecurityLock: boolean;
lastCommitData: GitHubCommit | null;
}
export function Controls({
setSecurityOpen,
showSecurityLock,
lastCommitData,
}: ControlsProps) {
const { gitHubToken } = useAuth();
const { selectedRepository } = useSelector(
(state: RootState) => state.initialQuery,
export function Controls({ setSecurityOpen, showSecurityLock }: ControlsProps) {
const params = useParams();
const { data: conversation } = useUserConversation(
params.conversationId ?? null,
);
const projectMenuCardData = React.useMemo(
() =>
selectedRepository && lastCommitData
? {
repoName: selectedRepository,
lastCommit: lastCommitData,
avatar: null, // TODO: fetch repo avatar
}
: null,
[selectedRepository, lastCommitData],
);
const [downloading, setDownloading] = React.useState(false);
const handleDownloadWorkspace = () => {
posthog.capture("download_workspace_button_clicked");
setDownloading(true);
};
return (
<div className="flex items-center justify-between">
@@ -46,9 +37,19 @@ export function Controls({
)}
</div>
<ProjectMenuCard
isConnectedToGitHub={!!gitHubToken}
githubData={projectMenuCardData}
<ConversationCard
variant="compact"
onDownloadWorkspace={handleDownloadWorkspace}
title={conversation?.title ?? ""}
lastUpdatedAt={conversation?.created_at ?? ""}
selectedRepository={conversation?.selected_repository ?? null}
status={conversation?.status}
/>
<DownloadModal
initialPath=""
onClose={() => setDownloading(false)}
isOpen={downloading}
/>
</div>
);

View File

@@ -1,17 +1,22 @@
import { useClickOutsideElement } from "#/hooks/use-click-outside-element";
import { cn } from "#/utils/utils";
import { ContextMenu } from "../context-menu/context-menu";
import { ContextMenuListItem } from "../context-menu/context-menu-list-item";
interface ConversationCardContextMenuProps {
onClose: () => void;
onDelete: (event: React.MouseEvent<HTMLButtonElement>) => void;
onEdit: (event: React.MouseEvent<HTMLButtonElement>) => void;
onDelete?: (event: React.MouseEvent<HTMLButtonElement>) => void;
onEdit?: (event: React.MouseEvent<HTMLButtonElement>) => void;
onDownload?: (event: React.MouseEvent<HTMLButtonElement>) => void;
position?: "top" | "bottom";
}
export function ConversationCardContextMenu({
onClose,
onDelete,
onEdit,
onDownload,
position = "bottom",
}: ConversationCardContextMenuProps) {
const ref = useClickOutsideElement<HTMLUListElement>(onClose);
@@ -19,14 +24,27 @@ export function ConversationCardContextMenu({
<ContextMenu
ref={ref}
testId="context-menu"
className="left-full float-right"
className={cn(
"right-0 absolute",
position === "top" && "bottom-full",
position === "bottom" && "top-full",
)}
>
<ContextMenuListItem testId="delete-button" onClick={onDelete}>
Delete
</ContextMenuListItem>
<ContextMenuListItem testId="edit-button" onClick={onEdit}>
Edit Title
</ContextMenuListItem>
{onDelete && (
<ContextMenuListItem testId="delete-button" onClick={onDelete}>
Delete
</ContextMenuListItem>
)}
{onEdit && (
<ContextMenuListItem testId="edit-button" onClick={onEdit}>
Edit Title
</ContextMenuListItem>
)}
{onDownload && (
<ContextMenuListItem testId="download-button" onClick={onDownload}>
Download Workspace
</ContextMenuListItem>
)}
</ContextMenu>
);
}

View File

@@ -7,25 +7,32 @@ import {
} from "./conversation-state-indicator";
import { EllipsisButton } from "./ellipsis-button";
import { ConversationCardContextMenu } from "./conversation-card-context-menu";
import { cn } from "#/utils/utils";
interface ConversationCardProps {
onDelete: () => void;
onChangeTitle: (title: string) => void;
isActive: boolean;
onClick?: () => void;
onDelete?: () => void;
onChangeTitle?: (title: string) => void;
onDownloadWorkspace?: () => void;
isActive?: boolean;
title: string;
selectedRepository: string | null;
lastUpdatedAt: string; // ISO 8601
status?: ProjectStatus;
variant?: "compact" | "default";
}
export function ConversationCard({
onClick,
onDelete,
onChangeTitle,
onDownloadWorkspace,
isActive,
title,
selectedRepository,
lastUpdatedAt,
status = "STOPPED",
variant = "default",
}: ConversationCardProps) {
const [contextMenuVisible, setContextMenuVisible] = React.useState(false);
const [titleMode, setTitleMode] = React.useState<"view" | "edit">("view");
@@ -34,7 +41,7 @@ export function ConversationCard({
const handleBlur = () => {
if (inputRef.current?.value) {
const trimmed = inputRef.current.value.trim();
onChangeTitle(trimmed);
onChangeTitle?.(trimmed);
inputRef.current!.value = trimmed;
} else {
// reset the value if it's empty
@@ -58,7 +65,7 @@ export function ConversationCard({
const handleDelete = (event: React.MouseEvent<HTMLButtonElement>) => {
event.preventDefault();
event.stopPropagation();
onDelete();
onDelete?.();
};
const handleEdit = (event: React.MouseEvent<HTMLButtonElement>) => {
@@ -68,16 +75,28 @@ export function ConversationCard({
setContextMenuVisible(false);
};
const handleDownload = (event: React.MouseEvent<HTMLButtonElement>) => {
event.stopPropagation();
onDownloadWorkspace?.();
};
React.useEffect(() => {
if (titleMode === "edit") {
inputRef.current?.focus();
}
}, [titleMode]);
const hasContextMenu = !!(onDelete || onChangeTitle || onDownloadWorkspace);
return (
<div
data-testid="conversation-card"
className="h-[100px] w-full px-[18px] py-4 border-b border-neutral-600 cursor-pointer"
onClick={onClick}
className={cn(
"h-[100px] w-full px-[18px] py-4 border-b border-neutral-600 cursor-pointer",
variant === "compact" &&
"h-auto w-fit rounded-xl border border-[#525252]",
)}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2 w-full">
@@ -97,31 +116,42 @@ export function ConversationCard({
<div className="flex items-center gap-2 relative">
<ConversationStateIndicator status={status} />
<EllipsisButton
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
setContextMenuVisible((prev) => !prev);
}}
/>
{hasContextMenu && (
<EllipsisButton
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
setContextMenuVisible((prev) => !prev);
}}
/>
)}
{contextMenuVisible && (
<ConversationCardContextMenu
onClose={() => setContextMenuVisible(false)}
onDelete={onDelete && handleDelete}
onEdit={onChangeTitle && handleEdit}
onDownload={onDownloadWorkspace && handleDownload}
position={variant === "compact" ? "top" : "bottom"}
/>
)}
</div>
</div>
{contextMenuVisible && (
<ConversationCardContextMenu
onClose={() => setContextMenuVisible(false)}
onDelete={handleDelete}
onEdit={handleEdit}
/>
)}
{selectedRepository && (
<ConversationRepoLink
selectedRepository={selectedRepository}
onClick={(e) => e.stopPropagation()}
/>
)}
<p className="text-xs text-neutral-400">
<time>{formatTimeDelta(new Date(lastUpdatedAt))} ago</time>
</p>
<div
className={cn(
variant === "compact" && "flex items-center justify-between mt-1",
)}
>
{selectedRepository && (
<ConversationRepoLink
selectedRepository={selectedRepository}
onClick={(e) => e.stopPropagation()}
/>
)}
<p className="text-xs text-neutral-400">
<time>{formatTimeDelta(new Date(lastUpdatedAt))} ago</time>
</p>
</div>
</div>
);
}

View File

@@ -1,5 +1,7 @@
import React from "react";
import { NavLink, useParams } from "react-router";
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
import { ConversationCard } from "./conversation-card";
import { useUserConversations } from "#/hooks/query/use-user-conversations";
import { useDeleteConversation } from "#/hooks/mutation/use-delete-conversation";
@@ -15,6 +17,7 @@ interface ConversationPanelProps {
}
export function ConversationPanel({ onClose }: ConversationPanelProps) {
const { t } = useTranslation();
const { conversationId: cid } = useParams();
const endSession = useEndSession();
const ref = useClickOutsideElement<HTMLDivElement>(onClose);
@@ -78,7 +81,9 @@ export function ConversationPanel({ onClose }: ConversationPanelProps) {
)}
{conversations?.length === 0 && (
<div className="flex flex-col items-center justify-center h-full">
<p className="text-neutral-400">No conversations found</p>
<p className="text-neutral-400">
{t(I18nKey.CONVERSATION$NO_CONVERSATIONS)}
</p>
</div>
)}
{conversations?.map((project) => (

View File

@@ -1,8 +1,12 @@
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
interface NewConversationButtonProps {
onClick: () => void;
}
export function NewConversationButton({ onClick }: NewConversationButtonProps) {
const { t } = useTranslation();
return (
<button
data-testid="new-conversation-button"
@@ -10,7 +14,7 @@ export function NewConversationButton({ onClick }: NewConversationButtonProps) {
onClick={onClick}
className="font-bold bg-[#4465DB] px-2 py-1 rounded"
>
+ New Project
+ {t(I18nKey.PROJECT$NEW)}
</button>
);
}

View File

@@ -1,5 +1,7 @@
import React from "react";
import hotToast from "react-hot-toast";
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
import { Feedback } from "#/api/open-hands.types";
import { useSubmitFeedback } from "#/hooks/mutation/use-submit-feedback";
import { ModalButton } from "#/components/shared/buttons/modal-button";
@@ -13,8 +15,9 @@ interface FeedbackFormProps {
}
export function FeedbackForm({ onClose, polarity }: FeedbackFormProps) {
const { t } = useTranslation();
const copiedToClipboardToast = () => {
hotToast("Password copied to clipboard", {
hotToast(t(I18nKey.FEEDBACK$PASSWORD_COPIED_MESSAGE), {
icon: "📋",
position: "bottom-right",
});
@@ -41,10 +44,13 @@ export function FeedbackForm({ onClose, polarity }: FeedbackFormProps) {
target="_blank"
rel="noreferrer"
>
Go to shared feedback
{t(I18nKey.FEEDBACK$GO_TO_FEEDBACK)}
</a>
<span onClick={() => onPressToast(password)} className="cursor-pointer">
Password: {password} <span className="text-gray-500">(copy)</span>
{t(I18nKey.FEEDBACK$PASSWORD)}: {password}{" "}
<span className="text-gray-500">
({t(I18nKey.FEEDBACK$COPY_LABEL)})
</span>
</span>
</div>,
{ duration: 10000 },
@@ -86,12 +92,14 @@ export function FeedbackForm({ onClose, polarity }: FeedbackFormProps) {
return (
<form onSubmit={handleSubmit} className="flex flex-col gap-6 w-full">
<label className="flex flex-col gap-2">
<span className="text-xs text-neutral-400">Email</span>
<span className="text-xs text-neutral-400">
{t(I18nKey.FEEDBACK$EMAIL_LABEL)}
</span>
<input
required
name="email"
type="email"
placeholder="Please enter your email"
placeholder={t(I18nKey.FEEDBACK$EMAIL_PLACEHOLDER)}
className="bg-[#27272A] px-3 py-[10px] rounded"
/>
</label>
@@ -104,11 +112,11 @@ export function FeedbackForm({ onClose, polarity }: FeedbackFormProps) {
type="radio"
defaultChecked
/>
Private
{t(I18nKey.FEEDBACK$PRIVATE_LABEL)}
</label>
<label className="flex gap-2 cursor-pointer">
<input name="permissions" value="public" type="radio" />
Public
{t(I18nKey.FEEDBACK$PUBLIC_LABEL)}
</label>
</div>
@@ -116,12 +124,12 @@ export function FeedbackForm({ onClose, polarity }: FeedbackFormProps) {
<ModalButton
disabled={isPending}
type="submit"
text="Submit"
text={t(I18nKey.FEEDBACK$CONTRIBUTE_LABEL)}
className="bg-[#4465DB] grow"
/>
<ModalButton
disabled={isPending}
text="Cancel"
text={t(I18nKey.FEEDBACK$CANCEL_LABEL)}
onClick={onClose}
className="bg-[#737373] grow"
/>

View File

@@ -1,4 +1,5 @@
import React from "react";
import { useTranslation } from "react-i18next";
import {
Autocomplete,
AutocompleteItem,
@@ -6,6 +7,7 @@ import {
} from "@nextui-org/react";
import { useDispatch } from "react-redux";
import posthog from "posthog-js";
import { I18nKey } from "#/i18n/declaration";
import { setSelectedRepository } from "#/state/initial-query-slice";
import { useConfig } from "#/hooks/query/use-config";
import { sanitizeQuery } from "#/utils/sanitize-query";
@@ -23,6 +25,7 @@ export function GitHubRepositorySelector({
userRepositories,
publicRepositories,
}: GitHubRepositorySelectorProps) {
const { t } = useTranslation();
const { data: config } = useConfig();
const [selectedKey, setSelectedKey] = React.useState<string | null>(null);
@@ -49,14 +52,14 @@ export function GitHubRepositorySelector({
dispatch(setSelectedRepository(null));
};
const emptyContent = "No results found.";
const emptyContent = t(I18nKey.GITHUB$NO_RESULTS);
return (
<Autocomplete
data-testid="github-repo-selector"
name="repo"
aria-label="GitHub Repository"
placeholder="Select a GitHub project"
placeholder={t(I18nKey.LANDING$SELECT_REPO)}
isVirtualized={false}
selectedKey={selectedKey}
inputProps={{
@@ -86,12 +89,12 @@ export function GitHubRepositorySelector({
rel="noreferrer noopener"
onClick={(e) => e.stopPropagation()}
>
Add more repositories...
{t(I18nKey.GITHUB$ADD_MORE_REPOS)}
</a>
</AutocompleteItem> // eslint-disable-next-line @typescript-eslint/no-explicit-any
) as any)}
{userRepositories.length > 0 && (
<AutocompleteSection showDivider title="Your Repos">
<AutocompleteSection showDivider title={t(I18nKey.GITHUB$YOUR_REPOS)}>
{userRepositories.map((repo) => (
<AutocompleteItem
data-testid="github-repo-item"
@@ -106,7 +109,7 @@ export function GitHubRepositorySelector({
</AutocompleteSection>
)}
{publicRepositories.length > 0 && (
<AutocompleteSection showDivider title="Public Repos">
<AutocompleteSection showDivider title={t(I18nKey.GITHUB$PUBLIC_REPOS)}>
{publicRepositories.map((repo) => (
<AutocompleteItem
data-testid="github-repo-item"

View File

@@ -1,4 +1,6 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
import { SuggestionBox } from "#/components/features/suggestions/suggestion-box";
import GitHubLogo from "#/assets/branding/github-logo.svg?react";
import { GitHubRepositorySelector } from "./github-repo-selector";
@@ -23,6 +25,7 @@ export function GitHubRepositoriesSuggestionBox({
gitHubAuthUrl,
user,
}: GitHubRepositoriesSuggestionBoxProps) {
const { t } = useTranslation();
const [connectToGitHubModalOpen, setConnectToGitHubModalOpen] =
React.useState(false);
const [searchQuery, setSearchQuery] = React.useState<string>("");
@@ -53,18 +56,18 @@ export function GitHubRepositoriesSuggestionBox({
return (
<>
<SuggestionBox
title="Open a Repo"
title={t(I18nKey.LANDING$OPEN_REPO)}
content={
isLoggedIn ? (
<GitHubRepositorySelector
onInputChange={setSearchQuery}
onSelect={handleSubmit}
publicRepositories={searchedRepos}
publicRepositories={searchedRepos || []}
userRepositories={repositories}
/>
) : (
<ModalButton
text="Connect to GitHub"
text={t(I18nKey.GITHUB$CONNECT)}
icon={<GitHubLogo width={20} height={20} />}
className="bg-[#791B80] w-full"
onClick={handleConnectToGitHub}

View File

@@ -1,10 +1,13 @@
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
import Clip from "#/icons/clip.svg?react";
export function AttachImageLabel() {
const { t } = useTranslation();
return (
<div className="flex self-start items-center text-[#A3A3A3] text-xs leading-[18px] -tracking-[0.08px] cursor-pointer">
<Clip width={16} height={16} />
Attach images
{t(I18nKey.LANDING$ATTACH_IMAGES)}
</div>
);
}

View File

@@ -1,6 +1,8 @@
import Markdown from "react-markdown";
import SyntaxHighlighter from "react-syntax-highlighter";
import { atomOneDark } from "react-syntax-highlighter/dist/esm/styles/hljs";
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
import { JupyterLine } from "#/utils/parse-cell-content";
interface JupyterCellOutputProps {
@@ -8,9 +10,12 @@ interface JupyterCellOutputProps {
}
export function JupyterCellOutput({ lines }: JupyterCellOutputProps) {
const { t } = useTranslation();
return (
<div className="rounded-lg bg-gray-800 dark:bg-gray-900 p-2 text-xs">
<div className="mb-1 text-gray-400">STDOUT/STDERR</div>
<div className="mb-1 text-gray-400">
{t(I18nKey.JUPYTER$OUTPUT_LABEL)}
</div>
<pre
className="scrollbar-custom scrollbar-thumb-gray-500 hover:scrollbar-thumb-gray-400 dark:scrollbar-thumb-white/10 dark:hover:scrollbar-thumb-white/20 overflow-auto px-5 max-h-[60vh] bg-gray-800"
style={{ padding: 0, marginBottom: 0, fontSize: "0.75rem" }}

View File

@@ -1,92 +0,0 @@
import React from "react";
import posthog from "posthog-js";
import { useTranslation } from "react-i18next";
import EllipsisH from "#/icons/ellipsis-h.svg?react";
import { ProjectMenuCardContextMenu } from "./project.menu-card-context-menu";
import { ProjectMenuDetailsPlaceholder } from "./project-menu-details-placeholder";
import { ProjectMenuDetails } from "./project-menu-details";
import { ConnectToGitHubModal } from "#/components/shared/modals/connect-to-github-modal";
import { ModalBackdrop } from "#/components/shared/modals/modal-backdrop";
import { DownloadModal } from "#/components/shared/download-modal";
import { I18nKey } from "#/i18n/declaration";
interface ProjectMenuCardProps {
isConnectedToGitHub: boolean;
githubData: {
avatar: string | null;
repoName: string;
lastCommit: GitHubCommit;
} | null;
}
export function ProjectMenuCard({
isConnectedToGitHub,
githubData,
}: ProjectMenuCardProps) {
const { t } = useTranslation();
const [contextMenuIsOpen, setContextMenuIsOpen] = React.useState(false);
const [connectToGitHubModalOpen, setConnectToGitHubModalOpen] =
React.useState(false);
const [downloading, setDownloading] = React.useState(false);
const toggleMenuVisibility = () => {
setContextMenuIsOpen((prev) => !prev);
};
const handleDownloadWorkspace = () => {
posthog.capture("download_workspace_button_clicked");
setDownloading(true);
};
const handleDownloadClose = () => {
setDownloading(false);
};
return (
<div className="px-4 py-[10px] w-[337px] rounded-xl border border-[#525252] flex justify-between items-center relative">
{!downloading && contextMenuIsOpen && (
<ProjectMenuCardContextMenu
isConnectedToGitHub={isConnectedToGitHub}
onConnectToGitHub={() => setConnectToGitHubModalOpen(true)}
onDownloadWorkspace={handleDownloadWorkspace}
onClose={() => setContextMenuIsOpen(false)}
/>
)}
{githubData && (
<ProjectMenuDetails
repoName={githubData.repoName}
avatar={githubData.avatar}
lastCommit={githubData.lastCommit}
/>
)}
{!githubData && (
<ProjectMenuDetailsPlaceholder
isConnectedToGitHub={isConnectedToGitHub}
onConnectToGitHub={() => setConnectToGitHubModalOpen(true)}
/>
)}
<DownloadModal
initialPath=""
onClose={handleDownloadClose}
isOpen={downloading}
/>
{!downloading && (
<button
type="button"
onClick={toggleMenuVisibility}
aria-label={t(I18nKey.PROJECT_MENU_CARD$OPEN)}
>
<EllipsisH width={36} height={36} />
</button>
)}
{connectToGitHubModalOpen && (
<ModalBackdrop onClose={() => setConnectToGitHubModalOpen(false)}>
<ConnectToGitHubModal
onClose={() => setConnectToGitHubModalOpen(false)}
/>
</ModalBackdrop>
)}
</div>
);
}

View File

@@ -1,41 +0,0 @@
import { useTranslation } from "react-i18next";
import { cn } from "#/utils/utils";
import CloudConnection from "#/icons/cloud-connection.svg?react";
import { I18nKey } from "#/i18n/declaration";
interface ProjectMenuDetailsPlaceholderProps {
isConnectedToGitHub: boolean;
onConnectToGitHub: () => void;
}
export function ProjectMenuDetailsPlaceholder({
isConnectedToGitHub,
onConnectToGitHub,
}: ProjectMenuDetailsPlaceholderProps) {
const { t } = useTranslation();
return (
<div className="flex flex-col">
<span className="text-sm leading-6 font-semibold">
{t(I18nKey.PROJECT_MENU_DETAILS_PLACEHOLDER$NEW_PROJECT_LABEL)}
</span>
<button
type="button"
onClick={onConnectToGitHub}
disabled={isConnectedToGitHub}
>
<span
className={cn(
"text-xs leading-4 text-[#A3A3A3] flex items-center gap-2",
"hover:underline hover:underline-offset-2",
)}
>
{!isConnectedToGitHub
? t(I18nKey.PROJECT_MENU_DETAILS_PLACEHOLDER$CONNECT_TO_GITHUB)
: t(I18nKey.PROJECT_MENU_DETAILS_PLACEHOLDER$CONNECTED)}
<CloudConnection width={12} height={12} />
</span>
</button>
</div>
);
}

View File

@@ -1,52 +0,0 @@
import { useTranslation } from "react-i18next";
import ExternalLinkIcon from "#/icons/external-link.svg?react";
import { formatTimeDelta } from "#/utils/format-time-delta";
import { I18nKey } from "#/i18n/declaration";
interface ProjectMenuDetailsProps {
repoName: string;
avatar: string | null;
lastCommit: GitHubCommit;
}
export function ProjectMenuDetails({
repoName,
avatar,
lastCommit,
}: ProjectMenuDetailsProps) {
const { t } = useTranslation();
return (
<div className="flex flex-col min-w-0">
<a
href={`https://github.com/${repoName}`}
target="_blank"
rel="noreferrer noopener"
className="flex items-center gap-2 min-w-0"
>
{avatar && (
<img
src={avatar}
alt=""
className="w-4 h-4 rounded-full flex-shrink-0"
/>
)}
<span className="text-sm leading-6 font-semibold truncate flex-1">
{repoName}
</span>
<ExternalLinkIcon width={16} height={16} className="flex-shrink-0" />
</a>
<a
href={lastCommit.html_url}
target="_blank"
rel="noreferrer noopener"
className="text-xs text-[#A3A3A3] hover:underline hover:underline-offset-2"
>
<span>{lastCommit.sha.slice(-7)}</span> <span>&middot;</span>{" "}
<span>
{formatTimeDelta(new Date(lastCommit.commit.author.date))}{" "}
{t(I18nKey.PROJECT_MENU_DETAILS$AGO_LABEL)}
</span>
</a>
</div>
);
}

View File

@@ -1,37 +0,0 @@
import { useTranslation } from "react-i18next";
import { useClickOutsideElement } from "#/hooks/use-click-outside-element";
import { ContextMenu } from "../context-menu/context-menu";
import { ContextMenuListItem } from "../context-menu/context-menu-list-item";
import { I18nKey } from "#/i18n/declaration";
interface ProjectMenuCardContextMenuProps {
isConnectedToGitHub: boolean;
onConnectToGitHub: () => void;
onDownloadWorkspace: () => void;
onClose: () => void;
}
export function ProjectMenuCardContextMenu({
isConnectedToGitHub,
onConnectToGitHub,
onDownloadWorkspace,
onClose,
}: ProjectMenuCardContextMenuProps) {
const menuRef = useClickOutsideElement<HTMLUListElement>(onClose);
const { t } = useTranslation();
return (
<ContextMenu
ref={menuRef}
className="absolute right-0 bottom-[calc(100%+8px)]"
>
{!isConnectedToGitHub && (
<ContextMenuListItem onClick={onConnectToGitHub}>
{t(I18nKey.PROJECT_MENU_CARD_CONTEXT_MENU$CONNECT_TO_GITHUB_LABEL)}
</ContextMenuListItem>
)}
<ContextMenuListItem onClick={onDownloadWorkspace}>
{t(I18nKey.PROJECT_MENU_CARD_CONTEXT_MENU$DOWNLOAD_FILES_LABEL)}
</ContextMenuListItem>
</ContextMenu>
);
}

View File

@@ -0,0 +1,19 @@
interface PathFormProps {
ref: React.RefObject<HTMLFormElement | null>;
onBlur: () => void;
defaultValue: string;
}
export function PathForm({ ref, onBlur, defaultValue }: PathFormProps) {
return (
<form ref={ref} onSubmit={(e) => e.preventDefault()} className="flex-1">
<input
name="url"
type="text"
defaultValue={defaultValue}
className="w-full bg-transparent"
onBlur={onBlur}
/>
</form>
);
}

View File

@@ -12,7 +12,7 @@ import { SettingsButton } from "#/components/shared/buttons/settings-button";
import { LoadingSpinner } from "#/components/shared/loading-spinner";
import { AccountSettingsModal } from "#/components/shared/modals/account-settings/account-settings-modal";
import { SettingsModal } from "#/components/shared/modals/settings/settings-modal";
import { useSettingsUpToDate } from "#/context/settings-up-to-date-context";
import { useCurrentSettings } from "#/context/settings-context";
import { useSettings } from "#/hooks/query/use-settings";
import { ConversationPanel } from "../conversation-panel/conversation-panel";
import { MULTI_CONVERSATION_UI } from "#/utils/feature-flags";
@@ -28,8 +28,13 @@ export function Sidebar() {
const user = useGitHubUser();
const { data: isAuthed } = useIsAuthed();
const { logout } = useAuth();
const { data: settings, isError: settingsIsError } = useSettings();
const { isUpToDate: settingsAreUpToDate } = useSettingsUpToDate();
const {
data: settings,
isError: settingsIsError,
isSuccess: settingsSuccessfulyFetched,
} = useSettings();
const { isUpToDate: settingsAreUpToDate } = useCurrentSettings();
const [accountSettingsModalOpen, setAccountSettingsModalOpen] =
React.useState(false);
@@ -72,7 +77,7 @@ export function Sidebar() {
<ExitProjectButton onClick={handleEndSession} />
{MULTI_CONVERSATION_UI && (
<TooltipButton
data-testid="toggle-conversation-panel"
testId="toggle-conversation-panel"
tooltip="Conversations"
ariaLabel="Conversations"
onClick={() => setConversationPanelIsOpen((prev) => !prev)}
@@ -106,7 +111,7 @@ export function Sidebar() {
<AccountSettingsModal onClose={handleAccountSettingsModalClose} />
)}
{settingsIsError ||
(showSettingsModal && (
(showSettingsModal && settingsSuccessfulyFetched && (
<SettingsModal
settings={settings}
onClose={() => setSettingsModalIsOpen(false)}

View File

@@ -1,3 +1,5 @@
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
import { LoadingSpinner } from "#/components/shared/loading-spinner";
import DefaultUserAvatar from "#/icons/default-user.svg?react";
import { cn } from "#/utils/utils";
@@ -11,11 +13,12 @@ interface UserAvatarProps {
}
export function UserAvatar({ onClick, avatarUrl, isLoading }: UserAvatarProps) {
const { t } = useTranslation();
return (
<TooltipButton
testId="user-avatar"
tooltip="Account settings"
ariaLabel="Account settings"
tooltip={t(I18nKey.USER$ACCOUNT_SETTINGS)}
ariaLabel={t(I18nKey.USER$ACCOUNT_SETTINGS)}
onClick={onClick}
className={cn(
"w-8 h-8 rounded-full flex items-center justify-center border-2 border-gray-200",
@@ -25,7 +28,7 @@ export function UserAvatar({ onClick, avatarUrl, isLoading }: UserAvatarProps) {
{!isLoading && avatarUrl && <Avatar src={avatarUrl} />}
{!isLoading && !avatarUrl && (
<DefaultUserAvatar
aria-label="user avatar placeholder"
aria-label={t(I18nKey.USER$AVATAR_PLACEHOLDER)}
width={20}
height={20}
/>

View File

@@ -1,3 +1,5 @@
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
import { SuggestionBox } from "./suggestion-box";
interface ImportProjectSuggestionBoxProps {
@@ -7,13 +9,14 @@ interface ImportProjectSuggestionBoxProps {
export function ImportProjectSuggestionBox({
onChange,
}: ImportProjectSuggestionBoxProps) {
const { t } = useTranslation();
return (
<SuggestionBox
title="+ Import Project"
title={t(I18nKey.LANDING$IMPORT_PROJECT)}
content={
<label htmlFor="import-project" className="w-full flex justify-center">
<span className="border-2 border-dashed border-neutral-600 rounded px-2 py-1 cursor-pointer">
Upload a .zip
{t(I18nKey.LANDING$UPLOAD_ZIP)}
</span>
<input
hidden

View File

@@ -1,8 +1,10 @@
import { useTranslation } from "react-i18next";
import { RefreshButton } from "#/components/shared/buttons/refresh-button";
import Lightbulb from "#/icons/lightbulb.svg?react";
import { I18nKey } from "#/i18n/declaration";
interface SuggestionBubbleProps {
suggestion: string;
suggestion: { key: string; value: string };
onClick: () => void;
onRefresh: () => void;
}
@@ -12,6 +14,7 @@ export function SuggestionBubble({
onClick,
onRefresh,
}: SuggestionBubbleProps) {
const { t } = useTranslation();
const handleRefresh = (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
onRefresh();
@@ -24,7 +27,7 @@ export function SuggestionBubble({
>
<div className="flex items-center gap-2">
<Lightbulb width={18} height={18} />
<span className="text-sm">{suggestion}</span>
<span className="text-sm">{t(suggestion.key as I18nKey)}</span>
</div>
<RefreshButton onClick={handleRefresh} />
</div>

View File

@@ -1,4 +1,7 @@
export type Suggestion = { label: string; value: string };
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
export type Suggestion = { label: I18nKey | string; value: string };
interface SuggestionItemProps {
suggestion: Suggestion;
@@ -6,6 +9,7 @@ interface SuggestionItemProps {
}
export function SuggestionItem({ suggestion, onClick }: SuggestionItemProps) {
const { t } = useTranslation();
return (
<li className="list-none border border-neutral-600 rounded-xl hover:bg-neutral-700 flex-1">
<button
@@ -14,7 +18,7 @@ export function SuggestionItem({ suggestion, onClick }: SuggestionItemProps) {
onClick={() => onClick(suggestion.value)}
className="text-[16px] leading-6 -tracking-[0.01em] text-center w-full p-3 font-semibold"
>
{suggestion.label}
{t(suggestion.label)}
</button>
</li>
);

View File

@@ -1,9 +1,12 @@
import { useSelector } from "react-redux";
import { useTranslation } from "react-i18next";
import { cn } from "#/utils/utils";
import { AgentState } from "#/types/agent-state";
import { RootState } from "#/store";
import { I18nKey } from "#/i18n/declaration";
export function TerminalStatusLabel() {
const { t } = useTranslation();
const { curAgentState } = useSelector((state: RootState) => state.agent);
return (
@@ -17,7 +20,7 @@ export function TerminalStatusLabel() {
: "bg-green-500",
)}
/>
Terminal
{t(I18nKey.WORKSPACE$TERMINAL_TAB_LABEL)}
</div>
);
}

View File

@@ -1,20 +1,24 @@
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
interface TOSCheckboxProps {
onChange: () => void;
}
export function TOSCheckbox({ onChange }: TOSCheckboxProps) {
const { t } = useTranslation();
return (
<label className="flex items-center gap-2">
<input type="checkbox" onChange={onChange} />
<span>
I accept the{" "}
{t(I18nKey.TOS$ACCEPT)}{" "}
<a
href="https://www.all-hands.dev/tos"
target="_blank"
rel="noopener noreferrer"
className="underline underline-offset-2 text-blue-500 hover:text-blue-700"
>
terms of service
{t(I18nKey.TOS$TERMS)}
</a>
</span>
</label>

View File

@@ -1,7 +1,11 @@
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
export function BetaBadge() {
const { t } = useTranslation();
return (
<span className="text-[11px] leading-5 text-root-primary bg-neutral-400 px-1 rounded-xl">
Beta
{t(I18nKey.BADGE$BETA)}
</span>
);
}

View File

@@ -0,0 +1,18 @@
import { useTranslation } from "react-i18next";
import { useActiveHost } from "#/hooks/query/use-active-host";
import { I18nKey } from "#/i18n/declaration";
export function ServedAppLabel() {
const { t } = useTranslation();
const { activeHost } = useActiveHost();
return (
<div className="flex items-center justify-between w-full">
<div className="flex items-center gap-2">
<div className="flex items-center gap-2">{t(I18nKey.APP$TITLE)}</div>
<span className="border rounded-md text- px-1 font-bold">BETA</span>
</div>
{activeHost && <div className="w-2 h-2 bg-green-500 rounded-full" />}
</div>
);
}

View File

@@ -22,7 +22,11 @@ export function ActionTooltip({ type, onClick }: ActionTooltipProps) {
<button
data-testid={`action-${type}-button`}
type="button"
aria-label={type === "confirm" ? "Confirm action" : "Reject action"}
aria-label={
type === "confirm"
? t(I18nKey.ACTION$CONFIRM)
: t(I18nKey.ACTION$REJECT)
}
className="bg-neutral-700 rounded-full p-1 hover:bg-neutral-800"
onClick={onClick}
>

View File

@@ -1,11 +1,14 @@
import { useTranslation } from "react-i18next";
import DocsIcon from "#/icons/docs.svg?react";
import { I18nKey } from "#/i18n/declaration";
import { TooltipButton } from "./tooltip-button";
export function DocsButton() {
const { t } = useTranslation();
return (
<TooltipButton
tooltip="Documentation"
ariaLabel="Documentation"
tooltip={t(I18nKey.SIDEBAR$DOCS)}
ariaLabel={t(I18nKey.SIDEBAR$DOCS)}
href="https://docs.all-hands.dev"
>
<DocsIcon width={28} height={28} />

View File

@@ -1,3 +1,5 @@
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
import NewProjectIcon from "#/icons/new-project.svg?react";
import { TooltipButton } from "./tooltip-button";
@@ -6,10 +8,12 @@ interface ExitProjectButtonProps {
}
export function ExitProjectButton({ onClick }: ExitProjectButtonProps) {
const { t } = useTranslation();
const startNewProject = t(I18nKey.PROJECT$START_NEW);
return (
<TooltipButton
tooltip="Start new project"
ariaLabel="Start new project"
tooltip={startNewProject}
ariaLabel={startNewProject}
onClick={onClick}
testId="new-project-button"
>

View File

@@ -1,3 +1,5 @@
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
import { cn } from "#/utils/utils";
import VSCodeIcon from "#/assets/vscode-alt.svg?react";
@@ -10,6 +12,9 @@ export function OpenVSCodeButton({
isDisabled,
onClick,
}: OpenVSCodeButtonProps) {
const { t } = useTranslation();
const buttonText = t(I18nKey.VSCODE$OPEN);
return (
<button
type="button"
@@ -21,10 +26,10 @@ export function OpenVSCodeButton({
? "bg-neutral-600 cursor-not-allowed"
: "bg-[#4465DB] hover:bg-[#3451C7]",
)}
aria-label="Open in VS Code"
aria-label={buttonText}
>
<VSCodeIcon width={20} height={20} />
Open in VS Code
{buttonText}
</button>
);
}

View File

@@ -1,16 +1,19 @@
import { FaCog } from "react-icons/fa";
import { useTranslation } from "react-i18next";
import { TooltipButton } from "./tooltip-button";
import { I18nKey } from "#/i18n/declaration";
interface SettingsButtonProps {
onClick: () => void;
}
export function SettingsButton({ onClick }: SettingsButtonProps) {
const { t } = useTranslation();
return (
<TooltipButton
testId="settings-button"
tooltip="Settings"
ariaLabel="Settings"
tooltip={t(I18nKey.SETTINGS$TITLE)}
ariaLabel={t(I18nKey.SETTINGS$TITLE)}
onClick={onClick}
>
<FaCog size={24} />

View File

@@ -1,13 +1,17 @@
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
interface StopButtonProps {
isDisabled?: boolean;
onClick?: () => void;
}
export function StopButton({ isDisabled, onClick }: StopButtonProps) {
const { t } = useTranslation();
return (
<button
data-testid="stop-button"
aria-label="Stop"
aria-label={t(I18nKey.BUTTON$STOP)}
disabled={isDisabled}
onClick={onClick}
type="button"

View File

@@ -1,4 +1,6 @@
import { useTranslation } from "react-i18next";
import ArrowSendIcon from "#/icons/arrow-send.svg?react";
import { I18nKey } from "#/i18n/declaration";
interface SubmitButtonProps {
isDisabled?: boolean;
@@ -6,9 +8,10 @@ interface SubmitButtonProps {
}
export function SubmitButton({ isDisabled, onClick }: SubmitButtonProps) {
const { t } = useTranslation();
return (
<button
aria-label="Send"
aria-label={t(I18nKey.BUTTON$SEND)}
disabled={isDisabled}
onClick={onClick}
type="submit"

View File

@@ -1,3 +1,6 @@
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
export interface DownloadProgressState {
filesTotal: number;
filesDownloaded: number;
@@ -16,6 +19,7 @@ export function DownloadProgress({
progress,
onCancel,
}: DownloadProgressProps) {
const { t } = useTranslation();
const formatBytes = (bytes: number) => {
const units = ["B", "KB", "MB", "GB"];
let size = bytes;
@@ -33,12 +37,12 @@ export function DownloadProgress({
<div className="mb-4">
<h3 className="text-lg font-semibold mb-2 text-white">
{progress.isDiscoveringFiles
? "Preparing Download..."
: "Downloading Files"}
? t(I18nKey.DOWNLOAD$PREPARING)
: t(I18nKey.DOWNLOAD$DOWNLOADING)}
</h3>
<p className="text-sm text-gray-400 truncate">
{progress.isDiscoveringFiles
? `Found ${progress.filesTotal} files...`
? t(I18nKey.DOWNLOAD$FOUND_FILES, { count: progress.filesTotal })
: progress.currentFile}
</p>
</div>
@@ -64,8 +68,11 @@ export function DownloadProgress({
<div className="flex justify-between text-sm text-gray-400">
<span>
{progress.isDiscoveringFiles
? `Scanning workspace...`
: `${progress.filesDownloaded} of ${progress.filesTotal} files`}
? t(I18nKey.DOWNLOAD$SCANNING)
: t(I18nKey.DOWNLOAD$FILES_PROGRESS, {
downloaded: progress.filesDownloaded,
total: progress.filesTotal,
})}
</span>
{!progress.isDiscoveringFiles && (
<span>{formatBytes(progress.bytesDownloadedPerSecond)}/s</span>
@@ -78,7 +85,7 @@ export function DownloadProgress({
onClick={onCancel}
className="px-4 py-2 text-sm text-gray-400 hover:text-white transition-colors"
>
Cancel
{t(I18nKey.DOWNLOAD$CANCEL)}
</button>
</div>
</div>

View File

@@ -1,24 +1,26 @@
import { useTranslation } from "react-i18next";
import BuildIt from "#/icons/build-it.svg?react";
import { I18nKey } from "#/i18n/declaration";
export function HeroHeading() {
const { t } = useTranslation();
return (
<div className="w-[304px] text-center flex flex-col gap-4 items-center py-4">
<BuildIt width={88} height={104} />
<h1 className="text-[38px] leading-[32px] -tracking-[0.02em]">
Let&apos;s Start Building!
{t(I18nKey.LANDING$TITLE)}
</h1>
<p className="mx-4 text-sm flex flex-col gap-2">
OpenHands makes it easy to build and maintain software using a simple
prompt.{" "}
{t(I18nKey.LANDING$SUBTITLE)}{" "}
<span className="">
Not sure how to start?{" "}
{t(I18nKey.LANDING$START_HELP)}{" "}
<a
rel="noopener noreferrer"
target="_blank"
href="https://docs.all-hands.dev/modules/usage/getting-started"
className="text-hyperlink underline underline-offset-[3px]"
>
Read this
{t(I18nKey.LANDING$START_HELP_LINK)}
</a>
</span>
</p>

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