Compare commits

..

45 Commits

Author SHA1 Message Date
openhands
d67c4c5848 Fix terminal command echo issue (#6661) and update tests 2025-03-14 16:33:13 +00:00
openhands
36b378c561 Fix terminal command echo issue (#6661) 2025-03-14 16:10:41 +00:00
tofarr
ec763f8105 Fix for error where credits is accessed even when billing is disabled (#7250) 2025-03-14 15:10:54 +00:00
Ryan H. Tran
165c0cc42e Add doc for local runtime (#7234)
Co-authored-by: Xingyao Wang <xingyao@all-hands.dev>
2025-03-14 22:09:33 +08:00
dependabot[bot]
1b4f15235e chore(deps): bump the version-all group with 4 updates (#7241)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: openhands <openhands@all-hands.dev>
2025-03-13 22:15:47 +01:00
Calvin Smith
303b7ab180 (fix): Conditional imports resolved in SWE-bench eval script while multiprocessing enabled (#7244)
Co-authored-by: Calvin Smith <calvin@all-hands.dev>
2025-03-13 13:29:11 -06:00
Rohit Malhotra
78d185b102 [Feat]: Support Gitlab PAT (#7064)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-03-13 14:44:49 -04:00
Ryan H. Tran
300bfbdf2d Upgrade openhands-aci to 0.2.6 (#7233) 2025-03-14 02:10:59 +08:00
Xingyao Wang
e2f414bf26 chore: update doc for allhands doc (#7242)
Co-authored-by: mamoodi <mamoodiha@gmail.com>
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-03-14 00:32:17 +08:00
Calvin Smith
3b955dd9d5 (fix): Improve formatting of summarizing LLM inputs (#7239)
Co-authored-by: Calvin Smith <calvin@all-hands.dev>
2025-03-13 10:21:29 -06:00
sp.wack
f1eb1f59c3 hotfix(frontend): Fix overriding tailwind animation classes (#7243) 2025-03-13 16:03:54 +00:00
sp.wack
e1f6929d98 feat: saas new user modal (#7098)
Co-authored-by: Tim O'Farrell <tofarr@gmail.com>
Co-authored-by: openhands <openhands@all-hands.dev>
2025-03-13 19:15:57 +04:00
Engel Nyst
2a7f926591 Detect condensation loops at 10 repetitions, not 3 (#7237) 2025-03-13 14:32:01 +00:00
Xingyao Wang
b8daab721d Update agent message to use first-person perspective (#7197)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-03-13 04:39:24 +08:00
Robert Brennan
b3cac69121 remove old microagents (#6964)
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
Co-authored-by: openhands <openhands@all-hands.dev>
2025-03-12 16:18:19 -04:00
Boxuan Li
49a29c19cb Remove mention of non-existent config micro_agent_name (#7209) 2025-03-13 03:33:59 +08:00
Robert Brennan
7084b0238c Fix HTTPException not callable error in action_execution_server middleware (#7215)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-03-13 03:10:13 +08:00
Elena Chistova
38e866cde4 Fix official SWE-Bench docker image prefix (#7214) 2025-03-12 18:23:19 +00:00
sp.wack
6ec4bc74bf chore(frontend): Turn off query caching by default (#7222) 2025-03-12 17:52:11 +00:00
juanmichelini
b36deca265 Added link to paper in commit0 README (#7221) 2025-03-12 17:17:22 +00:00
Engel Nyst
09d73d96c8 Add troubleshooting note for GitHub token in microagent (#7208)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-03-12 04:45:58 +01:00
dependabot[bot]
966c11f205 chore(deps): bump the version-all group across 1 directory with 17 updates (#7203)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: openhands <openhands@all-hands.dev>
2025-03-12 00:15:52 +00:00
Robert Brennan
5128377baa remove llamaindex (#7151)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-03-11 22:28:56 +00:00
Engel Nyst
924acb182b Add Swift Linux Installation Microagent (#7195)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-03-11 19:45:50 +00:00
tofarr
8043612420 Feat user settings (#7190) 2025-03-11 12:57:44 -06:00
Calvin Smith
637a1d5c17 Adds unit test for the interaction between agent controller truncation and condenser (#7186)
Co-authored-by: Calvin Smith <calvin@all-hands.dev>
2025-03-11 09:57:15 -06:00
AutoLTX
5e521a4a6e Expose accumulate and llm_metric from eventstream (backend) (#7082)
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-03-11 12:18:08 +01:00
chuckbutkus
2cb5b91300 Change names to prepare for moving to keycloak User ID (#7178) 2025-03-10 19:23:57 -04:00
answer-huang
d6e601ea2e bugfix: selected_repository may includes more then two parts, could b… (#7114) 2025-03-10 19:47:33 +00:00
Rohit Malhotra
ac680e7688 [Fix]: Remove remaining hard coded refs to sessions store (#7176)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-03-10 15:07:01 -04:00
mamoodi
4b04f09035 Update Cloud docs (#7158)
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-03-10 14:00:06 -04:00
mamoodi
e99f503e0f Release 0.28.1 (#7170) 2025-03-10 13:22:34 -04:00
Rohit Malhotra
f51eb93d3e Add error logs to API response from GH Service (#7165) 2025-03-09 21:45:13 +00:00
mamoodi
7c2a98d1ce Change back the runtime binding address to 0.0.0.0 (#7161) 2025-03-09 11:37:10 -04:00
Ray Myers
dc9489ddcd Include metadata like session_id in logs (#7145) 2025-03-08 00:28:51 +01:00
sp.wack
83851c398d chore(frontend): Restyle toasts and replace all current instances with new one (#6854) 2025-03-08 01:23:50 +04:00
Xingyao Wang
a4908f9a75 [agent] system message + SWE-Bench instruction improvements (#7018) 2025-03-08 00:27:02 +08:00
Robert Brennan
366fd7ab8a Improve agent loop tracking and make concurrent limit configurable (#6945)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Tim O'Farrell <tofarr@gmail.com>
2025-03-07 09:19:50 -07:00
dependabot[bot]
318fcbcfc7 chore(deps): bump the version-all group across 1 directory with 21 updates (#7146)
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-03-07 15:58:54 +00:00
sp.wack
521492f8f9 chore: Prevent user from modifying basic LLM settings in saas mode (#7137) 2025-03-07 19:14:06 +04:00
mamoodi
71f6b0b4a9 Release 0.28.0 (#7127)
Co-authored-by: Xingyao Wang <xingyao@all-hands.dev>
2025-03-07 09:17:01 -05:00
Xingyao Wang
b146b63380 chore: temporary fix to get sonnet 3.7 working again (#7140) 2025-03-06 20:30:35 +00:00
Engel Nyst
22064d5555 Add condensation observation to history and detect its loop (#7132) 2025-03-06 18:52:45 +01:00
sp.wack
08735a9b72 Fix loading spinner (#7134) 2025-03-06 17:40:13 +00:00
tofarr
54f982589a Fix annoying Avatar flicker when loading (#7129) 2025-03-06 15:59:28 +00:00
181 changed files with 4476 additions and 5372 deletions

View File

@@ -10,12 +10,6 @@ updates:
pre-commit:
patterns:
- "pre-commit"
llama:
patterns:
- "llama*"
chromadb:
patterns:
- "chromadb"
browsergym:
patterns:
- "browsergym*"

View File

@@ -36,7 +36,7 @@ jobs:
python-version: '3.12'
cache: 'poetry'
- name: Install Python dependencies using Poetry
run: poetry install --without evaluation,llama-index
run: poetry install --without evaluation
- name: Build Environment
run: make build
- name: Run tests

View File

@@ -54,7 +54,7 @@ jobs:
Hi! I started running the integration tests on your PR. You will receive a comment with the results shortly.
- name: Install Python dependencies using Poetry
run: poetry install --without evaluation,llama-index
run: poetry install --without evaluation
- name: Configure config.toml for testing with Haiku
env:
@@ -117,68 +117,6 @@ jobs:
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
# -------------------------------------------------------------
# Run VisualBrowsingAgent tests for DeepSeek, limited to t05 and t06
- name: Wait a little bit (again)
run: sleep 5
@@ -213,7 +151,7 @@ jobs:
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/* integration_tests/DelegatorAgent/* integration_tests/VisualBrowsingAgent/* # Only include the actual result directories
tar -czvf ../../../integration_tests_${TIMESTAMP}.tar.gz integration_tests/CodeActAgent/* integration_tests/VisualBrowsingAgent/* # Only include the actual result directories
- name: Upload evaluation results as artifact
uses: actions/upload-artifact@v4
@@ -254,12 +192,6 @@ 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 }}
---
**Integration Tests Report VisualBrowsing (DeepSeek)**
${{ env.INTEGRATION_TEST_REPORT_VISUALBROWSING_DEEPSEEK }}

View File

@@ -44,11 +44,11 @@ jobs:
python-version: ${{ matrix.python-version }}
cache: 'poetry'
- name: Install Python dependencies using Poetry
run: poetry install --without evaluation,llama-index
run: poetry install --without evaluation
- name: Build Environment
run: make build
- name: Run Tests
run: poetry run pytest --forked -n auto --cov=openhands --cov-report=xml -svv ./tests/unit --ignore=tests/unit/test_long_term_memory.py
run: poetry run pytest --forked -n auto --cov=openhands --cov-report=xml -svv ./tests/unit
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
env:

View File

@@ -133,7 +133,7 @@ install-python-dependencies:
export HNSWLIB_NO_NATIVE=1; \
poetry run pip install chroma-hnswlib; \
fi
@poetry install --without llama-index
@poetry install
@if [ -f "/etc/manjaro-release" ]; then \
echo "$(BLUE)Detected Manjaro Linux. Installing Playwright dependencies...$(RESET)"; \
poetry run pip install playwright; \
@@ -265,35 +265,6 @@ setup-config-prompts:
@read -p "Enter your LLM base URL [mostly used for local LLMs, leave blank if not needed - example: http://localhost:5001/v1/]: " llm_base_url; \
if [[ ! -z "$$llm_base_url" ]]; then echo "base_url=\"$$llm_base_url\"" >> $(CONFIG_FILE).tmp; fi
@echo "Enter your LLM Embedding Model"; \
echo "Choices are:"; \
echo " - openai"; \
echo " - azureopenai"; \
echo " - Embeddings available only with OllamaEmbedding:"; \
echo " - llama2"; \
echo " - mxbai-embed-large"; \
echo " - nomic-embed-text"; \
echo " - all-minilm"; \
echo " - stable-code"; \
echo " - bge-m3"; \
echo " - bge-large"; \
echo " - paraphrase-multilingual"; \
echo " - snowflake-arctic-embed"; \
echo " - Leave blank to default to 'BAAI/bge-small-en-v1.5' via huggingface"; \
read -p "> " llm_embedding_model; \
echo "embedding_model=\"$$llm_embedding_model\"" >> $(CONFIG_FILE).tmp; \
if [ "$$llm_embedding_model" = "llama2" ] || [ "$$llm_embedding_model" = "mxbai-embed-large" ] || [ "$$llm_embedding_model" = "nomic-embed-text" ] || [ "$$llm_embedding_model" = "all-minilm" ] || [ "$$llm_embedding_model" = "stable-code" ]; then \
read -p "Enter the local model URL for the embedding model (will set llm.embedding_base_url): " llm_embedding_base_url; \
echo "embedding_base_url=\"$$llm_embedding_base_url\"" >> $(CONFIG_FILE).tmp; \
elif [ "$$llm_embedding_model" = "azureopenai" ]; then \
read -p "Enter the Azure endpoint URL (will overwrite llm.base_url): " llm_base_url; \
echo "base_url=\"$$llm_base_url\"" >> $(CONFIG_FILE).tmp; \
read -p "Enter the Azure LLM Embedding Deployment Name: " llm_embedding_deployment_name; \
echo "embedding_deployment_name=\"$$llm_embedding_deployment_name\"" >> $(CONFIG_FILE).tmp; \
read -p "Enter the Azure API Version: " llm_api_version; \
echo "api_version=\"$$llm_api_version\"" >> $(CONFIG_FILE).tmp; \
fi
# Develop in container
docker-dev:

View File

@@ -132,15 +132,6 @@ api_key = ""
# Custom LLM provider
#custom_llm_provider = ""
# Embedding API base URL
#embedding_base_url = ""
# Embedding deployment name
#embedding_deployment_name = ""
# Embedding model to use
embedding_model = "local"
# Maximum number of characters in an observation's content
#max_message_chars = 10000
@@ -227,9 +218,6 @@ codeact_enable_llm_editor = false
# whether the IPython tool is enabled
codeact_enable_jupyter = true
# Name of the micro agent to use for this agent
#micro_agent_name = ""
# Memory enabled
#memory_enabled = false

View File

@@ -26,7 +26,7 @@ RUN apt-get update -y \
COPY ./pyproject.toml ./poetry.lock ./
RUN touch README.md
RUN export POETRY_CACHE_DIR && poetry install --without evaluation,llama-index --no-root && rm -rf $POETRY_CACHE_DIR
RUN export POETRY_CACHE_DIR && poetry install --without evaluation --no-root && rm -rf $POETRY_CACHE_DIR
FROM python:3.12.3-slim AS openhands-app

View File

@@ -333,12 +333,6 @@ Pour les configurations de développement, vous pouvez également définir des c
Les options de configuration de l'agent sont définies dans les sections `[agent]` et `[agent.<agent_name>]` du fichier `config.toml`.
**Configuration du micro-agent**
- `micro_agent_name`
- Type : `str`
- Valeur par défaut : `""`
- Description : Nom du micro-agent à utiliser pour cet agent
**Configuration de la mémoire**
- `memory_enabled`
- Type : `bool`

View File

@@ -328,12 +328,6 @@ LLM(大语言模型)配置选项在 `config.toml` 文件的 `[llm]` 部分中定
Agent 配置选项在 `config.toml` 文件的 `[agent]``[agent.<agent_name>]` 部分中定义。
**Microagent 配置**
- `micro_agent_name`
- 类型: `str`
- 默认值: `""`
- 描述: 用于此 agent 的 micro agent 名称
**内存配置**
- `memory_enabled`
- 类型: `bool`

View File

@@ -4,18 +4,30 @@ The GitHub Resolver automates code fixes and provides intelligent assistance for
## Setup
The Cloud Github Resolver is available automatically when you
The Cloud GitHub Resolver is available automatically when you
[grant OpenHands Cloud repository access](./openhands-cloud.md#adding-repositories).
## Usage
After granting OpenHands Cloud repository access, you can use the Cloud GitHub Resolver on the issues and pull requests
on the repository.
### Issues
On your repository, label an issue with `openhands`. OpenHands will attempt to fix the issue.
On your repository, label an issue with `openhands`. OpenHands will:
1. Comment on the issue to let you know it is working on it.
- You can click on the link to track the progress on OpenHands Cloud.
2. Open a pull request if it determines that the issue has been successfully resolved.
3. Comment on the issue with a summary of the performed tasks and a link to the pull request.
### Pull Requests
In order to get OpenHands to work on pull requests, use `@openhands` in top level or single inline comments to:
To get OpenHands to work on pull requests, use `@openhands` in top level or inline comments to:
- Ask questions
- Request updates
- Get code explanations
OpenHands will:
1. Comment on the PR to let you know it is working on it.
2. Perform the task.

View File

@@ -12,14 +12,16 @@ instructions on how to access it.
After visiting OpenHands Cloud, you will be asked to connect with your GitHub account:
1. After reading and accepting the terms of service, click `Connect to GitHub`.
2. Review the permissions requested by OpenHands and then click `Authorize OpenHands by All Hands AI`.
2. Review the permissions requested by OpenHands and then click `Authorize OpenHands AI`.
- OpenHands will require some permissions from your GitHub account. To read more about these permissions,
you can click the `Learn more` link on the GitHub authorize page.
## Adding Repositories
## Repository Access
### Adding Repository Access
You can grant OpenHands specific repository access:
1. Under the `Select a GitHub project` dropdown, select `Add more repositories...`.
1. Click the `Select a GitHub project` dropdown, select `Add more repositories...`.
2. Select the organization, then choose the specific repositories to grant OpenHands access to.
- Openhands requests short-lived tokens (8-hour expiry) with these permissions:
- Actions: Read and write
@@ -34,6 +36,10 @@ You can grant OpenHands specific repository access:
- Repository access for a user is granted based on:
- Granted permission for the repository.
- User's GitHub permissions (owner/collaborator).
3. Click on `Install & Authorize`.
You can manage repository access any time by following the above workflow or visiting the Settings page and selecting
`Configure GitHub Repositories` under the `GitHub Settings` section.
### Modifying Repository Access
You can modify repository access at any time by:
* Using the same `Select a GitHub project > Add more repositories` workflow, or
* Visiting the Settings page and selecting `Configure GitHub Repositories` under the `GitHub Settings` section.

View File

@@ -197,21 +197,6 @@ For development setups, you can also define custom named LLM configurations. See
- Default: `""`
- Description: Custom LLM provider
### Embeddings
- `embedding_base_url`
- Type: `str`
- Default: `""`
- Description: Embedding API base URL
- `embedding_deployment_name`
- Type: `str`
- Default: `""`
- Description: Embedding deployment name
- `embedding_model`
- Type: `str`
- Default: `"local"`
- Description: Embedding model to use
### Message Handling
- `max_message_chars`
@@ -296,23 +281,6 @@ For development setups, you can also define custom named LLM configurations. See
The agent configuration options are defined in the `[agent]` and `[agent.<agent_name>]` sections of the `config.toml` file.
### Microagent Configuration
- `micro_agent_name`
- Type: `str`
- Default: `""`
- Description: Name of the micro agent to use for this agent
### Memory Configuration
- `memory_enabled`
- Type: `bool`
- Default: `false`
- Description: Whether long-term memory (embeddings) is enabled
- `memory_max_threads`
- Type: `int`
- Default: `3`
- Description: The maximum number of threads indexing at the same time for embeddings
### LLM Configuration
- `llm_config`
- Type: `str`
@@ -387,7 +355,7 @@ To use these with the docker command, pass in `-e SANDBOX_<option>`. Example: `-
- `runtime_binding_address`
- Type: `str`
- Default: `127.0.0.1`
- Default: `0.0.0.0`
- Description: The binding address for the runtime ports. It specifies which network interface on the host machine Docker should bind the runtime ports to.
### Linting and Plugins

View File

@@ -36,7 +36,7 @@ At this time, we will follow the following release process:
1. All people who contributed public feedback will receive an email describing the data release and being given an opportunity to opt out.
2. The person or people in charge of the data release will perform quality control of the data, removing low-quality feedback,
removing email submitter email addresses, and attempting to remove any sensitive information.
3. The data will be released publicly under the MIT license through commonly used sites such as github or Hugging Face.
3. The data will be released publicly under the MIT license through commonly used sites such as GitHub or Hugging Face.
### What if I want my data deleted?

View File

@@ -39,7 +39,7 @@ You can provide custom directions for OpenHands by following the [README for the
### Custom configurations
Github resolver will automatically check for valid [repository secrets](https://docs.github.com/en/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions?tool=webui#creating-secrets-for-a-repository) or [repository variables](https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/store-information-in-variables#creating-configuration-variables-for-a-repository) to customize its behavior.
GitHub resolver will automatically check for valid [repository secrets](https://docs.github.com/en/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions?tool=webui#creating-secrets-for-a-repository) or [repository variables](https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/store-information-in-variables#creating-configuration-variables-for-a-repository) to customize its behavior.
The customization options you can set are:
| **Attribute name** | **Type** | **Purpose** | **Example** |

View File

@@ -76,7 +76,7 @@ You'll find OpenHands running at http://localhost:3000!
You can also [connect OpenHands to your local filesystem](https://docs.all-hands.dev/modules/usage/runtimes#connecting-to-your-filesystem),
run OpenHands in a scriptable [headless mode](https://docs.all-hands.dev/modules/usage/how-to/headless-mode),
interact with it via a [friendly CLI](https://docs.all-hands.dev/modules/usage/how-to/cli-mode),
or run it on tagged issues with [a github action](https://docs.all-hands.dev/modules/usage/how-to/github-action).
or run it on tagged issues with [a GitHub action](https://docs.all-hands.dev/modules/usage/how-to/github-action).
## Setup

View File

@@ -31,17 +31,11 @@ You will need your ChatGPT deployment name which can be found on the deployments
- `Base URL` to your Azure API Base URL (e.g. `https://example-endpoint.openai.azure.com`)
- `API Key` to your Azure API key
## Embeddings
OpenHands uses llama-index for embeddings. You can find their documentation on Azure [here](https://docs.llamaindex.ai/en/stable/api_reference/embeddings/azure_openai/).
### Azure OpenAI Configuration
When running OpenHands, set the following environment variables using `-e` in the
When running OpenHands, set the following environment variable using `-e` in the
[docker run command](/modules/usage/installation#start-the-app):
```
LLM_EMBEDDING_MODEL="azureopenai"
LLM_EMBEDDING_DEPLOYMENT_NAME="<your-embedding-deployment-name>" # e.g. "TextEmbedding...<etc>"
LLM_API_VERSION="<api-version>" # e.g. "2024-02-15-preview"
```

View File

@@ -10,6 +10,9 @@ We also support "remote" runtimes, which are typically managed by third-parties.
They can make setup a bit simpler and more scalable, especially
if you're running many OpenHands conversations in parallel (e.g. to do evaluation).
Additionally, we provide a "local" runtime that runs directly on your machine without Docker,
which can be useful in controlled environments like CI pipelines.
## Docker Runtime
This is the default Runtime that's used when you start OpenHands. You might notice
some flags being passed to `docker run` that make this possible:
@@ -56,11 +59,12 @@ any files that are mounted into its workspace.
This setup can cause some issues with file permissions (hence the `SANDBOX_USER_ID` variable)
but seems to work well on most systems.
## All Hands Runtime
The All Hands Runtime is currently in beta. You can request access by joining
the #remote-runtime-limited-beta channel on Slack ([see the README](https://github.com/All-Hands-AI/OpenHands?tab=readme-ov-file#-how-to-join-the-community) for an invite).
## OpenHands Remote Runtime
To use the All Hands Runtime, set the following environment variables when
OpenHands Remote Runtime is currently in beta (read [here](https://runtime.all-hands.dev/) for more details), it allows you to launch runtimes in parallel in the cloud.
Fill out [this form](https://docs.google.com/forms/d/e/1FAIpQLSckVz_JFwg2_mOxNZjCtr7aoBFI2Mwdan3f75J_TrdMS1JV2g/viewform) to apply if you want to try this out!
To use the OpenHands Remote Runtime, set the following environment variables when
starting OpenHands:
```bash
@@ -117,3 +121,66 @@ bash -i <(curl -sL https://get.daytona.io/openhands)
Once executed, OpenHands should be running locally and ready for use.
For more details and manual initialization, view the entire [README.md](https://github.com/All-Hands-AI/OpenHands/blob/main/openhands/runtime/impl/daytona/README.md)
## Local Runtime
The Local Runtime allows the OpenHands agent to execute actions directly on your local machine without using Docker. This runtime is primarily intended for controlled environments like CI pipelines or testing scenarios where Docker is not available.
:::caution
**Security Warning**: The Local Runtime runs without any sandbox isolation. The agent can directly access and modify files on your machine. Only use this runtime in controlled environments or when you fully understand the security implications.
:::
### Prerequisites
Before using the Local Runtime, ensure you have the following dependencies installed:
1. You have followed the [Development setup instructions](https://github.com/All-Hands-AI/OpenHands/blob/main/Development.md).
2. tmux is available on your system.
### Configuration
To use the Local Runtime, besides required configurations like the model, API key, you'll need to set the following options via environment variables or the [config.toml file](https://github.com/All-Hands-AI/OpenHands/blob/main/config.template.toml) when starting OpenHands:
- Via environment variables:
```bash
# Required
export RUNTIME=local
# Optional but recommended
export WORKSPACE_BASE=/path/to/your/workspace
```
- Via `config.toml`:
```toml
[core]
runtime = "local"
workspace_base = "/path/to/your/workspace"
```
If `WORKSPACE_BASE` is not set, the runtime will create a temporary directory for the agent to work in.
### Example Usage
Here's an example of how to start OpenHands with the Local Runtime in Headless Mode:
```bash
# Set the runtime type to local
export RUNTIME=local
# Optionally set a workspace directory
export WORKSPACE_BASE=/path/to/your/project
# Start OpenHands
poetry run python -m openhands.core.main -t "write a bash script that prints hi"
```
### Use Cases
The Local Runtime is particularly useful for:
- CI/CD pipelines where Docker is not available.
- Testing and development of OpenHands itself.
- Environments where container usage is restricted.
- Scenarios where direct file system access is required.

View File

@@ -1,6 +1,6 @@
# Commit0 Evaluation with OpenHands
This folder contains the evaluation harness that we built on top of the original [Commit0](https://commit-0.github.io/) ([paper](TBD)).
This folder contains the evaluation harness that we built on top of the original [Commit0](https://commit-0.github.io/) ([paper](https://arxiv.org/abs/2412.01769v1)).
The evaluation consists of three steps:

View File

@@ -3,7 +3,9 @@ import os
import subprocess
import tempfile
import time
from dataclasses import dataclass
from functools import partial
from typing import Callable
import pandas as pd
from tqdm import tqdm
@@ -91,12 +93,22 @@ def get_config(metadata: EvalMetadata, instance: pd.Series) -> AppConfig:
return config
@dataclass
class ConditionalImports:
"""We instantiate the values in this dataclass differently if we're evaluating SWE-bench or SWE-Gym."""
get_eval_report: Callable
APPLY_PATCH_FAIL: str
APPLY_PATCH_PASS: str
def process_instance(
instance: pd.Series,
metadata: EvalMetadata,
reset_logger: bool = True,
log_dir: str | None = None,
runtime_failure_count: int = 0,
conditional_imports: ConditionalImports | None = None,
) -> EvalOutput:
"""
Evaluate agent performance on a SWE-bench problem instance.
@@ -108,9 +120,18 @@ def process_instance(
log_dir (str | None, default=None): Path to directory where log files will be written. Must
be provided if `reset_logger` is set.
conditional_imports: A dataclass containing values that are imported differently based on
whether we're evaluating SWE-bench or SWE-Gym.
Raises:
AssertionError: if the `reset_logger` flag is set without a provided log directory.
AssertionError: if `conditional_imports` is not provided.
"""
assert (
conditional_imports is not None
), 'conditional_imports must be provided to run process_instance using multiprocessing'
# Setup the logger properly, so you can run multi-processing to parallelize the evaluation
if reset_logger:
assert (
@@ -124,7 +145,7 @@ def process_instance(
config = get_config(metadata, instance)
instance_id = instance.instance_id
model_patch = instance['model_patch']
test_spec: TestSpec = instance['test_spec']
test_spec = instance['test_spec']
logger.info(f'Starting evaluation for instance {instance_id}.')
if 'test_result' not in instance.keys():
@@ -196,7 +217,9 @@ def process_instance(
instance['test_result']['apply_patch_output'] = apply_patch_output
if 'APPLY_PATCH_FAIL' in apply_patch_output:
logger.info(f'[{instance_id}] {APPLY_PATCH_FAIL}:\n{apply_patch_output}')
logger.info(
f'[{instance_id}] {conditional_imports.APPLY_PATCH_FAIL}:\n{apply_patch_output}'
)
instance['test_result']['report']['failed_apply_patch'] = True
return EvalOutput(
@@ -205,7 +228,9 @@ def process_instance(
metadata=metadata,
)
elif 'APPLY_PATCH_PASS' in apply_patch_output:
logger.info(f'[{instance_id}] {APPLY_PATCH_PASS}:\n{apply_patch_output}')
logger.info(
f'[{instance_id}] {conditional_imports.APPLY_PATCH_PASS}:\n{apply_patch_output}'
)
# Run eval script in background and save output to log file
log_file = '/tmp/eval_output.log'
@@ -271,7 +296,7 @@ def process_instance(
with open(test_output_path, 'w') as f:
f.write(test_output)
try:
_report = get_eval_report(
_report = conditional_imports.get_eval_report(
test_spec=test_spec,
prediction={
'model_patch': model_patch,
@@ -345,7 +370,6 @@ if __name__ == '__main__':
)
from swegym.harness.test_spec import (
SWEbenchInstance,
TestSpec,
make_test_spec,
)
from swegym.harness.utils import load_swebench_dataset
@@ -357,7 +381,6 @@ if __name__ == '__main__':
)
from swebench.harness.test_spec.test_spec import (
SWEbenchInstance,
TestSpec,
make_test_spec,
)
from swebench.harness.utils import load_swebench_dataset
@@ -445,7 +468,15 @@ if __name__ == '__main__':
# The evaluation harness constrains the signature of `process_instance_func` but we need to
# pass extra information. Build a new function object to avoid issues with multiprocessing.
process_instance_func = partial(
process_instance, log_dir=output_file.replace('.jsonl', '.logs')
process_instance,
log_dir=output_file.replace('.jsonl', '.logs'),
# We have to explicitly pass these imports to the process_instance function, otherwise
# they won't be available in the multiprocessing context.
conditional_imports=ConditionalImports(
get_eval_report=get_eval_report,
APPLY_PATCH_FAIL=APPLY_PATCH_FAIL,
APPLY_PATCH_PASS=APPLY_PATCH_PASS,
),
)
run_evaluation(

View File

@@ -58,62 +58,89 @@ def _get_swebench_workspace_dir_name(instance: pd.Series) -> str:
def get_instruction(instance: pd.Series, metadata: EvalMetadata):
workspace_dir_name = _get_swebench_workspace_dir_name(instance)
# Instruction based on Anthropic's official trajectory
# https://github.com/eschluntz/swe-bench-experiments/tree/main/evaluation/verified/20241022_tools_claude-3-5-sonnet-updated/trajs
instruction = (
'<uploaded_files>\n'
f'/workspace/{workspace_dir_name}\n'
'</uploaded_files>\n'
f"I've uploaded a python code repository in the directory {workspace_dir_name}. Consider the following issue description:\n\n"
f'<issue_description>\n'
f'{instance.problem_statement}\n'
'</issue_description>\n\n'
'Can you help me implement the necessary changes to the repository so that the requirements specified in the <issue_description> are met?\n'
"I've already taken care of all changes to any of the test files described in the <issue_description>. This means you DON'T have to modify the testing logic or any of the tests in any way!\n"
"Also the development Python environment is already set up for you (i.e., all dependencies already installed), so you don't need to install other packages.\n"
'Your task is to make the minimal changes to non-test files in the /workspace directory to ensure the <issue_description> is satisfied.\n'
'Follow these steps to resolve the issue:\n'
'1. As a first step, it might be a good idea to explore the repo to familiarize yourself with its structure.\n'
'2. Create a script to reproduce the error and execute it with `python <filename.py>` using the BashTool, to confirm the error\n'
'3. Edit the sourcecode of the repo to resolve the issue\n'
'4. Rerun your reproduce script and confirm that the error is fixed!\n'
'5. Think about edgecases, add comprehensive tests for them in your reproduce script, and run them to make sure your fix handles them as well\n'
f'6. Once you are done with the initial implementation, please carefully re-read the problem description and check the difference between the current code and the base commit {instance["base_commit"]}. Do you think that the issue has been completely and comprehensively solved? Write tests to check the correctness of the solution, specifically focusing on tests that may point out any remaining problems that are not yet solved. Run all of the tests in the repo and check if any of them fail, and if they do fix the code. Repeat this process of carefully reading the problem description and current implementation, testing, and fixing any problems until you are confident that the current implementation is correct. Find and run any tests in the repo that are related to:\n'
' - The issue you are fixing\n'
' - The files you modified\n'
' - The functions you changed\n'
' Make sure all these tests pass with your changes.\n'
"Your thinking should be thorough and so it's fine if it's very long.\n"
)
instruction = f"""
<uploaded_files>
/workspace/{workspace_dir_name}
</uploaded_files>
I've uploaded a python code repository in the directory {workspace_dir_name}. Consider the following issue description:
<issue_description>
{instance.problem_statement}
</issue_description>
Can you help me implement the necessary changes to the repository so that the requirements specified in the <issue_description> are met?
I've already taken care of all changes to any of the test files described in the <issue_description>. This means you DON'T have to modify the testing logic or any of the tests in any way!
Also the development Python environment is already set up for you (i.e., all dependencies already installed), so you don't need to install other packages.
Your task is to make the minimal changes to non-test files in the /workspace/{workspace_dir_name} directory to ensure the <issue_description> is satisfied.
Follow these steps to resolve the issue:
1. EXPLORATION: First, thoroughly explore the repository structure using tools like `find` and `grep`.
- Identify all files mentioned in the problem statement
- Locate where the issue occurs in the codebase
- Understand the surrounding context and dependencies
- Use `grep` to search for relevant functions, classes, or error messages
2. ANALYSIS: Based on your exploration, think carefully about the problem and propose 2-5 possible approaches to fix the issue.
- Analyze the root cause of the problem
- Consider trade-offs between different solutions
- Select the most promising approach and explain your reasoning
3. TEST CREATION: Before implementing any fix, create a script to reproduce and verify the issue.
- Look at existing test files in the repository to understand the test format/structure
- Create a minimal reproduction script that demonstrates the issue
- Run your script to confirm the error exists
4. IMPLEMENTATION: Edit the source code to implement your chosen solution.
- Make minimal, focused changes to fix the issue
5. VERIFICATION: Test your implementation thoroughly.
- Run your reproduction script to verify the fix works
- Add edge cases to your test script to ensure comprehensive coverage
- Run existing tests related to the modified code to ensure you haven't broken anything
6. FINAL REVIEW: Carefully re-read the problem description and compare your changes with the base commit {instance["base_commit"]}.
- Ensure you've fully addressed all requirements
- Run any tests in the repository related to:
* The issue you are fixing
* The files you modified
* The functions you changed
- If any tests fail, revise your implementation until all tests pass
Be thorough in your exploration, testing, and reasoning. It's fine if your thinking process is lengthy - quality and completeness are more important than brevity.
"""
if RUN_WITH_BROWSING:
instruction += (
'<IMPORTANT!>\n'
'You SHOULD NEVER attempt to browse the web. '
'</IMPORTANT!>\n'
)
instruction += """
<IMPORTANT!>
You SHOULD NEVER attempt to browse the web.
</IMPORTANT!>
"""
return instruction
# TODO: migrate all swe-bench docker to ghcr.io/openhands
DOCKER_IMAGE_PREFIX = os.environ.get('EVAL_DOCKER_IMAGE_PREFIX', 'docker.io/xingyaoww/')
logger.info(f'Using docker image prefix: {DOCKER_IMAGE_PREFIX}')
DEFAULT_DOCKER_IMAGE_PREFIX = os.environ.get('EVAL_DOCKER_IMAGE_PREFIX', 'docker.io/xingyaoww/')
logger.info(f'Default docker image prefix: {DEFAULT_DOCKER_IMAGE_PREFIX}')
def get_instance_docker_image(instance_id: str, official_image: bool = False) -> str:
if official_image:
# Official SWE-Bench image
# swebench/sweb.eval.x86_64.django_1776_django-11333:v1
docker_image_prefix = 'docker.io/swebench/'
repo, name = instance_id.split('__')
image_name = f'sweb.eval.x86_64.{repo}_1776_{name}:latest'
logger.warning(f'Using official SWE-Bench image: {image_name}')
else:
# OpenHands version of the image
docker_image_prefix = DEFAULT_DOCKER_IMAGE_PREFIX
image_name = 'sweb.eval.x86_64.' + instance_id
image_name = image_name.replace(
'__', '_s_'
) # to comply with docker image naming convention
return (DOCKER_IMAGE_PREFIX.rstrip('/') + '/' + image_name).lower()
return (docker_image_prefix.rstrip('/') + '/' + image_name).lower()
def get_config(

View File

@@ -4,8 +4,10 @@ import userEvent from "@testing-library/user-event";
import { afterEach, beforeEach, describe, expect, it, test, vi } from "vitest";
import OpenHands from "#/api/open-hands";
import { PaymentForm } from "#/components/features/payment/payment-form";
import * as featureFlags from "#/utils/feature-flags";
describe("PaymentForm", () => {
const billingSettingsSpy = vi.spyOn(featureFlags, "BILLING_SETTINGS");
const getBalanceSpy = vi.spyOn(OpenHands, "getBalance");
const createCheckoutSessionSpy = vi.spyOn(OpenHands, "createCheckoutSession");
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
@@ -26,6 +28,7 @@ describe("PaymentForm", () => {
GITHUB_CLIENT_ID: "123",
POSTHOG_CLIENT_KEY: "456",
});
billingSettingsSpy.mockReturnValue(true);
});
afterEach(() => {

View File

@@ -72,8 +72,10 @@ describe("useTerminal", () => {
wrapper: Wrapper,
});
expect(mockTerminal.writeln).toHaveBeenNthCalledWith(1, "echo hello");
expect(mockTerminal.writeln).toHaveBeenNthCalledWith(2, "hello");
// Input commands should be displayed
expect(mockTerminal.writeln).toHaveBeenCalledWith("echo hello");
// Output commands should be displayed
expect(mockTerminal.writeln).toHaveBeenCalledWith("hello");
});
it("should hide secrets in the terminal", () => {
@@ -97,13 +99,31 @@ describe("useTerminal", () => {
},
);
// BUG: `vi.clearAllMocks()` does not clear the number of calls
// therefore, we need to assume the order of the calls based
// on the test order
expect(mockTerminal.writeln).toHaveBeenNthCalledWith(
3,
// Input command should be displayed with secrets masked
expect(mockTerminal.writeln).toHaveBeenCalledWith(
`export GITHUB_TOKEN=${"*".repeat(10)},${"*".repeat(10)},${"*".repeat(10)}`,
);
expect(mockTerminal.writeln).toHaveBeenNthCalledWith(4, "*".repeat(10));
// Output command should be displayed with secrets masked
expect(mockTerminal.writeln).toHaveBeenCalledWith("*".repeat(10));
});
it("should prevent duplicate command display", () => {
const inputCommand = "ls -la";
const commands: Command[] = [
{ content: inputCommand, type: "input" },
{ content: `${inputCommand}\nfile1.txt\nfile2.txt`, type: "output" },
];
render(<TestTerminalComponent commands={commands} secrets={[]} />, {
wrapper: Wrapper,
});
// Input command should be displayed
expect(mockTerminal.writeln).toHaveBeenCalledWith(inputCommand);
// Output should not be displayed since it starts with the input command
// This prevents the duplicate display of the command
expect(mockTerminal.writeln).not.toHaveBeenCalledWith(`${inputCommand}\nfile1.txt\nfile2.txt`);
});
});

View File

@@ -2,11 +2,13 @@ import { createRoutesStub } from "react-router";
import { afterEach, beforeAll, describe, expect, it, vi } from "vitest";
import { renderWithProviders } from "test-utils";
import { screen, waitFor } from "@testing-library/react";
import toast from "react-hot-toast";
import App from "#/routes/_oh.app/route";
import OpenHands from "#/api/open-hands";
import * as CustomToast from "#/utils/custom-toast-handlers";
describe("App", () => {
const errorToastSpy = vi.spyOn(CustomToast, "displayErrorToast");
const RouteStub = createRoutesStub([
{ Component: App, path: "/conversation/:conversationId" },
]);
@@ -34,26 +36,19 @@ describe("App", () => {
await screen.findByTestId("app-route");
});
it(
"should call endSession if the user does not have permission to view conversation",
async () => {
const errorToastSpy = vi.spyOn(toast, "error");
const getConversationSpy = vi.spyOn(OpenHands, "getConversation");
it("should call endSession if the user does not have permission to view conversation", async () => {
const getConversationSpy = vi.spyOn(OpenHands, "getConversation");
getConversationSpy.mockResolvedValue(null);
renderWithProviders(
<RouteStub initialEntries={["/conversation/9999"]} />,
);
getConversationSpy.mockResolvedValue(null);
renderWithProviders(<RouteStub initialEntries={["/conversation/9999"]} />);
await waitFor(() => {
expect(endSessionMock).toHaveBeenCalledOnce();
expect(errorToastSpy).toHaveBeenCalledOnce();
});
},
);
await waitFor(() => {
expect(endSessionMock).toHaveBeenCalledOnce();
expect(errorToastSpy).toHaveBeenCalledOnce();
});
});
it("should not call endSession if the user has permission", async () => {
const errorToastSpy = vi.spyOn(toast, "error");
const getConversationSpy = vi.spyOn(OpenHands, "getConversation");
getConversationSpy.mockResolvedValue({

View File

@@ -8,6 +8,7 @@ import MainApp from "#/routes/_oh/route";
import SettingsScreen from "#/routes/settings";
import Home from "#/routes/_oh._index/route";
import OpenHands from "#/api/open-hands";
import * as FeatureFlags from "#/utils/feature-flags";
const createAxiosNotFoundErrorObject = () =>
new AxiosError(
@@ -25,32 +26,32 @@ const createAxiosNotFoundErrorObject = () =>
},
);
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
const RouterStub = createRoutesStub([
{
// layout route
Component: MainApp,
path: "/",
children: [
{
// home route
Component: Home,
path: "/",
},
{
Component: SettingsScreen,
path: "/settings",
},
],
},
]);
afterEach(() => {
vi.clearAllMocks();
});
describe("Home Screen", () => {
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
const RouterStub = createRoutesStub([
{
// layout route
Component: MainApp,
path: "/",
children: [
{
// home route
Component: Home,
path: "/",
},
{
Component: SettingsScreen,
path: "/settings",
},
],
},
]);
afterEach(() => {
vi.clearAllMocks();
});
it("should render the home screen", () => {
renderWithProviders(<RouterStub initialEntries={["/"]} />);
});
@@ -77,41 +78,82 @@ describe("Home Screen", () => {
const settingsScreen = await screen.findByTestId("settings-screen");
expect(settingsScreen).toBeInTheDocument();
});
});
describe("Settings 404", () => {
it("should open the settings modal if GET /settings fails with a 404", async () => {
const error = createAxiosNotFoundErrorObject();
getSettingsSpy.mockRejectedValue(error);
describe("Settings 404", () => {
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
renderWithProviders(<RouterStub initialEntries={["/"]} />);
it("should open the settings modal if GET /settings fails with a 404", async () => {
const error = createAxiosNotFoundErrorObject();
getSettingsSpy.mockRejectedValue(error);
const settingsModal = await screen.findByTestId("ai-config-modal");
expect(settingsModal).toBeInTheDocument();
});
renderWithProviders(<RouterStub initialEntries={["/"]} />);
it("should navigate to the settings screen when clicking the advanced settings button", async () => {
const error = createAxiosNotFoundErrorObject();
getSettingsSpy.mockRejectedValue(error);
const settingsModal = await screen.findByTestId("ai-config-modal");
expect(settingsModal).toBeInTheDocument();
});
const user = userEvent.setup();
renderWithProviders(<RouterStub initialEntries={["/"]} />);
it("should navigate to the settings screen when clicking the advanced settings button", async () => {
const error = createAxiosNotFoundErrorObject();
getSettingsSpy.mockRejectedValue(error);
const settingsScreen = screen.queryByTestId("settings-screen");
expect(settingsScreen).not.toBeInTheDocument();
const user = userEvent.setup();
renderWithProviders(<RouterStub initialEntries={["/"]} />);
const settingsModal = await screen.findByTestId("ai-config-modal");
expect(settingsModal).toBeInTheDocument();
const settingsScreen = screen.queryByTestId("settings-screen");
expect(settingsScreen).not.toBeInTheDocument();
const advancedSettingsButton = await screen.findByTestId(
"advanced-settings-link",
);
await user.click(advancedSettingsButton);
const settingsModal = await screen.findByTestId("ai-config-modal");
expect(settingsModal).toBeInTheDocument();
const settingsScreenAfter = await screen.findByTestId("settings-screen");
expect(settingsScreenAfter).toBeInTheDocument();
const advancedSettingsButton = await screen.findByTestId(
"advanced-settings-link",
);
await user.click(advancedSettingsButton);
const settingsModalAfter = screen.queryByTestId("ai-config-modal");
expect(settingsModalAfter).not.toBeInTheDocument();
});
const settingsScreenAfter = await screen.findByTestId("settings-screen");
expect(settingsScreenAfter).toBeInTheDocument();
const settingsModalAfter = screen.queryByTestId("ai-config-modal");
expect(settingsModalAfter).not.toBeInTheDocument();
});
it("should not open the settings modal if GET /settings fails but is SaaS mode", async () => {
// TODO: Remove HIDE_LLM_SETTINGS check once released
vi.spyOn(FeatureFlags, "HIDE_LLM_SETTINGS").mockReturnValue(true);
// @ts-expect-error - we only need APP_MODE for this test
getConfigSpy.mockResolvedValue({ APP_MODE: "saas" });
const error = createAxiosNotFoundErrorObject();
getSettingsSpy.mockRejectedValue(error);
renderWithProviders(<RouterStub initialEntries={["/"]} />);
// small hack to wait for the modal to not appear
await expect(
screen.findByTestId("ai-config-modal", {}, { timeout: 1000 }),
).rejects.toThrow();
});
});
describe("Setup Payment modal", () => {
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
afterEach(() => {
vi.resetAllMocks();
});
it("should only render if SaaS mode and is new user", async () => {
// @ts-expect-error - we only need the APP_MODE for this test
getConfigSpy.mockResolvedValue({
APP_MODE: "saas",
});
vi.spyOn(FeatureFlags, "BILLING_SETTINGS").mockReturnValue(true);
const error = createAxiosNotFoundErrorObject();
getSettingsSpy.mockRejectedValue(error);
renderWithProviders(<RouterStub initialEntries={["/"]} />);
const setupPaymentModal = await screen.findByTestId("proceed-to-stripe-button");
expect(setupPaymentModal).toBeInTheDocument();
});
});

View File

@@ -1,6 +1,15 @@
import { render, screen, waitFor, within } from "@testing-library/react";
import { createRoutesStub } from "react-router";
import { afterEach, describe, expect, it, test, vi } from "vitest";
import {
afterEach,
beforeAll,
beforeEach,
describe,
expect,
it,
test,
vi,
} from "vitest";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import userEvent, { UserEvent } from "@testing-library/user-event";
import OpenHands from "#/api/open-hands";
@@ -11,6 +20,7 @@ import { MOCK_DEFAULT_USER_SETTINGS } from "#/mocks/handlers";
import { PostApiSettings } from "#/types/settings";
import * as ConsentHandlers from "#/utils/handle-capture-consent";
import AccountSettings from "#/routes/account-settings";
import * as FeatureFlags from "#/utils/feature-flags";
const toggleAdvancedSettings = async (user: UserEvent) => {
const advancedSwitch = await screen.findByTestId("advanced-settings-switch");
@@ -29,6 +39,11 @@ describe("Settings Screen", () => {
useAppLogout: vi.fn().mockReturnValue({ handleLogout: handleLogoutMock }),
}));
beforeAll(() => {
// TODO: Remove this once we release
vi.spyOn(FeatureFlags, "HIDE_LLM_SETTINGS").mockReturnValue(true);
});
afterEach(() => {
vi.clearAllMocks();
});
@@ -67,6 +82,14 @@ describe("Settings Screen", () => {
});
describe("Account Settings", () => {
beforeEach(() => {
getConfigSpy.mockResolvedValue({
APP_MODE: "oss",
GITHUB_CLIENT_ID: "123",
POSTHOG_CLIENT_KEY: "456",
});
});
it("should render the account settings", async () => {
renderSettingsScreen();
@@ -280,6 +303,14 @@ describe("Settings Screen", () => {
});
describe("LLM Settings", () => {
beforeEach(() => {
getConfigSpy.mockResolvedValue({
APP_MODE: "oss",
GITHUB_CLIENT_ID: "123",
POSTHOG_CLIENT_KEY: "456",
});
});
it("should render the basic LLM settings by default", async () => {
renderSettingsScreen();
@@ -427,8 +458,7 @@ describe("Settings Screen", () => {
expect(input).not.toBeInTheDocument();
});
it("should render the runtime settings input if SaaS mode", async () => {
const user = userEvent.setup();
it.skip("should render the runtime settings input if SaaS mode", async () => {
getConfigSpy.mockResolvedValue({
APP_MODE: "saas",
GITHUB_CLIENT_ID: "123",
@@ -436,12 +466,10 @@ describe("Settings Screen", () => {
});
renderSettingsScreen();
await toggleAdvancedSettings(user);
screen.getByTestId("runtime-settings-input");
await screen.findByTestId("runtime-settings-input");
});
it("should set the default runtime setting set", async () => {
it.skip("should set the default runtime setting set", async () => {
getConfigSpy.mockResolvedValue({
APP_MODE: "saas",
GITHUB_CLIENT_ID: "123",
@@ -455,13 +483,11 @@ describe("Settings Screen", () => {
renderSettingsScreen();
await toggleAdvancedSettings(userEvent.setup());
const input = await screen.findByTestId("runtime-settings-input");
expect(input).toHaveValue("1x (2 core, 8G)");
});
it("should always have the runtime input disabled", async () => {
it.skip("should always have the runtime input disabled", async () => {
getConfigSpy.mockResolvedValue({
APP_MODE: "saas",
GITHUB_CLIENT_ID: "123",
@@ -470,8 +496,6 @@ describe("Settings Screen", () => {
renderSettingsScreen();
await toggleAdvancedSettings(userEvent.setup());
const input = await screen.findByTestId("runtime-settings-input");
expect(input).toBeDisabled();
});
@@ -490,8 +514,6 @@ describe("Settings Screen", () => {
renderSettingsScreen();
await toggleAdvancedSettings(user);
const input = await screen.findByTestId("runtime-settings-input");
await user.click(input);
@@ -699,7 +721,7 @@ describe("Settings Screen", () => {
expect(saveSettingsSpy).toHaveBeenCalledWith(
expect.objectContaining({
llm_api_key: "", // empty because it's not set previously
github_token: undefined,
provider_tokens: undefined,
language: "no",
}),
);
@@ -736,7 +758,7 @@ describe("Settings Screen", () => {
expect(saveSettingsSpy).toHaveBeenCalledWith(
expect.objectContaining({
github_token: undefined,
provider_tokens: undefined,
llm_api_key: "", // empty because it's not set previously
llm_model: "openai/gpt-4o",
}),
@@ -779,7 +801,7 @@ describe("Settings Screen", () => {
expect(saveSettingsSpy).toHaveBeenCalledWith({
...mockCopy,
github_token: undefined, // not set
provider_tokens: undefined, // not set
llm_api_key: "", // reset as well
});
expect(screen.queryByTestId("reset-modal")).not.toBeInTheDocument();
@@ -953,4 +975,104 @@ describe("Settings Screen", () => {
);
});
});
describe("SaaS mode", () => {
beforeEach(() => {
getConfigSpy.mockResolvedValue({
APP_MODE: "saas",
GITHUB_CLIENT_ID: "123",
POSTHOG_CLIENT_KEY: "456",
});
});
it("should hide the entire LLM section", async () => {
renderSettingsScreen();
await waitFor(() => {
screen.getByTestId("account-settings-form"); // wait for the account settings form to render
const llmSection = screen.queryByTestId("llm-settings-section");
expect(llmSection).not.toBeInTheDocument();
});
});
it("should not render LLM Provider, LLM Model, and LLM API Key inputs", async () => {
renderSettingsScreen();
await waitFor(() => {
screen.getByTestId("account-settings-form"); // wait for the account settings form to render
const providerInput = screen.queryByTestId("llm-provider-input");
const modelInput = screen.queryByTestId("llm-model-input");
const apiKeyInput = screen.queryByTestId("llm-api-key-input");
const llmApiKeyHelpAnchor = screen.queryByTestId(
"llm-api-key-help-anchor",
);
const advancedSettingsSwitch = screen.queryByTestId(
"advanced-settings-switch",
);
expect(providerInput).not.toBeInTheDocument();
expect(modelInput).not.toBeInTheDocument();
expect(apiKeyInput).not.toBeInTheDocument();
expect(llmApiKeyHelpAnchor).not.toBeInTheDocument();
expect(advancedSettingsSwitch).not.toBeInTheDocument();
});
});
// We might want to bring this back if users wish to set advanced settings
it.skip("should render advanced settings by default", async () => {
renderSettingsScreen();
await waitFor(() => {
screen.getByTestId("account-settings-form"); // wait for the account settings form to render
const agentInput = screen.getByTestId("agent-input"); // this is only rendered in advanced mode
expect(agentInput).toBeInTheDocument();
// we should not allow the user to change these fields
const customModelInput = screen.queryByTestId("llm-custom-model-input");
const baseUrlInput = screen.queryByTestId("base-url-input");
expect(customModelInput).not.toBeInTheDocument();
expect(baseUrlInput).not.toBeInTheDocument();
});
});
it("should not submit the unwanted fields when saving", async () => {
const user = userEvent.setup();
renderSettingsScreen();
const saveButton = await screen.findByText("Save Changes");
await user.click(saveButton);
expect(saveSettingsSpy).toHaveBeenCalledWith(
expect.objectContaining({
llm_api_key: undefined,
llm_base_url: undefined,
llm_model: undefined,
}),
);
});
it("should not submit the unwanted fields when resetting", async () => {
const user = userEvent.setup();
renderSettingsScreen();
const resetButton = await screen.findByText("Reset to defaults");
await user.click(resetButton);
const modal = await screen.findByTestId("reset-modal");
const confirmButton = within(modal).getByText("Reset");
await user.click(confirmButton);
expect(saveSettingsSpy).toHaveBeenCalledWith(
expect.objectContaining({
llm_api_key: undefined,
llm_base_url: undefined,
llm_model: undefined,
}),
);
});
});
});

View File

@@ -1,7 +1,9 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { handleStatusMessage } from "#/services/actions";
import { handleStatusMessage, handleActionMessage } from "#/services/actions";
import store from "#/store";
import { trackError } from "#/utils/error-handler";
import ActionType from "#/types/action-type";
import { ActionMessage } from "#/types/message";
// Mock dependencies
vi.mock("#/utils/error-handler", () => ({
@@ -56,4 +58,89 @@ describe("Actions Service", () => {
}));
});
});
describe("handleActionMessage", () => {
it("should use first-person perspective for task completion messages", () => {
// Test partial completion
const messagePartial: ActionMessage = {
id: 1,
action: ActionType.FINISH,
source: "agent",
message: "",
timestamp: new Date().toISOString(),
args: {
final_thought: "",
task_completed: "partial",
outputs: "",
thought: ""
}
};
// Mock implementation to capture the message
let capturedPartialMessage = "";
(store.dispatch as any).mockImplementation((action: any) => {
if (action.type === "chat/addAssistantMessage" &&
action.payload.includes("believe that the task was **completed partially**")) {
capturedPartialMessage = action.payload;
}
});
handleActionMessage(messagePartial);
expect(capturedPartialMessage).toContain("I believe that the task was **completed partially**");
// Test not completed
const messageNotCompleted: ActionMessage = {
id: 2,
action: ActionType.FINISH,
source: "agent",
message: "",
timestamp: new Date().toISOString(),
args: {
final_thought: "",
task_completed: "false",
outputs: "",
thought: ""
}
};
// Mock implementation to capture the message
let capturedNotCompletedMessage = "";
(store.dispatch as any).mockImplementation((action: any) => {
if (action.type === "chat/addAssistantMessage" &&
action.payload.includes("believe that the task was **not completed**")) {
capturedNotCompletedMessage = action.payload;
}
});
handleActionMessage(messageNotCompleted);
expect(capturedNotCompletedMessage).toContain("I believe that the task was **not completed**");
// Test completed successfully
const messageCompleted: ActionMessage = {
id: 3,
action: ActionType.FINISH,
source: "agent",
message: "",
timestamp: new Date().toISOString(),
args: {
final_thought: "",
task_completed: "true",
outputs: "",
thought: ""
}
};
// Mock implementation to capture the message
let capturedCompletedMessage = "";
(store.dispatch as any).mockImplementation((action: any) => {
if (action.type === "chat/addAssistantMessage" &&
action.payload.includes("believe that the task was **completed successfully**")) {
capturedCompletedMessage = action.payload;
}
});
handleActionMessage(messageCompleted);
expect(capturedCompletedMessage).toContain("I believe that the task was **completed successfully**");
});
});
});

View File

@@ -1,8 +1,12 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { trackError, showErrorToast, showChatError } from "#/utils/error-handler";
import posthog from "posthog-js";
import toast from "react-hot-toast";
import {
trackError,
showErrorToast,
showChatError,
} from "#/utils/error-handler";
import * as Actions from "#/services/actions";
import * as CustomToast from "#/utils/custom-toast-handlers";
vi.mock("posthog-js", () => ({
default: {
@@ -10,12 +14,6 @@ vi.mock("posthog-js", () => ({
},
}));
vi.mock("react-hot-toast", () => ({
default: {
error: vi.fn(),
},
}));
vi.mock("#/services/actions", () => ({
handleStatusMessage: vi.fn(),
}));
@@ -38,9 +36,12 @@ describe("Error Handler", () => {
trackError(error);
expect(posthog.captureException).toHaveBeenCalledWith(new Error("Test error"), {
error_source: "test",
});
expect(posthog.captureException).toHaveBeenCalledWith(
new Error("Test error"),
{
error_source: "test",
},
);
});
it("should include additional metadata in PostHog event", () => {
@@ -55,15 +56,19 @@ describe("Error Handler", () => {
trackError(error);
expect(posthog.captureException).toHaveBeenCalledWith(new Error("Test error"), {
error_source: "test",
extra: "info",
details: { foo: "bar" },
});
expect(posthog.captureException).toHaveBeenCalledWith(
new Error("Test error"),
{
error_source: "test",
extra: "info",
details: { foo: "bar" },
},
);
});
});
describe("showErrorToast", () => {
const errorToastSpy = vi.spyOn(CustomToast, "displayErrorToast");
it("should log error and show toast", () => {
const error = {
message: "Toast error",
@@ -73,12 +78,15 @@ describe("Error Handler", () => {
showErrorToast(error);
// Verify PostHog logging
expect(posthog.captureException).toHaveBeenCalledWith(new Error("Toast error"), {
error_source: "toast-test",
});
expect(posthog.captureException).toHaveBeenCalledWith(
new Error("Toast error"),
{
error_source: "toast-test",
},
);
// Verify toast was shown
expect(toast.error).toHaveBeenCalled();
expect(errorToastSpy).toHaveBeenCalled();
});
it("should include metadata in PostHog event when showing toast", () => {
@@ -90,10 +98,13 @@ describe("Error Handler", () => {
showErrorToast(error);
expect(posthog.captureException).toHaveBeenCalledWith(new Error("Toast error"), {
error_source: "toast-test",
context: "testing",
});
expect(posthog.captureException).toHaveBeenCalledWith(
new Error("Toast error"),
{
error_source: "toast-test",
context: "testing",
},
);
});
it("should log errors from different sources with appropriate metadata", () => {
@@ -104,10 +115,13 @@ describe("Error Handler", () => {
metadata: { id: "error.agent" },
});
expect(posthog.captureException).toHaveBeenCalledWith(new Error("Agent error"), {
error_source: "agent-status",
id: "error.agent",
});
expect(posthog.captureException).toHaveBeenCalledWith(
new Error("Agent error"),
{
error_source: "agent-status",
id: "error.agent",
},
);
showErrorToast({
message: "Server error",
@@ -115,11 +129,14 @@ describe("Error Handler", () => {
metadata: { error_code: 500, details: "Internal error" },
});
expect(posthog.captureException).toHaveBeenCalledWith(new Error("Server error"), {
error_source: "server",
error_code: 500,
details: "Internal error",
});
expect(posthog.captureException).toHaveBeenCalledWith(
new Error("Server error"),
{
error_source: "server",
error_code: 500,
details: "Internal error",
},
);
});
it("should log feedback submission errors with conversation context", () => {
@@ -130,11 +147,14 @@ describe("Error Handler", () => {
metadata: { conversationId: "123", error },
});
expect(posthog.captureException).toHaveBeenCalledWith(new Error("Feedback submission failed"), {
error_source: "feedback",
conversationId: "123",
error,
});
expect(posthog.captureException).toHaveBeenCalledWith(
new Error("Feedback submission failed"),
{
error_source: "feedback",
conversationId: "123",
error,
},
);
});
});
@@ -149,9 +169,12 @@ describe("Error Handler", () => {
showChatError(error);
// Verify PostHog logging
expect(posthog.captureException).toHaveBeenCalledWith(new Error("Chat error"), {
error_source: "chat-test",
});
expect(posthog.captureException).toHaveBeenCalledWith(
new Error("Chat error"),
{
error_source: "chat-test",
},
);
// Verify error message was shown in chat
expect(Actions.handleStatusMessage).toHaveBeenCalledWith({

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "openhands-frontend",
"version": "0.28.0",
"version": "0.28.1",
"private": true,
"type": "module",
"engines": {
@@ -9,43 +9,43 @@
"dependencies": {
"@heroui/react": "2.7.4",
"@monaco-editor/react": "^4.7.0-rc.0",
"@react-router/node": "^7.2.0",
"@react-router/serve": "^7.2.0",
"@react-types/shared": "^3.27.0",
"@react-router/node": "^7.3.0",
"@react-router/serve": "^7.3.0",
"@react-types/shared": "^3.28.0",
"@reduxjs/toolkit": "^2.6.0",
"@stripe/react-stripe-js": "^3.1.1",
"@stripe/stripe-js": "^5.7.0",
"@tanstack/react-query": "^5.66.11",
"@stripe/react-stripe-js": "^3.3.0",
"@stripe/stripe-js": "^5.10.0",
"@tanstack/react-query": "^5.67.2",
"@vitejs/plugin-react": "^4.3.2",
"@xterm/addon-fit": "^0.10.0",
"@xterm/xterm": "^5.4.0",
"axios": "^1.8.1",
"axios": "^1.8.2",
"clsx": "^2.1.1",
"eslint-config-airbnb-typescript": "^18.0.0",
"framer-motion": "^12.4.7",
"framer-motion": "^12.4.10",
"i18next": "^24.2.2",
"i18next-browser-languagedetector": "^8.0.4",
"i18next-http-backend": "^3.0.2",
"isbot": "^5.1.23",
"jose": "^6.0.8",
"monaco-editor": "^0.52.2",
"posthog-js": "^1.225.1",
"posthog-js": "^1.229.3",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-highlight": "^0.15.0",
"react-hot-toast": "^2.5.1",
"react-i18next": "^15.4.1",
"react-icons": "^5.5.0",
"react-markdown": "^10.0.1",
"react-markdown": "^10.1.0",
"react-redux": "^9.2.0",
"react-router": "^7.2.0",
"react-router": "^7.3.0",
"react-syntax-highlighter": "^15.6.1",
"react-textarea-autosize": "^8.5.7",
"remark-gfm": "^4.0.1",
"sirv-cli": "^3.0.1",
"socket.io-client": "^4.8.1",
"tailwind-merge": "^3.0.2",
"vite": "^6.2.0",
"vite": "^6.2.1",
"web-vitals": "^3.5.2",
"ws": "^8.18.1"
},
@@ -80,29 +80,29 @@
},
"devDependencies": {
"@mswjs/socket.io-binding": "^0.1.1",
"@playwright/test": "^1.50.1",
"@react-router/dev": "^7.2.0",
"@playwright/test": "^1.51.0",
"@react-router/dev": "^7.3.0",
"@tailwindcss/typography": "^0.5.16",
"@tanstack/eslint-plugin-query": "^5.66.1",
"@tanstack/eslint-plugin-query": "^5.67.2",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.1",
"@testing-library/react": "^16.2.0",
"@testing-library/user-event": "^14.6.1",
"@types/node": "^22.13.8",
"@types/node": "^22.13.9",
"@types/react": "^19.0.8",
"@types/react-dom": "^19.0.3",
"@types/react-highlight": "^0.12.8",
"@types/react-syntax-highlighter": "^15.5.13",
"@types/ws": "^8.5.14",
"@types/ws": "^8.18.0",
"@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0",
"@vitest/coverage-v8": "^3.0.7",
"@vitest/coverage-v8": "^3.0.8",
"autoprefixer": "^10.4.20",
"cross-env": "^7.0.3",
"eslint": "^8.57.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-airbnb-typescript": "^18.0.0",
"eslint-config-prettier": "^10.0.2",
"eslint-config-prettier": "^10.1.1",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-prettier": "^5.2.3",

View File

@@ -8,7 +8,7 @@
* - Please do NOT serve this file on production.
*/
const PACKAGE_VERSION = '2.7.0'
const PACKAGE_VERSION = '2.7.3'
const INTEGRITY_CHECKSUM = '00729d72e3b82faf54ca8b9621dbb96f'
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
const activeClientIds = new Set()

View File

@@ -281,6 +281,13 @@ class OpenHands {
return data.redirect_url;
}
static async createBillingSessionResponse(): Promise<string> {
const { data } = await openHands.post(
"/api/billing/create-customer-setup-session",
);
return data.redirect_url;
}
static async getBalance(): Promise<string> {
const { data } = await openHands.get<{ credits: string }>(
"/api/billing/credits",

View File

@@ -48,6 +48,7 @@ export interface GetConfigResponse {
APP_SLUG?: string;
GITHUB_CLIENT_ID: string;
POSTHOG_CLIENT_KEY: string;
STRIPE_PUBLISHABLE_KEY?: string;
}
export interface GetVSCodeUrlResponse {

View File

@@ -1,5 +1,4 @@
import { useDispatch, useSelector } from "react-redux";
import toast from "react-hot-toast";
import React from "react";
import posthog from "posthog-js";
import { useParams } from "react-router";
@@ -23,6 +22,7 @@ import { ScrollToBottomButton } from "#/components/shared/buttons/scroll-to-bott
import { LoadingSpinner } from "#/components/shared/loading-spinner";
import { useGetTrajectory } from "#/hooks/mutation/use-get-trajectory";
import { downloadTrajectory } from "#/utils/download-files";
import { displayErrorToast } from "#/utils/custom-toast-handlers";
function getEntryPoint(
hasRepository: boolean | null,
@@ -98,7 +98,7 @@ export function ChatInterface() {
const onClickExportTrajectoryButton = () => {
if (!params.conversationId) {
toast.error("ConversationId unknown, cannot download trajectory");
displayErrorToast("ConversationId unknown, cannot download trajectory");
return;
}
@@ -110,7 +110,7 @@ export function ChatInterface() {
);
},
onError: (error) => {
toast.error(error.message);
displayErrorToast(error.message);
},
});
};

View File

@@ -0,0 +1,48 @@
import { useMutation } from "@tanstack/react-query";
import { Trans, useTranslation } from "react-i18next";
import AllHandsLogo from "#/assets/branding/all-hands-logo.svg?react";
import { ModalBackdrop } from "#/components/shared/modals/modal-backdrop";
import { ModalBody } from "#/components/shared/modals/modal-body";
import OpenHands from "#/api/open-hands";
import { BrandButton } from "../settings/brand-button";
import { displayErrorToast } from "#/utils/custom-toast-handlers";
export function SetupPaymentModal() {
const { t } = useTranslation();
const { mutate, isPending } = useMutation({
mutationFn: OpenHands.createBillingSessionResponse,
onSuccess: (data) => {
window.location.href = data;
},
onError: () => {
displayErrorToast(t("BILLING$ERROR_WHILE_CREATING_SESSION"));
},
});
return (
<ModalBackdrop>
<ModalBody className="border border-tertiary">
<AllHandsLogo width={68} height={46} />
<div className="flex flex-col gap-2 w-full items-center text-center">
<h1 className="text-2xl font-bold">{t("BILLING$YOUVE_GOT_50")}</h1>
<p>
<Trans
i18nKey="BILLING$CLAIM_YOUR_50"
components={{ b: <strong /> }}
/>
</p>
</div>
<BrandButton
testId="proceed-to-stripe-button"
type="submit"
variant="primary"
className="w-full"
isDisabled={isPending}
onClick={mutate}
>
{t("BILLING$PROCEED_TO_STRIPE")}
</BrandButton>
</ModalBody>
</ModalBackdrop>
);
}

View File

@@ -2,7 +2,6 @@ import React from "react";
import { FaListUl } from "react-icons/fa";
import { useDispatch } from "react-redux";
import posthog from "posthog-js";
import toast from "react-hot-toast";
import { NavLink, useLocation } from "react-router";
import { useGitHubUser } from "#/hooks/query/use-github-user";
import { UserActions } from "./user-actions";
@@ -10,7 +9,6 @@ import { AllHandsLogoButton } from "#/components/shared/buttons/all-hands-logo-b
import { DocsButton } from "#/components/shared/buttons/docs-button";
import { ExitProjectButton } from "#/components/shared/buttons/exit-project-button";
import { SettingsButton } from "#/components/shared/buttons/settings-button";
import { LoadingSpinner } from "#/components/shared/loading-spinner";
import { SettingsModal } from "#/components/shared/modals/settings/settings-modal";
import { useCurrentSettings } from "#/context/settings-context";
import { useSettings } from "#/hooks/query/use-settings";
@@ -23,6 +21,8 @@ import { ConversationPanelWrapper } from "../conversation-panel/conversation-pan
import { useLogout } from "#/hooks/mutation/use-logout";
import { useConfig } from "#/hooks/query/use-config";
import { cn } from "#/utils/utils";
import { displayErrorToast } from "#/utils/custom-toast-handlers";
import { HIDE_LLM_SETTINGS } from "#/utils/feature-flags";
export function Sidebar() {
const location = useLocation();
@@ -43,7 +43,12 @@ export function Sidebar() {
const [conversationPanelIsOpen, setConversationPanelIsOpen] =
React.useState(false);
// TODO: Remove HIDE_LLM_SETTINGS check once released
const isSaas = HIDE_LLM_SETTINGS() && config?.APP_MODE === "saas";
React.useEffect(() => {
if (isSaas) return;
if (location.pathname === "/settings") {
setSettingsModalIsOpen(false);
} else if (
@@ -53,10 +58,10 @@ export function Sidebar() {
) {
// We don't show toast errors for settings in the global error handler
// because we have a special case for 404 errors
toast.error(
displayErrorToast(
"Something went wrong while fetching settings. Please reload the page.",
);
} else if (settingsError?.status === 404) {
} else if (config?.APP_MODE === "oss" && settingsError?.status === 404) {
setSettingsModalIsOpen(true);
}
}, [
@@ -111,15 +116,13 @@ export function Sidebar() {
>
<SettingsButton />
</NavLink>
{!user.isLoading && (
<UserActions
user={
user.data ? { avatar_url: user.data.avatar_url } : undefined
}
onLogout={handleLogout}
/>
)}
{user.isLoading && <LoadingSpinner size="small" />}
<UserActions
user={
user.data ? { avatar_url: user.data.avatar_url } : undefined
}
onLogout={handleLogout}
isLoading={user.isFetching}
/>
</div>
</nav>

View File

@@ -5,9 +5,10 @@ import { AccountSettingsContextMenu } from "../context-menu/account-settings-con
interface UserActionsProps {
onLogout: () => void;
user?: { avatar_url: string };
isLoading?: boolean;
}
export function UserActions({ onLogout, user }: UserActionsProps) {
export function UserActions({ onLogout, user, isLoading }: UserActionsProps) {
const [accountContextMenuIsVisible, setAccountContextMenuIsVisible] =
React.useState(false);
@@ -26,7 +27,11 @@ export function UserActions({ onLogout, user }: UserActionsProps) {
return (
<div data-testid="user-actions" className="w-8 h-8 relative">
<UserAvatar avatarUrl={user?.avatar_url} onClick={toggleAccountMenu} />
<UserAvatar
avatarUrl={user?.avatar_url}
onClick={toggleAccountMenu}
isLoading={isLoading}
/>
{accountContextMenuIsVisible && (
<AccountSettingsContextMenu

View File

@@ -1,10 +1,10 @@
import React from "react";
import { MutateOptions } from "@tanstack/react-query";
import toast from "react-hot-toast";
import { useSettings } from "#/hooks/query/use-settings";
import { useSaveSettings } from "#/hooks/mutation/use-save-settings";
import { PostSettings, Settings } from "#/types/settings";
import { retrieveAxiosErrorMessage } from "#/utils/retrieve-axios-error-message";
import { displayErrorToast } from "#/utils/custom-toast-handlers";
type SaveUserSettingsConfig = {
onSuccess: MutateOptions<void, Error, Partial<PostSettings>>["onSuccess"];
@@ -47,7 +47,7 @@ export function SettingsProvider({ children }: SettingsProviderProps) {
onSuccess: config?.onSuccess,
onError: (error) => {
const errorMessage = retrieveAxiosErrorMessage(error);
toast.error(errorMessage);
displayErrorToast(errorMessage);
},
});
};

View File

@@ -17,7 +17,7 @@ const saveSettingsMutationFn = async (settings: Partial<PostSettings>) => {
? ""
: settings.LLM_API_KEY?.trim() || undefined,
remote_runtime_resource_factor: settings.REMOTE_RUNTIME_RESOURCE_FACTOR,
github_token: settings.github_token,
provider_tokens: settings.provider_tokens,
unset_github_token: settings.unset_github_token,
enable_default_condenser: settings.ENABLE_DEFAULT_CONDENSER,
enable_sound_notifications: settings.ENABLE_SOUND_NOTIFICATIONS,

View File

@@ -1,8 +1,8 @@
import { useMutation } from "@tanstack/react-query";
import toast from "react-hot-toast";
import { Feedback } from "#/api/open-hands.types";
import OpenHands from "#/api/open-hands";
import { useConversation } from "#/context/conversation-context";
import { displayErrorToast } from "#/utils/custom-toast-handlers";
type SubmitFeedbackArgs = {
feedback: Feedback;
@@ -14,7 +14,7 @@ export const useSubmitFeedback = () => {
mutationFn: ({ feedback }: SubmitFeedbackArgs) =>
OpenHands.submitFeedback(conversationId, feedback),
onError: (error) => {
toast.error(error.message);
displayErrorToast(error.message);
},
});
};

View File

@@ -11,4 +11,6 @@ export const useAIConfigOptions = () =>
useQuery({
queryKey: ["ai-config-options"],
queryFn: fetchAiConfigOptions,
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 15, // 15 minutes
});

View File

@@ -14,5 +14,7 @@ export const useAppInstallations = () => {
githubTokenIsSet &&
!!config?.GITHUB_CLIENT_ID &&
config?.APP_MODE === "saas",
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 15, // 15 minutes
});
};

View File

@@ -50,6 +50,8 @@ export const useAppRepositories = () => {
Array.isArray(installations) &&
installations.length > 0 &&
config?.APP_MODE === "saas",
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 15, // 15 minutes
});
// TODO: Once we create our custom dropdown component, we should fetch data onEndReached

View File

@@ -1,6 +1,7 @@
import { useQuery } from "@tanstack/react-query";
import { useConfig } from "./use-config";
import OpenHands from "#/api/open-hands";
import { BILLING_SETTINGS } from "#/utils/feature-flags";
export const useBalance = () => {
const { data: config } = useConfig();
@@ -8,6 +9,6 @@ export const useBalance = () => {
return useQuery({
queryKey: ["user", "balance"],
queryFn: OpenHands.getBalance,
enabled: config?.APP_MODE === "saas",
enabled: config?.APP_MODE === "saas" && BILLING_SETTINGS(),
});
};

View File

@@ -5,4 +5,6 @@ export const useConfig = () =>
useQuery({
queryKey: ["config"],
queryFn: OpenHands.getConfig,
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 15, // 15 minutes
});

View File

@@ -18,6 +18,8 @@ export const useConversationConfig = () => {
return OpenHands.getRuntimeId(conversationId);
},
enabled: status !== WsClientProviderStatus.DISCONNECTED && !!conversationId,
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 15, // 15 minutes
});
React.useEffect(() => {

View File

@@ -19,6 +19,8 @@ export const useGitHubUser = () => {
queryFn: OpenHands.getGitHubUser,
enabled: githubTokenIsSet && !!config?.APP_MODE,
retry: false,
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 15, // 15 minutes
});
React.useEffect(() => {

View File

@@ -15,6 +15,7 @@ export const useIsAuthed = () => {
queryFn: () => OpenHands.authenticate(appMode!),
enabled: !!appMode,
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 15, // 15 minutes
retry: false,
meta: {
disableToast: true,

View File

@@ -23,5 +23,7 @@ export const useListFiles = (config: UseListFilesConfig = DEFAULT_CONFIG) => {
queryKey: ["files", conversationId, config?.path],
queryFn: () => OpenHands.getFiles(conversationId, config?.path),
enabled: !!(isActive && config?.enabled),
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 15, // 15 minutes
});
};

View File

@@ -7,5 +7,7 @@ export function useSearchRepositories(query: string) {
queryFn: () => OpenHands.searchGitHubRepositories(query, 3),
enabled: !!query,
select: (data) => data.map((repo) => ({ ...repo, is_public: true })),
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 15, // 15 minutes
});
}

View File

@@ -21,6 +21,8 @@ const getSettingsQueryFn = async () => {
ENABLE_DEFAULT_CONDENSER: apiSettings.enable_default_condenser,
ENABLE_SOUND_NOTIFICATIONS: apiSettings.enable_sound_notifications,
USER_CONSENTS_TO_ANALYTICS: apiSettings.user_consents_to_analytics,
PROVIDER_TOKENS: apiSettings.provider_tokens,
IS_NEW_USER: false,
};
};
@@ -34,6 +36,8 @@ export const useSettings = () => {
// would want to show the modal immediately if the
// settings are not found
retry: (_, error) => error.status !== 404,
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 15, // 15 minutes
meta: {
disableToast: true,
},

View File

@@ -7,4 +7,6 @@ export const useUserConversation = (cid: string | null) =>
queryFn: () => OpenHands.getConversation(cid!),
enabled: !!cid,
retry: false,
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 15, // 15 minutes
});

View File

@@ -9,6 +9,5 @@ export const useUserConversations = () => {
queryKey: ["user", "conversations"],
queryFn: OpenHands.getUserConversations,
enabled: !!userIsAuthenticated,
staleTime: 0,
});
};

View File

@@ -15,6 +15,8 @@ export const useUserRepositories = () => {
initialPageParam: 1,
getNextPageParam: (lastPage) => lastPage.nextPage,
enabled: githubTokenIsSet && config?.APP_MODE === "oss",
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 15, // 15 minutes
});
// TODO: Once we create our custom dropdown component, we should fetch data onEndReached

View File

@@ -13,6 +13,8 @@ export const useVSCodeUrl = (config: { enabled: boolean }) => {
},
enabled: !!conversationId && config.enabled,
refetchOnMount: false,
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 15, // 15 minutes
});
return data;

View File

@@ -86,6 +86,8 @@ export const useTerminal = ({
const handleEnter = (command: string) => {
terminal.current?.write("\r\n");
// Send the command to the backend but don't echo it back in the terminal
// The backend will include the command in its response
send(getTerminalCommand(command));
};
@@ -131,10 +133,26 @@ export const useTerminal = ({
content = content.replaceAll(secret, "*".repeat(10));
});
terminal.current?.writeln(
parseTerminalOutput(content.replaceAll("\n", "\r\n").trim()),
);
// Check if this is an output that starts with the previous input command
// This happens when the backend echoes back the command in the output
let shouldDisplayContent = true;
if (type === "output" && i > 0 && commands[i - 1].type === "input") {
const prevInputCommand = commands[i - 1].content.trim();
// If the output starts with the input command, remove it to avoid duplication
if (content.trim().startsWith(prevInputCommand)) {
// Skip displaying this part as it's a duplicate of the user's input
// that's already shown in the terminal
shouldDisplayContent = false;
}
}
if (shouldDisplayContent) {
terminal.current?.writeln(
parseTerminalOutput(content.replaceAll("\n", "\r\n").trim()),
);
}
// Add a new prompt after the output
if (type === "output") {
terminal.current.write(`\n$ `);
}

View File

@@ -312,4 +312,9 @@ export enum I18nKey {
BUTTON$MARK_NOT_HELPFUL = "BUTTON$MARK_NOT_HELPFUL",
BUTTON$EXPORT_CONVERSATION = "BUTTON$EXPORT_CONVERSATION",
BILLING$CLICK_TO_TOP_UP = "BILLING$CLICK_TO_TOP_UP",
BILLING$YOUVE_GOT_50 = "BILLING$YOUVE_GOT_50",
BILLING$ERROR_WHILE_CREATING_SESSION = "BILLING$ERROR_WHILE_CREATING_SESSION",
BILLING$CLAIM_YOUR_50 = "BILLING$CLAIM_YOUR_50",
BILLING$PROCEED_TO_STRIPE = "BILLING$PROCEED_TO_STRIPE",
BILLING$YOURE_IN = "BILLING$YOURE_IN",
}

View File

@@ -4647,5 +4647,80 @@
"fr": "Ajouter des fonds à votre compte",
"tr": "Hesabınıza bakiye ekleyin",
"de": "Guthaben zu Ihrem Konto hinzufügen"
},
"BILLING$YOUVE_GOT_50": {
"en": "You've got $50 in free OpenHands credits",
"ja": "OpenHandsの無料クレジット$50を獲得しました",
"zh-CN": "您获得了 $50 的 OpenHands 免费额度",
"zh-TW": "您獲得了 $50 的 OpenHands 免費額度",
"ko-KR": "OpenHands 무료 크레딧 $50를 받았습니다",
"no": "Du har fått $50 i gratis OpenHands-kreditter",
"it": "Hai ottenuto $50 in crediti gratuiti OpenHands",
"pt": "Você ganhou $50 em créditos gratuitos OpenHands",
"es": "Has recibido $50 en créditos gratuitos de OpenHands",
"ar": "لديك 50$ من رصيد OpenHands المجاني",
"fr": "Vous avez reçu $50 de crédits OpenHands gratuits",
"tr": "OpenHands'de $50 ücretsiz kredi kazandınız",
"de": "Sie haben $50 in kostenlosen OpenHands-Guthaben erhalten"
},
"BILLING$ERROR_WHILE_CREATING_SESSION": {
"en": "Error occurred while setting up your payment session. Please try again later.",
"ja": "お支払いセッションの設定中にエラーが発生しました。後ほど再度お試しください。",
"zh-CN": "设置支付会话时发生错误。请稍后再试。",
"zh-TW": "設置支付會話時發生錯誤。請稍後再試。",
"ko-KR": "결제 세션 설정 중 오류가 발생했습니다. 나중에 다시 시도해 주세요.",
"no": "Det oppstod en feil under oppsett av betalingsøkten. Vennligst prøv igjen senere.",
"it": "Si è verificato un errore durante la configurazione della sessione di pagamento. Si prega di riprovare più tardi.",
"pt": "Ocorreu um erro ao configurar sua sessão de pagamento. Por favor, tente novamente mais tarde.",
"es": "Se produjo un error al configurar tu sesión de pago. Por favor, inténtalo de nuevo más tarde.",
"ar": "حدث خطأ أثناء إعداد جلسة الدفع الخاصة بك. يرجى المحاولة مرة أخرى لاحقًا.",
"fr": "Une erreur s'est produite lors de la configuration de votre session de paiement. Veuillez réessayer plus tard.",
"tr": "Ödeme oturumunuz kurulurken bir hata oluştu. Lütfen daha sonra tekrar deneyin.",
"de": "Beim Einrichten Ihrer Zahlungssitzung ist ein Fehler aufgetreten. Bitte versuchen Sie es später erneut."
},
"BILLING$CLAIM_YOUR_50": {
"en": "Add a credit card with Stripe to claim your $50. <b>We won't charge you without asking first!</b>",
"ja": "Stripeでクレジットカードを追加して$50を獲得。<b>事前の確認なしで請求することはありません!</b>",
"zh-CN": "添加 Stripe 信用卡以领取 $50。<b>我们不会在未经您同意的情况下收费!</b>",
"zh-TW": "添加 Stripe 信用卡以領取 $50。<b>我們不會在未經您同意的情況下收費!</b>",
"ko-KR": "Stripe에 신용카드를 추가하여 $50를 받으세요. <b>사전 동의 없이 요금이 청구되지 않습니다!</b>",
"no": "Legg til et kredittkort med Stripe for å få $50. <b>Vi belaster deg ikke uten å spørre først!</b>",
"it": "Aggiungi una carta di credito con Stripe per ottenere $50. <b>Non ti addebiteremo nulla senza chiedere prima!</b>",
"pt": "Adicione um cartão de crédito com Stripe para receber $50. <b>Não cobraremos sem perguntar primeiro!</b>",
"es": "Añade una tarjeta de crédito con Stripe para reclamar tus $50. <b>¡No te cobraremos sin preguntarte primero!</b>",
"ar": "أضف بطاقة ائتمان مع Stripe للحصول على 50$. <b>لن نقوم بالخصم دون إذن مسبق!</b>",
"fr": "Ajoutez une carte de crédit avec Stripe pour obtenir 50$. <b>Nous ne vous facturerons pas sans vous demander d'abord !</b>",
"tr": "50$ almak için Stripe ile kredi kartı ekleyin. <b>Önce sormadan ücret almayacağız!</b>",
"de": "Fügen Sie eine Kreditkarte mit Stripe hinzu, um $50 zu erhalten. <b>Wir belasten Sie nicht ohne vorherige Zustimmung!</b>"
},
"BILLING$PROCEED_TO_STRIPE": {
"en": "Add Billing Info",
"ja": "請求情報を追加",
"zh-CN": "添加账单信息",
"zh-TW": "添加帳單資訊",
"ko-KR": "결제 정보 추가",
"no": "Legg til betalingsinformasjon",
"it": "Aggiungi informazioni di fatturazione",
"pt": "Adicionar informações de pagamento",
"es": "Añadir información de facturación",
"ar": "إضافة معلومات الفواتير",
"fr": "Ajouter les informations de facturation",
"tr": "Fatura Bilgisi Ekle",
"de": "Zahlungsinformationen hinzufügen"
},
"BILLING$YOURE_IN": {
"en": "You're in! You can start using your $50 in free credits now.",
"ja": "登録完了!$50分の無料クレジットを今すぐご利用いただけます。",
"zh-CN": "您已加入!现在可以开始使用$50的免费额度了。",
"zh-TW": "您已加入!現在可以開始使用$50的免費額度了。",
"ko-KR": "가입 완료! 지금 바로 $50 상당의 무료 크레딧을 사용하실 수 있습니다.",
"no": "Du er med! Du kan begynne å bruke dine $50 i gratis kreditter nå.",
"it": "Ci sei! Puoi iniziare a utilizzare i tuoi $50 in crediti gratuiti ora.",
"pt": "Você está dentro! Você pode começar a usar seus $50 em créditos gratuitos agora.",
"es": "¡Ya estás dentro! Puedes empezar a usar tus $50 en créditos gratuitos ahora.",
"ar": "أنت معنا! يمكنك البدء في استخدام رصيدك المجاني البالغ 50 دولارًا الآن.",
"fr": "C'est fait ! Vous pouvez commencer à utiliser vos 50 $ de crédits gratuits maintenant.",
"tr": "Başardın! Şimdi $50 değerindeki ücretsiz kredilerini kullanmaya başlayabilirsin.",
"de": "Du bist dabei! Du kannst jetzt deine $50 an kostenlosen Guthaben nutzen."
}
}

View File

@@ -1,5 +1,3 @@
<svg width="11" height="11" viewBox="0 0 11 11" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M5.69949 5.72974L7.91965 7.9505L8.35077 7.51999L6.13001 5.29922L8.35077 3.07907L7.92026 2.64795L5.69949 4.86871L3.47934 2.64795L3.04883 3.07907L5.26898 5.29922L3.04883 7.51938L3.47934 7.9505L5.69949 5.72974Z"
fill="black" />
<svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.5 13.0602L16.2123 16.7725L17.273 15.7118L13.5607 11.9995L17.273 8.28723L16.2123 7.22657L12.5 10.9389L8.78771 7.22656L7.72705 8.28722L11.4394 11.9995L7.72706 15.7119L8.78772 16.7725L12.5 13.0602Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 387 B

After

Width:  |  Height:  |  Size: 327 B

View File

@@ -1,8 +1,4 @@
import { delay, http, HttpResponse } from "msw";
import Stripe from "stripe";
const TEST_STRIPE_SECRET_KEY = "";
const PRICE_ID = "";
export const STRIPE_BILLING_HANDLERS = [
http.get("/api/billing/credits", async () => {
@@ -10,27 +6,17 @@ export const STRIPE_BILLING_HANDLERS = [
return HttpResponse.json({ credits: "100" });
}),
http.post("/api/billing/create-checkout-session", async ({ request }) => {
http.post("/api/billing/create-checkout-session", async () => {
await delay();
const body = await request.json();
return HttpResponse.json({
redirect_url: "https://stripe.com/some-checkout",
});
}),
if (body && typeof body === "object" && body.amount) {
const stripe = new Stripe(TEST_STRIPE_SECRET_KEY);
const session = await stripe.checkout.sessions.create({
line_items: [
{
price: PRICE_ID,
quantity: body.amount,
},
],
mode: "payment",
success_url: "http://localhost:3001/settings/billing/?checkout=success",
cancel_url: "http://localhost:3001/settings/billing/?checkout=cancel",
});
if (session.url) return HttpResponse.json({ redirect_url: session.url });
}
return HttpResponse.json({ message: "Invalid request" }, { status: 400 });
http.post("/api/billing/create-customer-setup-session", async () => {
await delay();
return HttpResponse.json({
redirect_url: "https://stripe.com/some-customer-setup",
});
}),
];

View File

@@ -22,12 +22,13 @@ export const MOCK_DEFAULT_USER_SETTINGS: ApiSettings | PostApiSettings = {
enable_default_condenser: DEFAULT_SETTINGS.ENABLE_DEFAULT_CONDENSER,
enable_sound_notifications: DEFAULT_SETTINGS.ENABLE_SOUND_NOTIFICATIONS,
user_consents_to_analytics: DEFAULT_SETTINGS.USER_CONSENTS_TO_ANALYTICS,
provider_tokens: DEFAULT_SETTINGS.PROVIDER_TOKENS,
};
const MOCK_USER_PREFERENCES: {
settings: ApiSettings | PostApiSettings;
settings: ApiSettings | PostApiSettings | null;
} = {
settings: MOCK_DEFAULT_USER_SETTINGS,
settings: null,
};
const conversations: Conversation[] = [
@@ -174,22 +175,24 @@ export const handlers = [
),
http.get("/api/options/config", () => {
const mockSaas = import.meta.env.VITE_MOCK_SAAS === "true";
const config: GetConfigResponse = {
APP_MODE: mockSaas ? "saas" : "oss",
GITHUB_CLIENT_ID: "fake-github-client-id",
POSTHOG_CLIENT_KEY: "fake-posthog-client-key",
STRIPE_PUBLISHABLE_KEY: "",
};
return HttpResponse.json(config);
}),
http.get("/api/settings", async () => {
await delay();
const settings: ApiSettings = {
...MOCK_USER_PREFERENCES.settings,
language: "no",
};
// @ts-expect-error - mock types
if (settings.github_token) settings.github_token_is_set = true;
const { settings } = MOCK_USER_PREFERENCES;
if (!settings) return HttpResponse.json(null, { status: 404 });
if (Object.keys(settings.provider_tokens).length > 0)
settings.github_token_is_set = true;
return HttpResponse.json(settings);
}),
@@ -201,17 +204,19 @@ export const handlers = [
if (typeof body === "object") {
newSettings = { ...body };
if (newSettings.unset_github_token) {
newSettings.github_token = undefined;
newSettings.provider_tokens = { github: "", gitlab: "" };
newSettings.github_token_is_set = false;
delete newSettings.unset_github_token;
}
}
MOCK_USER_PREFERENCES.settings = {
const fullSettings = {
...MOCK_DEFAULT_USER_SETTINGS,
...MOCK_USER_PREFERENCES.settings,
...newSettings,
};
MOCK_USER_PREFERENCES.settings = fullSettings;
return HttpResponse.json(null, { status: 200 });
}

View File

@@ -3,8 +3,8 @@ import {
QueryCache,
MutationCache,
} from "@tanstack/react-query";
import toast from "react-hot-toast";
import { retrieveAxiosErrorMessage } from "./utils/retrieve-axios-error-message";
import { displayErrorToast } from "./utils/custom-toast-handlers";
const shownErrors = new Set<string>();
export const queryClientConfig: QueryClientConfig = {
@@ -14,7 +14,7 @@ export const queryClientConfig: QueryClientConfig = {
const errorMessage = retrieveAxiosErrorMessage(error);
if (!shownErrors.has(errorMessage)) {
toast.error(errorMessage || "An error occurred");
displayErrorToast(errorMessage || "An error occurred");
shownErrors.add(errorMessage);
setTimeout(() => {
@@ -28,14 +28,8 @@ export const queryClientConfig: QueryClientConfig = {
onError: (error, _, __, mutation) => {
if (!mutation?.meta?.disableToast) {
const message = retrieveAxiosErrorMessage(error);
toast.error(message);
displayErrorToast(message);
}
},
}),
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 15, // 15 minutes
},
},
};

View File

@@ -24,7 +24,10 @@ function Home() {
});
return (
<div className="bg-base-secondary h-full rounded-xl flex flex-col items-center justify-center relative overflow-y-auto px-2">
<div
data-testid="home-screen"
className="bg-base-secondary h-full rounded-xl flex flex-col items-center justify-center relative overflow-y-auto px-2"
>
<HeroHeading />
<div className="flex flex-col gap-8 w-full md:w-[600px] items-center">
<div className="flex flex-col gap-2 w-full">

View File

@@ -1,5 +1,4 @@
import React from "react";
import toast from "react-hot-toast";
import { useDispatch, useSelector } from "react-redux";
import { setImportedProjectZip } from "#/state/initial-query-slice";
import { RootState } from "#/store";
@@ -7,6 +6,7 @@ import { base64ToBlob } from "#/utils/base64-to-blob";
import { useUploadFiles } from "../../../hooks/mutation/use-upload-files";
import { RUNTIME_INACTIVE_STATES } from "#/types/agent-state";
import { displayErrorToast } from "#/utils/custom-toast-handlers";
export const useHandleRuntimeActive = () => {
const dispatch = useDispatch();
@@ -29,7 +29,7 @@ export const useHandleRuntimeActive = () => {
{ files: [file] },
{
onError: () => {
toast.error("Failed to upload project files.");
displayErrorToast("Failed to upload project files.");
},
},
);

View File

@@ -1,5 +1,4 @@
import React from "react";
import toast from "react-hot-toast";
import { useDispatch } from "react-redux";
import { useWsClient } from "#/context/ws-client-provider";
import { generateAgentStateChangeEvent } from "#/services/agent-state-service";
@@ -7,6 +6,7 @@ import { addErrorMessage } from "#/state/chat-slice";
import { AgentState } from "#/types/agent-state";
import { ErrorObservation } from "#/types/core/observations";
import { useEndSession } from "../../../hooks/use-end-session";
import { displayErrorToast } from "#/utils/custom-toast-handlers";
interface ServerError {
error: boolean | string;
@@ -32,15 +32,15 @@ export const useHandleWSEvents = () => {
if (isServerError(event)) {
if (event.error_code === 401) {
toast.error("Session expired.");
displayErrorToast("Session expired.");
endSession();
return;
}
if (typeof event.error === "string") {
toast.error(event.error);
displayErrorToast(event.error);
} else {
toast.error(event.message);
displayErrorToast(event.message);
}
return;
}

View File

@@ -3,7 +3,6 @@ import React from "react";
import { Outlet } from "react-router";
import { useDispatch, useSelector } from "react-redux";
import { FaServer } from "react-icons/fa";
import toast from "react-hot-toast";
import { useTranslation } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
import {
@@ -36,6 +35,7 @@ import { TerminalStatusLabel } from "#/components/features/terminal/terminal-sta
import { useSettings } from "#/hooks/query/use-settings";
import { clearFiles, clearInitialPrompt } from "#/state/initial-query-slice";
import { RootState } from "#/store";
import { displayErrorToast } from "#/utils/custom-toast-handlers";
function AppContent() {
useConversationConfig();
@@ -66,7 +66,7 @@ function AppContent() {
React.useEffect(() => {
if (isFetched && !conversation) {
toast.error(
displayErrorToast(
"This conversation does not exist, or you do not have permission to access it.",
);
endSession();

View File

@@ -1,5 +1,13 @@
import React from "react";
import { useRouteError, isRouteErrorResponse, Outlet } from "react-router";
import {
useRouteError,
isRouteErrorResponse,
Outlet,
useNavigate,
useLocation,
useSearchParams,
} from "react-router";
import { useTranslation } from "react-i18next";
import i18n from "#/i18n";
import { useGitHubAuthUrl } from "#/hooks/use-github-auth-url";
import { useIsAuthed } from "#/hooks/query/use-is-authed";
@@ -10,6 +18,10 @@ import { AnalyticsConsentFormModal } from "#/components/features/analytics/analy
import { useSettings } from "#/hooks/query/use-settings";
import { useAuth } from "#/context/auth-context";
import { useMigrateUserConsent } from "#/hooks/use-migrate-user-consent";
import { useBalance } from "#/hooks/query/use-balance";
import { SetupPaymentModal } from "#/components/features/payment/setup-payment-modal";
import { BILLING_SETTINGS } from "#/utils/feature-flags";
import { displaySuccessToast } from "#/utils/custom-toast-handlers";
export function ErrorBoundary() {
const error = useRouteError();
@@ -44,11 +56,14 @@ export function ErrorBoundary() {
}
export default function MainApp() {
const navigate = useNavigate();
const { pathname } = useLocation();
const [searchParams] = useSearchParams();
const { githubTokenIsSet } = useAuth();
const { data: settings } = useSettings();
const { error, isFetching } = useBalance();
const { migrateUserConsent } = useMigrateUserConsent();
const [consentFormIsOpen, setConsentFormIsOpen] = React.useState(false);
const { t } = useTranslation();
const config = useConfig();
const {
@@ -62,6 +77,8 @@ export default function MainApp() {
gitHubClientId: config.data?.GITHUB_CLIENT_ID || null,
});
const [consentFormIsOpen, setConsentFormIsOpen] = React.useState(false);
React.useEffect(() => {
if (settings?.LANGUAGE) {
i18n.changeLanguage(settings.LANGUAGE);
@@ -84,6 +101,17 @@ export default function MainApp() {
});
}, []);
React.useEffect(() => {
// Don't allow users to use the app if it 402s
if (error?.status === 402 && pathname !== "/") {
navigate("/");
} else if (!isFetching && searchParams.get("free_credits") === "success") {
displaySuccessToast(t("BILLING$YOURE_IN"));
searchParams.delete("free_credits");
navigate("/");
}
}, [error?.status, pathname, isFetching]);
const userIsAuthed = !!isAuthed && !authError;
const renderWaitlistModal =
!isFetchingAuth && !userIsAuthed && config.data?.APP_MODE === "saas";
@@ -116,6 +144,10 @@ export default function MainApp() {
}}
/>
)}
{BILLING_SETTINGS() &&
config.data?.APP_MODE === "saas" &&
settings?.IS_NEW_USER && <SetupPaymentModal />}
</div>
);
}

View File

@@ -25,6 +25,8 @@ import {
displayErrorToast,
displaySuccessToast,
} from "#/utils/custom-toast-handlers";
import { PostSettings } from "#/types/settings";
import { HIDE_LLM_SETTINGS } from "#/utils/feature-flags";
const REMOTE_RUNTIME_OPTIONS = [
{ key: 1, label: "1x (2 core, 8G)" },
@@ -50,18 +52,25 @@ function AccountSettings() {
const isFetching = isFetchingSettings || isFetchingResources;
const isSuccess = isSuccessfulSettings && isSuccessfulResources;
const isSaas = config?.APP_MODE === "saas";
const shouldHandleSpecialSaasCase = HIDE_LLM_SETTINGS() && isSaas;
const determineWhetherToToggleAdvancedSettings = () => {
if (shouldHandleSpecialSaasCase) return true;
if (isSuccess) {
return (
isCustomModel(resources.models, settings.LLM_MODEL) ||
hasAdvancedSettingsSet(settings)
hasAdvancedSettingsSet({
...settings,
PROVIDER_TOKENS: settings.PROVIDER_TOKENS || {},
})
);
}
return false;
};
const isSaas = config?.APP_MODE === "saas";
const hasAppSlug = !!config?.APP_SLUG;
const isGitHubTokenSet = settings?.GITHUB_TOKEN_IS_SET;
const isLLMKeySet = settings?.LLM_API_KEY === "**********";
@@ -106,58 +115,81 @@ function AccountSettings() {
formData.get("enable-memory-condenser-switch")?.toString() === "on";
const enableSoundNotifications =
formData.get("enable-sound-notifications-switch")?.toString() === "on";
const llmBaseUrl = formData.get("base-url-input")?.toString() || "";
const llmApiKey =
formData.get("llm-api-key-input")?.toString() ||
(isLLMKeySet
? undefined // don't update if it's already set
: ""); // reset if it's first time save to avoid 500 error
saveSettings(
{
github_token:
formData.get("github-token-input")?.toString() || undefined,
LANGUAGE: languageValue,
user_consents_to_analytics: userConsentsToAnalytics,
ENABLE_DEFAULT_CONDENSER: enableMemoryCondenser,
ENABLE_SOUND_NOTIFICATIONS: enableSoundNotifications,
LLM_MODEL: customLlmModel || fullLlmModel,
LLM_BASE_URL: formData.get("base-url-input")?.toString() || "",
LLM_API_KEY:
formData.get("llm-api-key-input")?.toString() ||
(isLLMKeySet
? undefined // don't update if it's already set
: ""), // reset if it's first time save to avoid 500 error
AGENT: formData.get("agent-input")?.toString(),
SECURITY_ANALYZER:
formData.get("security-analyzer-input")?.toString() || "",
REMOTE_RUNTIME_RESOURCE_FACTOR:
remoteRuntimeResourceFactor ||
DEFAULT_SETTINGS.REMOTE_RUNTIME_RESOURCE_FACTOR,
CONFIRMATION_MODE: confirmationModeIsEnabled,
// we don't want the user to be able to modify these settings in SaaS
const finalLlmModel = shouldHandleSpecialSaasCase
? undefined
: customLlmModel || fullLlmModel;
const finalLlmBaseUrl = shouldHandleSpecialSaasCase
? undefined
: llmBaseUrl;
const finalLlmApiKey = shouldHandleSpecialSaasCase ? undefined : llmApiKey;
const githubToken = formData.get("github-token-input")?.toString();
const newSettings = {
github_token: githubToken,
provider_tokens: githubToken
? {
github: githubToken,
gitlab: "",
}
: undefined,
LANGUAGE: languageValue,
user_consents_to_analytics: userConsentsToAnalytics,
ENABLE_DEFAULT_CONDENSER: enableMemoryCondenser,
ENABLE_SOUND_NOTIFICATIONS: enableSoundNotifications,
LLM_MODEL: finalLlmModel,
LLM_BASE_URL: finalLlmBaseUrl,
LLM_API_KEY: finalLlmApiKey,
AGENT: formData.get("agent-input")?.toString(),
SECURITY_ANALYZER:
formData.get("security-analyzer-input")?.toString() || "",
REMOTE_RUNTIME_RESOURCE_FACTOR:
remoteRuntimeResourceFactor ||
DEFAULT_SETTINGS.REMOTE_RUNTIME_RESOURCE_FACTOR,
CONFIRMATION_MODE: confirmationModeIsEnabled,
};
saveSettings(newSettings, {
onSuccess: () => {
handleCaptureConsent(userConsentsToAnalytics);
displaySuccessToast("Settings saved");
setLlmConfigMode(isAdvancedSettingsSet ? "advanced" : "basic");
},
{
onSuccess: () => {
handleCaptureConsent(userConsentsToAnalytics);
displaySuccessToast("Settings saved");
setLlmConfigMode(isAdvancedSettingsSet ? "advanced" : "basic");
},
onError: (error) => {
const errorMessage = retrieveAxiosErrorMessage(error);
displayErrorToast(errorMessage);
},
onError: (error) => {
const errorMessage = retrieveAxiosErrorMessage(error);
displayErrorToast(errorMessage);
},
);
});
};
const handleReset = () => {
saveSettings(
{
...DEFAULT_SETTINGS,
LLM_API_KEY: "", // reset LLM API key
const newSettings: Partial<PostSettings> = {
...DEFAULT_SETTINGS,
LLM_API_KEY: "", // reset LLM API key
};
// we don't want the user to be able to modify these settings in SaaS
// and we should make sure they aren't included in the reset
if (shouldHandleSpecialSaasCase) {
delete newSettings.LLM_API_KEY;
delete newSettings.LLM_BASE_URL;
delete newSettings.LLM_MODEL;
}
saveSettings(newSettings, {
onSuccess: () => {
displaySuccessToast("Settings reset");
setResetSettingsModalIsOpen(false);
setLlmConfigMode(isAdvancedSettingsSet ? "advanced" : "basic");
},
{
onSuccess: () => {
displaySuccessToast("Settings reset");
setResetSettingsModalIsOpen(false);
setLlmConfigMode(isAdvancedSettingsSet ? "advanced" : "basic");
},
},
);
});
};
React.useEffect(() => {
@@ -190,150 +222,162 @@ function AccountSettings() {
return (
<>
<form
data-testid="account-settings-form"
ref={formRef}
action={onSubmit}
className="flex flex-col grow overflow-auto"
>
<div className="flex flex-col gap-12 px-11 py-9">
<section className="flex flex-col gap-6">
<div className="flex items-center gap-7">
<h2 className="text-[28px] leading-8 tracking-[-0.02em] font-bold">
LLM Settings
</h2>
<SettingsSwitch
testId="advanced-settings-switch"
defaultIsToggled={isAdvancedSettingsSet}
onToggle={onToggleAdvancedMode}
>
Advanced
</SettingsSwitch>
</div>
{!shouldHandleSpecialSaasCase && (
<section
data-testid="llm-settings-section"
className="flex flex-col gap-6"
>
<div className="flex items-center gap-7">
<h2 className="text-[28px] leading-8 tracking-[-0.02em] font-bold">
LLM Settings
</h2>
{!shouldHandleSpecialSaasCase && (
<SettingsSwitch
testId="advanced-settings-switch"
defaultIsToggled={isAdvancedSettingsSet}
onToggle={onToggleAdvancedMode}
>
Advanced
</SettingsSwitch>
)}
</div>
{llmConfigMode === "basic" && (
<ModelSelector
models={modelsAndProviders}
currentModel={settings.LLM_MODEL}
/>
)}
{llmConfigMode === "basic" && !shouldHandleSpecialSaasCase && (
<ModelSelector
models={modelsAndProviders}
currentModel={settings.LLM_MODEL}
/>
)}
{llmConfigMode === "advanced" && (
<SettingsInput
testId="llm-custom-model-input"
name="llm-custom-model-input"
label="Custom Model"
defaultValue={settings.LLM_MODEL}
placeholder="anthropic/claude-3-5-sonnet-20241022"
type="text"
className="w-[680px]"
/>
)}
{llmConfigMode === "advanced" && (
<SettingsInput
testId="base-url-input"
name="base-url-input"
label="Base URL"
defaultValue={settings.LLM_BASE_URL}
placeholder="https://api.openai.com"
type="text"
className="w-[680px]"
/>
)}
{llmConfigMode === "advanced" && !shouldHandleSpecialSaasCase && (
<SettingsInput
testId="llm-custom-model-input"
name="llm-custom-model-input"
label="Custom Model"
defaultValue={settings.LLM_MODEL}
placeholder="anthropic/claude-3-5-sonnet-20241022"
type="text"
className="w-[680px]"
/>
)}
{llmConfigMode === "advanced" && !shouldHandleSpecialSaasCase && (
<SettingsInput
testId="base-url-input"
name="base-url-input"
label="Base URL"
defaultValue={settings.LLM_BASE_URL}
placeholder="https://api.openai.com"
type="text"
className="w-[680px]"
/>
)}
<SettingsInput
testId="llm-api-key-input"
name="llm-api-key-input"
label="API Key"
type="password"
className="w-[680px]"
startContent={
isLLMKeySet && <KeyStatusIcon isSet={isLLMKeySet} />
}
placeholder={isLLMKeySet ? "**********" : ""}
/>
{!shouldHandleSpecialSaasCase && (
<SettingsInput
testId="llm-api-key-input"
name="llm-api-key-input"
label="API Key"
type="password"
className="w-[680px]"
startContent={
isLLMKeySet && <KeyStatusIcon isSet={isLLMKeySet} />
}
placeholder={isLLMKeySet ? "**********" : ""}
/>
)}
<HelpLink
testId="llm-api-key-help-anchor"
text="Don't know your API key?"
linkText="Click here for instructions"
href="https://docs.all-hands.dev/modules/usage/llms"
/>
{!shouldHandleSpecialSaasCase && (
<HelpLink
testId="llm-api-key-help-anchor"
text="Don't know your API key?"
linkText="Click here for instructions"
href="https://docs.all-hands.dev/modules/usage/llms"
/>
)}
{llmConfigMode === "advanced" && (
<SettingsDropdownInput
testId="agent-input"
name="agent-input"
label="Agent"
items={
resources?.agents.map((agent) => ({
key: agent,
label: agent,
})) || []
}
defaultSelectedKey={settings.AGENT}
isClearable={false}
/>
)}
{isSaas && llmConfigMode === "advanced" && (
<SettingsDropdownInput
testId="runtime-settings-input"
name="runtime-settings-input"
label={
<>
Runtime Settings (
<a href="mailto:contact@all-hands.dev">
get in touch for access
</a>
)
</>
}
items={REMOTE_RUNTIME_OPTIONS}
defaultSelectedKey={settings.REMOTE_RUNTIME_RESOURCE_FACTOR?.toString()}
isDisabled
isClearable={false}
/>
)}
{llmConfigMode === "advanced" && (
<SettingsSwitch
testId="enable-confirmation-mode-switch"
onToggle={setConfirmationModeIsEnabled}
defaultIsToggled={!!settings.CONFIRMATION_MODE}
isBeta
>
Enable confirmation mode
</SettingsSwitch>
)}
{llmConfigMode === "advanced" && (
<SettingsSwitch
testId="enable-memory-condenser-switch"
name="enable-memory-condenser-switch"
defaultIsToggled={!!settings.ENABLE_DEFAULT_CONDENSER}
>
Enable memory condensation
</SettingsSwitch>
)}
{llmConfigMode === "advanced" && confirmationModeIsEnabled && (
<div>
{llmConfigMode === "advanced" && (
<SettingsDropdownInput
testId="security-analyzer-input"
name="security-analyzer-input"
label="Security Analyzer"
testId="agent-input"
name="agent-input"
label="Agent"
items={
resources?.securityAnalyzers.map((analyzer) => ({
key: analyzer,
label: analyzer,
resources?.agents.map((agent) => ({
key: agent,
label: agent,
})) || []
}
defaultSelectedKey={settings.SECURITY_ANALYZER}
isClearable
showOptionalTag
defaultSelectedKey={settings.AGENT}
isClearable={false}
/>
</div>
)}
</section>
)}
{isSaas && llmConfigMode === "advanced" && (
<SettingsDropdownInput
testId="runtime-settings-input"
name="runtime-settings-input"
label={
<>
Runtime Settings (
<a href="mailto:contact@all-hands.dev">
get in touch for access
</a>
)
</>
}
items={REMOTE_RUNTIME_OPTIONS}
defaultSelectedKey={settings.REMOTE_RUNTIME_RESOURCE_FACTOR?.toString()}
isDisabled
isClearable={false}
/>
)}
{llmConfigMode === "advanced" && (
<SettingsSwitch
testId="enable-confirmation-mode-switch"
onToggle={setConfirmationModeIsEnabled}
defaultIsToggled={!!settings.CONFIRMATION_MODE}
isBeta
>
Enable confirmation mode
</SettingsSwitch>
)}
{llmConfigMode === "advanced" && (
<SettingsSwitch
testId="enable-memory-condenser-switch"
name="enable-memory-condenser-switch"
defaultIsToggled={!!settings.ENABLE_DEFAULT_CONDENSER}
>
Enable memory condensation
</SettingsSwitch>
)}
{llmConfigMode === "advanced" && confirmationModeIsEnabled && (
<div>
<SettingsDropdownInput
testId="security-analyzer-input"
name="security-analyzer-input"
label="Security Analyzer"
items={
resources?.securityAnalyzers.map((analyzer) => ({
key: analyzer,
label: analyzer,
})) || []
}
defaultSelectedKey={settings.SECURITY_ANALYZER}
isClearable
showOptionalTag
/>
</div>
)}
</section>
)}
<section className="flex flex-col gap-6">
<h2 className="text-[28px] leading-8 tracking-[-0.02em] font-bold">

View File

@@ -62,13 +62,12 @@ const messageActions = {
let successPrediction = "";
if (message.args.task_completed === "partial") {
successPrediction =
"The agent thinks that the task was **completed partially**.";
"I believe that the task was **completed partially**.";
} else if (message.args.task_completed === "false") {
successPrediction =
"The agent thinks that the task was **not completed**.";
successPrediction = "I believe that the task was **not completed**.";
} else if (message.args.task_completed === "true") {
successPrediction =
"The agent thinks that the task was **completed successfully**.";
"I believe that the task was **completed successfully**.";
}
if (successPrediction) {
// if final_thought is not empty, add a new line before the success prediction

View File

@@ -15,6 +15,11 @@ export const DEFAULT_SETTINGS: Settings = {
ENABLE_DEFAULT_CONDENSER: true,
ENABLE_SOUND_NOTIFICATIONS: false,
USER_CONSENTS_TO_ANALYTICS: false,
PROVIDER_TOKENS: {
github: "",
gitlab: "",
},
IS_NEW_USER: true,
};
/**

View File

@@ -21,7 +21,6 @@ export interface InitConfig {
LLM_MODEL: string;
};
token?: string;
github_token?: string;
latest_event_id?: unknown; // Not sure what this is
}

View File

@@ -1,3 +1,5 @@
export type Provider = "github" | "gitlab";
export type Settings = {
LLM_MODEL: string;
LLM_BASE_URL: string;
@@ -11,6 +13,8 @@ export type Settings = {
ENABLE_DEFAULT_CONDENSER: boolean;
ENABLE_SOUND_NOTIFICATIONS: boolean;
USER_CONSENTS_TO_ANALYTICS: boolean | null;
PROVIDER_TOKENS: Record<Provider, string>;
IS_NEW_USER?: boolean;
};
export type ApiSettings = {
@@ -26,16 +30,17 @@ export type ApiSettings = {
enable_default_condenser: boolean;
enable_sound_notifications: boolean;
user_consents_to_analytics: boolean | null;
provider_tokens: Record<Provider, string>;
};
export type PostSettings = Settings & {
github_token: string;
provider_tokens: Record<Provider, string>;
unset_github_token: boolean;
user_consents_to_analytics: boolean | null;
};
export type PostApiSettings = ApiSettings & {
github_token: string;
provider_tokens: Record<Provider, string>;
unset_github_token: boolean;
user_consents_to_analytics: boolean | null;
};

View File

@@ -1,25 +0,0 @@
import toast from "react-hot-toast";
export const displayErrorToast = (error: string) => {
toast.error(error, {
position: "top-right",
style: {
background: "#454545",
border: "1px solid #717888",
color: "#fff",
borderRadius: "4px",
},
});
};
export const displaySuccessToast = (message: string) => {
toast.success(message, {
position: "top-right",
style: {
background: "#454545",
border: "1px solid #717888",
color: "#fff",
borderRadius: "4px",
},
});
};

View File

@@ -0,0 +1,22 @@
import { CSSProperties } from "react";
import toast, { ToastOptions } from "react-hot-toast";
const TOAST_STYLE: CSSProperties = {
background: "#454545",
border: "1px solid #717888",
color: "#fff",
borderRadius: "4px",
};
const TOAST_OPTIONS: ToastOptions = {
position: "top-right",
style: TOAST_STYLE,
};
export const displayErrorToast = (error: string) => {
toast.error(error, TOAST_OPTIONS);
};
export const displaySuccessToast = (message: string) => {
toast.success(message, TOAST_OPTIONS);
};

View File

@@ -1,6 +1,6 @@
import posthog from "posthog-js";
import toast from "react-hot-toast";
import { handleStatusMessage } from "#/services/actions";
import { displayErrorToast } from "./custom-toast-handlers";
interface ErrorDetails {
message: string;
@@ -23,7 +23,7 @@ export function showErrorToast({
metadata = {},
}: ErrorDetails) {
trackError({ message, source, metadata });
toast.error(message);
displayErrorToast(message);
}
export function showChatError({

View File

@@ -13,3 +13,4 @@ function loadFeatureFlag(
}
export const BILLING_SETTINGS = () => loadFeatureFlag("BILLING_SETTINGS");
export const HIDE_LLM_SETTINGS = () => loadFeatureFlag("HIDE_LLM_SETTINGS");

View File

@@ -59,6 +59,18 @@ export const extractSettings = (formData: FormData): Partial<Settings> => {
ENABLE_DEFAULT_CONDENSER,
} = extractAdvancedFormData(formData);
// Extract provider tokens
const githubToken = formData.get("github-token")?.toString();
const gitlabToken = formData.get("gitlab-token")?.toString();
const providerTokens: Record<string, string> = {};
if (githubToken) {
providerTokens.github = githubToken;
}
if (gitlabToken) {
providerTokens.gitlab = gitlabToken;
}
return {
LLM_MODEL: CUSTOM_LLM_MODEL || LLM_MODEL,
LLM_API_KEY,
@@ -68,5 +80,6 @@ export const extractSettings = (formData: FormData): Partial<Settings> => {
CONFIRMATION_MODE,
SECURITY_ANALYZER,
ENABLE_DEFAULT_CONDENSER,
PROVIDER_TOKENS: providerTokens,
};
};

View File

@@ -17,6 +17,7 @@ export default {
tertiary: "#454545", // gray, used for inputs
"tertiary-light": "#B7BDC2", // lighter gray, used for borders and placeholder text
content: "#ECEDEE", // light gray, used mostly for text
"content-2": "#F9FBFE",
},
},
},

View File

@@ -14,6 +14,8 @@ the GitHub API.
You can use `curl` with the `GITHUB_TOKEN` to interact with GitHub's API.
ALWAYS use the GitHub API for operations instead of a web browser.
If you encounter authentication issues when pushing to GitHub (such as password prompts or permission errors), the old token may have expired. In such case, update the remote URL to include the current token: `git remote set-url origin https://${GITHUB_TOKEN}@github.com/username/repo.git`
Here are some instructions for pushing, but ONLY do this if the user asks you to:
* NEVER push directly to the `main` or `master` branch
* Git config (username and email) is pre-set. Do not modify.

View File

@@ -0,0 +1,83 @@
---
name: swift-linux
type: knowledge
agent: CodeActAgent
version: 1.0.0
triggers:
- swift-linux
- swift-debian
- swift-installation
---
# Swift Installation Guide for Debian Linux
This document provides instructions for installing Swift on Debian 12 (Bookworm).
> This setup is intended for non-UI development tasks on Swift on Linux.
## Prerequisites
Before installing Swift, you need to install the required dependencies for your system. You can find the most up-to-date list of dependencies for your specific Linux distribution and version at the [Swift.org tarball installation guide](https://www.swift.org/install/linux/tarball/).
FOR EXAMPLE, the dependencies you may need to install for Debian 12 could be:
```bash
sudo apt-get update
sudo apt-get install -y \
binutils-gold \
gcc \
git \
libcurl4-openssl-dev \
libedit-dev \
libicu-dev \
libncurses-dev \
libpython3-dev \
libsqlite3-dev \
libxml2-dev \
pkg-config \
tzdata \
uuid-dev
```
## Download and Install Swift
1. Find the latest Swift version for Debian:
Go to the [Swift.org download page](https://www.swift.org/download/) to find the latest Swift version compatible with Debian 12 (Bookworm).
Look for a tarball named something like `swift-<VERSION>-RELEASE-debian12.tar.gz` (e.g., `swift-6.0.3-RELEASE-debian12.tar.gz`).
The URL pattern is typically:
```
https://download.swift.org/swift-<VERSION>-release/debian12/swift-<VERSION>-RELEASE/swift-<VERSION>-RELEASE-debian12.tar.gz
```
Where `<VERSION>` is the Swift version number (e.g., `6.0.3`).
2. Download the Swift binary for Debian 12:
```bash
cd /workspace
wget https://download.swift.org/swift-6.0.3-release/debian12/swift-6.0.3-RELEASE/swift-6.0.3-RELEASE-debian12.tar.gz
```
3. Extract the archive:
> **Note**: Make sure to install Swift in the `/workspace` directory, but outside the git repository to avoid committing the Swift binaries.
4. Add Swift to your PATH by adding the following line to your `~/.bashrc` file:
```bash
echo 'export PATH=/workspace/swift-6.0.3-RELEASE-debian12/usr/bin:$PATH' >> ~/.bashrc
source ~/.bashrc
```
> **Note**: Make sure to update the version number in the PATH to match the version you downloaded.
## Verify Installation
Verify that Swift is correctly installed by running:
```bash
swift --version
```

View File

@@ -1,9 +1,5 @@
from dotenv import load_dotenv
from openhands.agenthub.micro.agent import MicroAgent
from openhands.agenthub.micro.registry import all_microagents
from openhands.controller.agent import Agent
load_dotenv()
@@ -14,26 +10,13 @@ from openhands.agenthub import ( # noqa: E402
dummy_agent,
visualbrowsing_agent,
)
from openhands.controller.agent import Agent # noqa: E402
__all__ = [
'Agent',
'codeact_agent',
'delegator_agent',
'dummy_agent',
'browsing_agent',
'visualbrowsing_agent',
]
for agent in all_microagents.values():
name = agent['name']
prompt = agent['prompt']
anon_class = type(
name,
(MicroAgent,),
{
'prompt': prompt,
'agent_definition': agent,
},
)
Agent.register(name, anon_class)

View File

@@ -1,11 +1,63 @@
You are OpenHands agent, a helpful AI assistant that can interact with a computer to solve tasks.
<IMPORTANT>
* If user provides a path, you should NOT assume it's relative to the current working directory. Instead, you should explore the file system to find the file before working on it.
<ROLE>
Your primary role is to assist users by executing commands, modifying code, and solving technical problems effectively. You should be thorough, methodical, and prioritize quality over speed.
* If the user asks a question, like "why is X happening", don't try to fix the problem. Just give an answer to the question.
</ROLE>
<EFFICIENCY>
* Each action you take is somewhat expensive. Wherever possible, combine multiple actions into a single action, e.g. combine multiple bash commands into one, using sed and grep to edit/view multiple files at once.
* When exploring the codebase, use efficient tools like find, grep, and git commands with appropriate filters to minimize unnecessary operations.
</EFFICIENCY>
<FILE_SYSTEM_GUIDELINES>
* When a user provides a file path, do NOT assume it's relative to the current working directory. First explore the file system to locate the file before working on it.
* If asked to edit a file, edit the file directly, rather than creating a new file with a different filename.
* For global search-and-replace operations, consider using `sed` instead of opening file editors multiple times.
</FILE_SYSTEM_GUIDELINES>
<CODE_QUALITY>
* Write clean, efficient code with minimal comments. Avoid redundancy in comments: Do not repeat information that can be easily inferred from the code itself.
* When implementing solutions, focus on making the minimal changes needed to solve the problem.
* Before implementing any changes, first thoroughly understand the codebase through exploration.
* If you are adding a lot of code to a function or file, consider splitting the function or file into smaller pieces when appropriate.
</CODE_QUALITY>
<VERSION_CONTROL>
* When configuring git credentials, use "openhands" as the user.name and "openhands@all-hands.dev" as the user.email by default, unless explicitly instructed otherwise.
* You MUST NOT include comments in the code unless they are necessary to describe non-obvious behavior.
* If the user asks you to edit a file, you should edit the file directly, do NOT create a new file with the updated content unless the user explicitly instructs you to do so.
* When you are doing global search-and-replace, consider using `sed` instead of running file editor multiple times.
* Only use GITHUB_TOKEN and other credentials in ways that the user has asked for and would expect. Do NOT make potentially dangerous changes (e.g. pushing to main, deleting a repository) unless explicitly asked to do so.
* Exercise caution with git operations. Do NOT make potentially dangerous changes (e.g., pushing to main, deleting repositories) unless explicitly asked to do so.
</VERSION_CONTROL>
<PROBLEM_SOLVING_WORKFLOW>
1. EXPLORATION: Thoroughly explore relevant files and understand the context before proposing solutions
2. ANALYSIS: Consider multiple approaches and select the most promising one
3. TESTING:
* For bug fixes: Create tests to verify issues before implementing fixes
* For new features: Consider test-driven development when appropriate
* If the repository lacks testing infrastructure and implementing tests would require extensive setup, consult with the user before investing time in building testing infrastructure
4. IMPLEMENTATION: Make focused, minimal changes to address the problem
5. VERIFICATION: Test your implementation thoroughly, including edge cases
</PROBLEM_SOLVING_WORKFLOW>
<SECURITY>
* Only use GITHUB_TOKEN and other credentials in ways the user has explicitly requested and would expect.
* Use APIs to work with GitHub or other platforms, unless the user asks otherwise or your task requires browsing.
* If you've made repeated attempts to solve a problem, but the tests won't pass or the user says it's still broken, reflect on 5-7 different possible sources of the problem. Assess the likelihood of these options, and proceed with fixing the most likely one.
</IMPORTANT>
</SECURITY>
<ENVIRONMENT_SETUP>
* When user asks you to run an application, don't stop if the application is not installed. Instead, please install the application and run the command again.
* If you encounter missing dependencies:
1. First, look around in the repository for existing dependency files (requirements.txt, pyproject.toml, package.json, Gemfile, etc.)
2. If dependency files exist, use them to install all dependencies at once (e.g., `pip install -r requirements.txt`, `npm install`, etc.)
3. Only install individual packages directly if no dependency files are found or if only specific packages are needed
* Similarly, if you encounter missing dependencies for essential tools requested by the user, install them when possible.
</ENVIRONMENT_SETUP>
<TROUBLESHOOTING>
* If you've made repeated attempts to solve a problem but tests still fail or the user reports it's still broken:
1. Step back and reflect on 5-7 different possible sources of the problem
2. Assess the likelihood of each possible cause
3. Methodically address the most likely causes, starting with the highest probability
4. Document your reasoning process
* When you run into any major issue while executing a plan from the user, please don't try to directly work around it. Instead, propose a new plan and confirm with the user before proceeding.
</TROUBLESHOOTING>

View File

@@ -1,9 +1,25 @@
from litellm import ChatCompletionToolParam, ChatCompletionToolParamFunctionChunk
_BASH_DESCRIPTION = """Execute a bash command in the terminal.
* Long running commands: For commands that may run indefinitely, it should be run in the background and the output should be redirected to a file, e.g. command = `python3 app.py > server.log 2>&1 &`.
* Interact with running process: If a bash command returns exit code `-1`, this means the process is not yet finished. By setting `is_input` to `true`, the assistant can interact with the running process and send empty `command` to retrieve any additional logs, or send additional text (set `command` to the text) to STDIN of the running process, or send command like `C-c` (Ctrl+C), `C-d` (Ctrl+D), `C-z` (Ctrl+Z) to interrupt the process.
* One command at a time: You can only execute one bash command at a time. If you need to run multiple commands sequentially, you can use `&&` or `;` to chain them together.
_BASH_DESCRIPTION = """Execute a bash command in the terminal within a persistent shell session.
### Command Execution
* One command at a time: You can only execute one bash command at a time. If you need to run multiple commands sequentially, use `&&` or `;` to chain them together.
* Persistent session: Commands execute in a persistent shell session where environment variables, virtual environments, and working directory persist between commands.
* Timeout: Commands have a soft timeout of 120 seconds, once that's reached, you have the option to continue or interrupt the command (see section below for details)
### Running and Interacting with Processes
* Long running commands: For commands that may run indefinitely, run them in the background and redirect output to a file, e.g. `python3 app.py > server.log 2>&1 &`.
* Interact with running process: If a bash command returns exit code `-1`, this means the process is not yet finished. By setting `is_input` to `true`, you can:
- Send empty `command` to retrieve additional logs
- Send text (set `command` to the text) to STDIN of the running process
- Send control commands like `C-c` (Ctrl+C), `C-d` (Ctrl+D), or `C-z` (Ctrl+Z) to interrupt the process
### Best Practices
* Directory verification: Before creating new directories or files, first verify the parent directory exists and is the correct location.
* Directory management: Try to maintain working directory by using absolute paths and avoiding excessive use of `cd`.
### Output Handling
* Output truncation: If the output exceeds a maximum length, it will be truncated before being returned.
"""
CmdRunTool = ChatCompletionToolParam(

View File

@@ -7,10 +7,28 @@ _STR_REPLACE_EDITOR_DESCRIPTION = """Custom editing tool for viewing, creating a
* If a `command` generates a long output, it will be truncated and marked with `<response clipped>`
* The `undo_edit` command will revert the last edit made to the file at `path`
Notes for using the `str_replace` command:
* The `old_str` parameter should match EXACTLY one or more consecutive lines from the original file. Be mindful of whitespaces!
* If the `old_str` parameter is not unique in the file, the replacement will not be performed. Make sure to include enough context in `old_str` to make it unique
* The `new_str` parameter should contain the edited lines that should replace the `old_str`
Before using this tool:
1. Use the view tool to understand the file's contents and context
2. Verify the directory path is correct (only applicable when creating new files):
- Use the view tool to verify the parent directory exists and is the correct location
When making edits:
- Ensure the edit results in idiomatic, correct code
- Do not leave the code in a broken state
- Always use absolute file paths (starting with /)
CRITICAL REQUIREMENTS FOR USING THIS TOOL:
1. EXACT MATCHING: The `old_str` parameter must match EXACTLY one or more consecutive lines from the file, including all whitespace and indentation. The tool will fail if `old_str` matches multiple locations or doesn't match exactly with the file content.
2. UNIQUENESS: The `old_str` must uniquely identify a single instance in the file:
- Include sufficient context before and after the change point (3-5 lines recommended)
- If not unique, the replacement will not be performed
3. REPLACEMENT: The `new_str` parameter should contain the edited lines that replace the `old_str`. Both strings must be different.
Remember: when making multiple file edits in a row to the same file, you should prefer to send all edits in a single message with multiple calls to this tool, rather than multiple messages with a single call each.
"""
StrReplaceEditorTool = ChatCompletionToolParam(

View File

@@ -1,17 +0,0 @@
## Introduction
This package contains definitions of micro-agents. A micro-agent is defined
in the following structure:
```
[AgentName]
├── agent.yaml
└── prompt.md
```
Note that `prompt.md` could use jinja2 template syntax. During runtime, `prompt.md`
is loaded and rendered, and used together with `agent.yaml` to initialize a
micro-agent.
Micro-agents can be used independently. You can also use `ManagerAgent` which knows
how to coordinate the agents and collaboratively finish a task.

View File

@@ -1,2 +0,0 @@
* `browse` - opens a web page. Arguments:
* `url` - the URL to open

View File

@@ -1,3 +0,0 @@
* `delegate` - send a task to another agent from the list provided. Arguments:
* `agent` - the agent to which the task is delegated. MUST match a name in the list of agents provided.
* `inputs` - a dictionary of input parameters to the agent, as specified in the list

View File

@@ -1,2 +0,0 @@
* `finish` - if you're absolutely certain that you've completed your task, use the finish action to stop working. Arguments:
* `outputs` - a dictionary representing the outputs of your task, if any

View File

@@ -1,3 +0,0 @@
* `message` - make a plan, set a goal, record your thoughts, or ask for more input from the user. Arguments:
* `content` - the thought to record
* `wait_for_response` - set to `true` to wait for the user to respond before proceeding

View File

@@ -1,2 +0,0 @@
* `read` - reads the content of a file. Arguments:
* `path` - the path of the file to read

View File

@@ -1,2 +0,0 @@
* `reject` - reject the task. Arguments:
* `outputs` - a dictionary with only a `reason` attribute

View File

@@ -1,2 +0,0 @@
* `run` - runs a command on the command line in a Linux shell. Arguments:
* `command` - the command to run

View File

@@ -1,3 +0,0 @@
* `write` - writes the content to a file. Arguments:
* `path` - the path of the file to write
* `content` - the content to write to the file

View File

@@ -1,5 +0,0 @@
Your response MUST be in JSON format. It must be an object, and it must contain two fields:
* `action`, which is one of the actions specified here
* `args`, which is a map of key-value pairs, specifying the arguments for that action
You MUST NOT include any other text besides the JSON response

View File

@@ -1,4 +0,0 @@
Here is a recent history of actions you've taken in service of this plan,
as well as observations you've made. This only includes the MOST RECENT
actions and observations--more may have happened before that.
They are time-ordered, with your most recent action at the bottom.

View File

@@ -1,88 +0,0 @@
from jinja2 import BaseLoader, Environment
from openhands.agenthub.micro.instructions import instructions
from openhands.agenthub.micro.registry import all_microagents
from openhands.controller.agent import Agent
from openhands.controller.state.state import State
from openhands.core.config import AgentConfig
from openhands.core.message import ImageContent, Message, TextContent
from openhands.events.action import Action
from openhands.events.event import Event
from openhands.events.serialization.action import action_from_dict
from openhands.events.serialization.event import event_to_memory
from openhands.io import json
from openhands.llm.llm import LLM
def parse_response(orig_response: str) -> Action:
# attempt to load the JSON dict from the response
action_dict = json.loads(orig_response)
# load the action from the dict
return action_from_dict(action_dict)
def to_json(obj: object, **kwargs: dict) -> str:
"""Serialize an object to str format"""
return json.dumps(obj, **kwargs)
class MicroAgent(Agent):
VERSION = '1.0'
prompt = ''
agent_definition: dict = {}
def history_to_json(
self, history: list[Event], max_events: int = 20, **kwargs: dict
) -> str:
"""
Serialize and simplify history to str format
"""
processed_history = []
event_count = 0
for event in reversed(history):
if event_count >= max_events:
break
processed_history.append(
event_to_memory(event, self.llm.config.max_message_chars)
)
event_count += 1
# history is in reverse order, let's fix it
processed_history.reverse()
# everything starts with a message
# the first message is already in the prompt as the task
# TODO: so we don't need to include it in the history
return json.dumps(processed_history, **kwargs)
def __init__(self, llm: LLM, config: AgentConfig):
super().__init__(llm, config)
if 'name' not in self.agent_definition:
raise ValueError('Agent definition must contain a name')
self.prompt_template = Environment(loader=BaseLoader()).from_string(self.prompt)
self.delegates = all_microagents.copy()
del self.delegates[self.agent_definition['name']]
def step(self, state: State) -> Action:
last_user_message, last_image_urls = state.get_current_user_intent()
prompt = self.prompt_template.render(
state=state,
instructions=instructions,
to_json=to_json,
history_to_json=self.history_to_json,
delegates=self.delegates,
latest_user_message=last_user_message,
)
content: list[TextContent | ImageContent] = [TextContent(text=prompt)]
if self.llm.vision_is_active() and last_image_urls:
content.append(ImageContent(image_urls=last_image_urls))
message = Message(role='user', content=content)
resp = self.llm.completion(
messages=self.llm.format_messages_for_llm(message),
)
action_resp = resp['choices'][0]['message']['content']
action = parse_response(action_resp)
return action

View File

@@ -1,6 +0,0 @@
name: CoderAgent
description: Given a particular task, and a detailed description of the codebase, accomplishes the task
inputs:
task: string
summary: string
outputs: {}

View File

@@ -1,27 +0,0 @@
# Task
You are a software engineer. You've inherited an existing codebase, which you
need to modify to complete this task:
{{ state.inputs.task }}
{% if state.inputs.summary %}
Here's a summary of the codebase, as it relates to this task:
{{ state.inputs.summary }}
{% endif %}
## Available Actions
{{ instructions.actions.run }}
{{ instructions.actions.write }}
{{ instructions.actions.read }}
{{ instructions.actions.message }}
{{ instructions.actions.finish }}
Do NOT finish until you have completed the tasks.
## History
{{ instructions.history_truncated }}
{{ history_to_json(state.history, max_events=20) }}
## Format
{{ instructions.format.action }}

View File

@@ -1,25 +0,0 @@
## Introduction
CommitWriterAgent can help write git commit message. Example:
```bash
WORKSPACE_MOUNT_PATH="`PWD`" \
poetry run python openhands/core/main.py -t "dummy task" -c CommitWriterAgent -d ./
```
This agent is special in the sense that it doesn't need a task. Once called,
it attempts to read all diff in the git staging area and write a good commit
message.
## Future work
### Feedback loop
The commit message could be (optionally) shown to the customer or
other agents, so that CommitWriterAgent could gather feedback to further
improve the commit message.
### Task rejection
When the agent cannot compile a commit message (e.g. not git repository), it
should reject the task with an explanation.

View File

@@ -1,6 +0,0 @@
name: CommitWriterAgent
description: "Write a git commit message for files in the git staging area"
inputs: {}
outputs:
answer: string
reason: string

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