mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
5 Commits
0.19.0
...
feature/ru
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
14bec04b4b | ||
|
|
3bdaf0abaa | ||
|
|
d0ba5882a0 | ||
|
|
a1b40f1550 | ||
|
|
8f14520a87 |
2
.github/workflows/dummy-agent-test.yml
vendored
2
.github/workflows/dummy-agent-test.yml
vendored
@@ -36,8 +36,6 @@ jobs:
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Install tmux
|
||||
run: sudo apt-get update && sudo apt-get install -y tmux
|
||||
- name: Install poetry via pipx
|
||||
run: pipx install poetry
|
||||
- name: Set up Python
|
||||
|
||||
2
.github/workflows/eval-runner.yml
vendored
2
.github/workflows/eval-runner.yml
vendored
@@ -29,8 +29,6 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install tmux
|
||||
run: sudo apt-get update && sudo apt-get install -y tmux
|
||||
- name: Install poetry via pipx
|
||||
run: pipx install poetry
|
||||
|
||||
|
||||
4
.github/workflows/ghcr-build.yml
vendored
4
.github/workflows/ghcr-build.yml
vendored
@@ -56,7 +56,7 @@ jobs:
|
||||
docker-images: false
|
||||
swap-storage: true
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3.2.0
|
||||
uses: docker/setup-qemu-action@v3.0.0
|
||||
with:
|
||||
image: tonistiigi/binfmt:latest
|
||||
- name: Login to GHCR
|
||||
@@ -119,7 +119,7 @@ jobs:
|
||||
docker-images: false
|
||||
swap-storage: true
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3.2.0
|
||||
uses: docker/setup-qemu-action@v3.0.0
|
||||
with:
|
||||
image: tonistiigi/binfmt:latest
|
||||
- name: Login to GHCR
|
||||
|
||||
2
.github/workflows/py-unit-tests-mac.yml
vendored
2
.github/workflows/py-unit-tests-mac.yml
vendored
@@ -31,8 +31,6 @@ jobs:
|
||||
key: ${{ runner.os }}-poetry-${{ hashFiles('**/poetry.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-poetry-
|
||||
- name: Install tmux
|
||||
run: brew install tmux
|
||||
- name: Install poetry via pipx
|
||||
run: pipx install poetry
|
||||
- name: Install Python dependencies using Poetry
|
||||
|
||||
2
.github/workflows/py-unit-tests.yml
vendored
2
.github/workflows/py-unit-tests.yml
vendored
@@ -30,8 +30,6 @@ jobs:
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Install tmux
|
||||
run: sudo apt-get update && sudo apt-get install -y tmux
|
||||
- name: Install poetry via pipx
|
||||
run: pipx install poetry
|
||||
- name: Set up Python
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
---
|
||||
name: repo
|
||||
type: repo
|
||||
agent: CodeActAgent
|
||||
agent: CodeAct
|
||||
---
|
||||
This repository contains the code for OpenHands, an automated AI software engineer. It has a Python backend
|
||||
(in the `openhands` directory) and React frontend (in the `frontend` directory).
|
||||
|
||||
4
Makefile
4
Makefile
@@ -106,7 +106,7 @@ check-poetry:
|
||||
@if command -v poetry > /dev/null; then \
|
||||
POETRY_VERSION=$(shell poetry --version 2>&1 | sed -E 's/Poetry \(version ([0-9]+\.[0-9]+\.[0-9]+)\)/\1/'); \
|
||||
IFS='.' read -r -a POETRY_VERSION_ARRAY <<< "$$POETRY_VERSION"; \
|
||||
if [ $${POETRY_VERSION_ARRAY[0]} -gt 1 ] || ([ $${POETRY_VERSION_ARRAY[0]} -eq 1 ] && [ $${POETRY_VERSION_ARRAY[1]} -ge 8 ]); then \
|
||||
if [ $${POETRY_VERSION_ARRAY[0]} -ge 1 ] && [ $${POETRY_VERSION_ARRAY[1]} -ge 8 ]; then \
|
||||
echo "$(BLUE)$(shell poetry --version) is already installed.$(RESET)"; \
|
||||
else \
|
||||
echo "$(RED)Poetry 1.8 or later is required. You can install poetry by running the following command, then adding Poetry to your PATH:"; \
|
||||
@@ -190,7 +190,7 @@ build-frontend:
|
||||
# Start backend
|
||||
start-backend:
|
||||
@echo "$(YELLOW)Starting backend...$(RESET)"
|
||||
@poetry run uvicorn openhands.server.listen:app --host $(BACKEND_HOST) --port $(BACKEND_PORT) --reload --reload-exclude "./workspace"
|
||||
@poetry run uvicorn openhands.server.listen:app --host $(BACKEND_HOST) --port $(BACKEND_PORT) --reload --reload-exclude "$(shell pwd)/workspace"
|
||||
|
||||
# Start frontend
|
||||
start-frontend:
|
||||
|
||||
@@ -180,12 +180,6 @@ model = "gpt-4o"
|
||||
# https://docs.litellm.ai/docs/completion/token_usage
|
||||
#custom_tokenizer = ""
|
||||
|
||||
# Whether to use native tool calling if supported by the model. Can be true, false, or None by default, which chooses the model's default behavior based on the evaluation.
|
||||
# ATTENTION: Based on evaluation, enabling native function calling may lead to worse results
|
||||
# in some scenarios. Use with caution and consider testing with your specific use case.
|
||||
# https://github.com/All-Hands-AI/OpenHands/pull/4711
|
||||
#native_tool_calling = None
|
||||
|
||||
[llm.gpt4o-mini]
|
||||
api_key = "your-api-key"
|
||||
model = "gpt-4o"
|
||||
@@ -198,16 +192,6 @@ model = "gpt-4o"
|
||||
# agent.CodeActAgent
|
||||
##############################################################################
|
||||
[agent]
|
||||
|
||||
# whether the browsing tool is enabled
|
||||
codeact_enable_browsing = true
|
||||
|
||||
# whether the LLM draft editor is enabled
|
||||
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 = ""
|
||||
|
||||
@@ -220,12 +204,6 @@ codeact_enable_jupyter = true
|
||||
# LLM config group to use
|
||||
#llm_config = 'your-llm-config-group'
|
||||
|
||||
# Whether to use microagents at all
|
||||
#use_microagents = true
|
||||
|
||||
# List of microagents to disable
|
||||
#disabled_microagents = []
|
||||
|
||||
[agent.RepoExplorerAgent]
|
||||
# Example: use a cheaper model for RepoExplorerAgent to reduce cost, especially
|
||||
# useful when an agent doesn't demand high quality but uses a lot of tokens
|
||||
|
||||
@@ -71,7 +71,6 @@ ENV VIRTUAL_ENV=/app/.venv \
|
||||
COPY --chown=openhands:app --chmod=770 --from=backend-builder ${VIRTUAL_ENV} ${VIRTUAL_ENV}
|
||||
RUN playwright install --with-deps chromium
|
||||
|
||||
COPY --chown=openhands:app --chmod=770 ./microagents ./microagents
|
||||
COPY --chown=openhands:app --chmod=770 ./openhands ./openhands
|
||||
COPY --chown=openhands:app --chmod=777 ./openhands/runtime/plugins ./openhands/runtime/plugins
|
||||
COPY --chown=openhands:app --chmod=770 ./openhands/agenthub ./openhands/agenthub
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
# Documentation Style Guide
|
||||
|
||||
## General Writing Principles
|
||||
|
||||
- **Clarity & Conciseness**: Always prioritize clarity and brevity. Avoid unnecessary jargon or overly complex explanations.
|
||||
Keep sentences short and to the point.
|
||||
- **Gradual Complexity**: Start with the simplest, most basic setup, and then gradually introduce more advanced
|
||||
concepts and configurations.
|
||||
|
||||
## Formatting Guidelines
|
||||
|
||||
### Headers
|
||||
|
||||
Use **Title Case** for the first and second level headers.
|
||||
|
||||
Example:
|
||||
- **Basic Usage**
|
||||
- **Advanced Configuration Options**
|
||||
|
||||
### Lists
|
||||
|
||||
When listing items or options, use bullet points to enhance readability.
|
||||
|
||||
Example:
|
||||
- Option A
|
||||
- Option B
|
||||
- Option C
|
||||
|
||||
### Procedures
|
||||
|
||||
For instructions or processes that need to be followed in a specific order, use numbered steps.
|
||||
|
||||
Example:
|
||||
1. Step one: Do this.
|
||||
2. Step two: Complete this action.
|
||||
3. Step three: Verify the result.
|
||||
|
||||
### Code Blocks
|
||||
|
||||
* Use code blocks for multi-line inputs, outputs, commands and code samples.
|
||||
|
||||
Example:
|
||||
```bash
|
||||
docker run -it \
|
||||
-e THIS=this \
|
||||
-e THAT=that
|
||||
...
|
||||
```
|
||||
@@ -4,9 +4,10 @@
|
||||
|
||||
Achieving full replication of production-grade applications with LLMs is a complex endeavor. Our strategy involves:
|
||||
|
||||
- **Core Technical Research:** Focusing on foundational research to understand and improve the technical aspects of code generation and handling.
|
||||
- **Task Planning:** Developing capabilities for bug detection, codebase management, and optimization.
|
||||
- **Evaluation:** Establishing comprehensive evaluation metrics to better understand and improve our agents.
|
||||
1. **Core Technical Research:** Focusing on foundational research to understand and improve the technical aspects of code generation and handling
|
||||
2. **Specialist Abilities:** Enhancing the effectiveness of core components through data curation, training methods, and more
|
||||
3. **Task Planning:** Developing capabilities for bug detection, codebase management, and optimization
|
||||
4. **Evaluation:** Establishing comprehensive evaluation metrics to better understand and improve our models
|
||||
|
||||
## Default Agent
|
||||
|
||||
@@ -14,14 +15,11 @@ Our default Agent is currently the [CodeActAgent](agents), which is capable of g
|
||||
|
||||
## Built With
|
||||
|
||||
OpenHands is built using a combination of powerful frameworks and libraries, providing a robust foundation for its
|
||||
development. Here are the key technologies used in the project:
|
||||
OpenHands is built using a combination of powerful frameworks and libraries, providing a robust foundation for its development. Here are the key technologies used in the project:
|
||||
|
||||
       
|
||||
|
||||
Please note that the selection of these technologies is in progress, and additional technologies may be added or
|
||||
existing ones may be removed as the project evolves. We strive to adopt the most suitable and efficient tools to
|
||||
enhance the capabilities of OpenHands.
|
||||
Please note that the selection of these technologies is in progress, and additional technologies may be added or existing ones may be removed as the project evolves. We strive to adopt the most suitable and efficient tools to enhance the capabilities of OpenHands.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ take precedence.
|
||||
|
||||
# Table of Contents
|
||||
|
||||
- [Core Configuration](#core-configuration)
|
||||
1. [Core Configuration](#core-configuration)
|
||||
- [API Keys](#api-keys)
|
||||
- [Workspace](#workspace)
|
||||
- [Debugging and Logging](#debugging-and-logging)
|
||||
@@ -21,7 +21,7 @@ take precedence.
|
||||
- [Task Management](#task-management)
|
||||
- [Sandbox Configuration](#sandbox-configuration)
|
||||
- [Miscellaneous](#miscellaneous)
|
||||
- [LLM Configuration](#llm-configuration)
|
||||
2. [LLM Configuration](#llm-configuration)
|
||||
- [AWS Credentials](#aws-credentials)
|
||||
- [API Configuration](#api-configuration)
|
||||
- [Custom LLM Provider](#custom-llm-provider)
|
||||
@@ -30,20 +30,20 @@ take precedence.
|
||||
- [Model Selection](#model-selection)
|
||||
- [Retrying](#retrying)
|
||||
- [Advanced Options](#advanced-options)
|
||||
- [Agent Configuration](#agent-configuration)
|
||||
3. [Agent Configuration](#agent-configuration)
|
||||
- [Microagent Configuration](#microagent-configuration)
|
||||
- [Memory Configuration](#memory-configuration)
|
||||
- [LLM Configuration](#llm-configuration-2)
|
||||
- [ActionSpace Configuration](#actionspace-configuration)
|
||||
- [Microagent Usage](#microagent-usage)
|
||||
- [Sandbox Configuration](#sandbox-configuration)
|
||||
4. [Sandbox Configuration](#sandbox-configuration-2)
|
||||
- [Execution](#execution)
|
||||
- [Container Image](#container-image)
|
||||
- [Networking](#networking)
|
||||
- [Linting and Plugins](#linting-and-plugins)
|
||||
- [Dependencies and Environment](#dependencies-and-environment)
|
||||
- [Evaluation](#evaluation)
|
||||
- [Security Configuration](#security-configuration)
|
||||
5. [Security Configuration](#security-configuration)
|
||||
- [Confirmation Mode](#confirmation-mode)
|
||||
- [Security Analyzer](#security-analyzer)
|
||||
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
# ✅ Providing Feedback
|
||||
|
||||
When using OpenHands, you will encounter cases where things work well, and others where they don't. We encourage you to
|
||||
provide feedback when you use OpenHands to help give feedback to the development team, and perhaps more importantly,
|
||||
create an open corpus of coding agent training examples -- Share-OpenHands!
|
||||
When using OpenHands, you will encounter cases where things work well, and others where they don't. We encourage you to provide feedback when you use OpenHands to help give feedback to the development team, and perhaps more importantly, create an open corpus of coding agent training examples -- Share-OpenHands!
|
||||
|
||||
## 📝 How to Provide Feedback
|
||||
|
||||
Providing feedback is easy! When you are using OpenHands, you can press the thumbs-up or thumbs-down button at any point
|
||||
during your interaction. You will be prompted to provide your email address
|
||||
(e.g. so we can contact you if we want to ask any follow-up questions), and you can choose whether you want to provide feedback publicly or privately.
|
||||
Providing feedback is easy! When you are using OpenHands, you can press the thumbs-up or thumbs-down button at any point during your interaction. You will be prompted to provide your email address (e.g. so we can contact you if we want to ask any follow-up questions), and you can choose whether you want to provide feedback publicly or privately.
|
||||
|
||||
<iframe width="560" height="315" src="https://www.youtube.com/embed/5rFx-StMVV0?si=svo7xzp6LhGK_GXr" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
|
||||
|
||||
@@ -18,11 +14,8 @@ during your interaction. You will be prompted to provide your email address
|
||||
|
||||
When you submit data, you can submit it either publicly or privately.
|
||||
|
||||
- **Public** data will be distributed under the MIT License, like OpenHands itself, and can be used by the community to
|
||||
train and test models. Obviously, feedback that you can make public will be more valuable for the community as a whole,
|
||||
so when you are not dealing with sensitive information, we would encourage you to choose this option!
|
||||
- **Private** data will be made available to the OpenHands team for the purpose of improving OpenHands.
|
||||
However, a link with a unique ID will still be created that you can share publicly with others.
|
||||
* **Public** data will be distributed under the MIT License, like OpenHands itself, and can be used by the community to train and test models. Obviously, feedback that you can make public will be more valuable for the community as a whole, so when you are not dealing with sensitive information, we would encourage you to choose this option!
|
||||
* **Private** data will only be shared with the OpenHands team for the purpose of improving OpenHands.
|
||||
|
||||
### Who collects and stores the data?
|
||||
|
||||
@@ -34,17 +27,13 @@ The public data will be released when we hit fixed milestones, such as 1,000 pub
|
||||
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.
|
||||
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.
|
||||
|
||||
### What if I want my data deleted?
|
||||
|
||||
For data on the All Hands AI servers, we are happy to delete it at request:
|
||||
|
||||
**One Piece of Data:** If you want one piece of data deleted, we will shortly be adding a mechanism to delete pieces of
|
||||
data using the link and password that is displayed on the interface when you submit data.
|
||||
**One Piece of Data:** If you want one piece of data deleted, we will shortly be adding a mechanism to delete pieces of data using the link and password that is displayed on the interface when you submit data.
|
||||
|
||||
**All Data:** If you would like all pieces of your data deleted, or you do not have the ID and password that you
|
||||
received when submitting the data, please contact `contact@all-hands.dev` from the email address that you registered
|
||||
when you originally submitted the data.
|
||||
**All Data:** If you would like all pieces of your data deleted, or you do not have the ID and password that you received when submitting the data, please contact `contact@all-hands.dev` from the email address that you registered when you originally submitted the data.
|
||||
|
||||
@@ -44,7 +44,7 @@ For example, we might build a TODO app:
|
||||
|
||||
We can keep iterating on the app once the skeleton is there:
|
||||
|
||||
> Please allow adding an optional due date to every task.
|
||||
> Please allow adding an optional due date to every task
|
||||
|
||||
Just like with normal development, it's good to commit and push your code frequently.
|
||||
This way you can always revert back to an old state if the agent goes off track.
|
||||
@@ -59,15 +59,15 @@ OpenHands can also do a great job adding new code to an existing code base.
|
||||
|
||||
For example, you can ask OpenHands to add a new GitHub action to your project
|
||||
which lints your code. OpenHands may take a peek at your codebase to see what language
|
||||
it should use and then drop a new file into `./github/workflows/lint.yml`.
|
||||
it should use, but then it can just drop a new file into `./github/workflows/lint.yml`
|
||||
|
||||
> Please add a GitHub action that lints the code in this repository.
|
||||
> Please add a GitHub action that lints the code in this repository
|
||||
|
||||
Some tasks might require a bit more context. While OpenHands can use `ls` and `grep`
|
||||
to search through your codebase, providing context up front allows it to move faster,
|
||||
and more accurately. And it'll cost you fewer tokens!
|
||||
|
||||
> Please modify ./backend/api/routes.js to add a new route that returns a list of all tasks.
|
||||
> Please modify ./backend/api/routes.js to add a new route that returns a list of all tasks
|
||||
|
||||
> Please add a new React component that displays a list of Widgets to the ./frontend/components
|
||||
> directory. It should use the existing Widget component.
|
||||
@@ -78,15 +78,15 @@ OpenHands does great at refactoring existing code, especially in small chunks.
|
||||
You probably don't want to try rearchitecting your whole codebase, but breaking up
|
||||
long files and functions, renaming variables, etc. tend to work very well.
|
||||
|
||||
> Please rename all the single-letter variables in ./app.go.
|
||||
> Please rename all the single-letter variables in ./app.go
|
||||
|
||||
> Please break the function `build_and_deploy_widgets` into two functions, `build_widgets` and `deploy_widgets` in widget.php.
|
||||
> Please break the function `build_and_deploy_widgets` into two functions, `build_widgets` and `deploy_widgets` in widget.php
|
||||
|
||||
> Please break ./api/routes.js into separate files for each route.
|
||||
> Please break ./api/routes.js into separate files for each route
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
OpenHands can also help you track down and fix bugs in your code. But as any
|
||||
OpenHands can also help you track down and fix bugs in your code. But, as any
|
||||
developer knows, bug fixing can be extremely tricky, and often OpenHands will need more context.
|
||||
It helps if you've diagnosed the bug, but want OpenHands to figure out the logic.
|
||||
|
||||
@@ -94,18 +94,18 @@ It helps if you've diagnosed the bug, but want OpenHands to figure out the logic
|
||||
|
||||
> The `search_widgets` function in ./app.py is doing a case-sensitive search. Please make it case-insensitive.
|
||||
|
||||
It often helps to do test-driven development when bug fixing with an agent.
|
||||
It often helps to do test-driven development when bugfixing with an agent.
|
||||
You can ask the agent to write a new test, and then iterate until it fixes the bug:
|
||||
|
||||
> The `hello` function crashes on the empty string. Please write a test that reproduces this bug, then fix the code so it passes.
|
||||
|
||||
## More
|
||||
|
||||
OpenHands is capable of helping out on just about any coding task but it takes some practice
|
||||
OpenHands is capable of helping out on just about any coding task. But it takes some practice
|
||||
to get the most out of it. Remember to:
|
||||
* Keep your tasks small.
|
||||
* Be as specific as possible.
|
||||
* Provide as much context as possible.
|
||||
* Commit and push frequently.
|
||||
* Keep your tasks small
|
||||
* Be as specific as possible
|
||||
* Provide as much context as possible
|
||||
* Commit and push frequently
|
||||
|
||||
See [Prompting Best Practices](./prompting/prompting-best-practices) for more tips on how to get the most out of OpenHands.
|
||||
|
||||
@@ -26,9 +26,9 @@ To run OpenHands in CLI mode with Docker:
|
||||
|
||||
1. Set the following environmental variables in your terminal:
|
||||
|
||||
- `WORKSPACE_BASE` to the directory you want OpenHands to edit (Ex: `export WORKSPACE_BASE=$(pwd)/workspace`).
|
||||
- `LLM_MODEL` to the model to use (Ex: `export LLM_MODEL="anthropic/claude-3-5-sonnet-20241022"`).
|
||||
- `LLM_API_KEY` to the API key (Ex: `export LLM_API_KEY="sk_test_12345"`).
|
||||
* `WORKSPACE_BASE` to the directory you want OpenHands to edit (Ex: `export WORKSPACE_BASE=$(pwd)/workspace`).
|
||||
* `LLM_MODEL` to the model to use (Ex: `export LLM_MODEL="anthropic/claude-3-5-sonnet-20241022"`).
|
||||
* `LLM_API_KEY` to the API key (Ex: `export LLM_API_KEY="sk_test_12345"`).
|
||||
|
||||
2. Run the following Docker command:
|
||||
|
||||
|
||||
@@ -9,8 +9,8 @@ as python and Node.js but may need other software installed by default.
|
||||
|
||||
You have two options for customization:
|
||||
|
||||
- Use an existing image with the required software.
|
||||
- Create your own custom Docker image.
|
||||
1. Use an existing image with the required software.
|
||||
2. Create your own custom Docker image.
|
||||
|
||||
If you choose the first option, you can skip the `Create Your Docker Image` section.
|
||||
|
||||
@@ -58,3 +58,7 @@ sandbox_base_container_image="custom-image"
|
||||
### Run
|
||||
|
||||
Run OpenHands by running ```make run``` in the top level directory.
|
||||
|
||||
## Technical Explanation
|
||||
|
||||
Please refer to [custom docker image section of the runtime documentation](https://docs.all-hands.dev/modules/usage/architecture/runtime#advanced-how-openhands-builds-and-maintains-od-runtime-images) for more details.
|
||||
|
||||
@@ -21,10 +21,10 @@ the [README for the OpenHands Resolver](https://github.com/All-Hands-AI/OpenHand
|
||||
### Iterative resolution
|
||||
|
||||
1. Create an issue in the repository.
|
||||
2. Add the `fix-me` label to the issue, or leave a comment starting with `@openhands-agent`.
|
||||
3. Review the attempt to resolve the issue by checking the pull request.
|
||||
4. Follow up with feedback through general comments, review comments, or inline thread comments.
|
||||
5. Add the `fix-me` label to the pull request, or address a specific comment by starting with `@openhands-agent`.
|
||||
2. Add the `fix-me` label to the issue, or leave a comment starting with `@openhands-agent`
|
||||
3. Review the attempt to resolve the issue by checking the pull request
|
||||
4. Follow up with feedback through general comments, review comments, or inline thread comments
|
||||
5. Add the `fix-me` label to the pull request, or address a specific comment by starting with `@openhands-agent`
|
||||
|
||||
### Label versus Macro
|
||||
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
|
||||
## Introduction
|
||||
|
||||
OpenHands provides a user-friendly Graphical User Interface (GUI) mode for interacting with the AI assistant.
|
||||
This mode offers an intuitive way to set up the environment, manage settings, and communicate with the AI.
|
||||
OpenHands provides a user-friendly Graphical User Interface (GUI) mode for interacting with the AI assistant. This mode offers an intuitive way to set up the environment, manage settings, and communicate with the AI.
|
||||
|
||||
## Installation and Setup
|
||||
|
||||
1. Follow the instructions in the [Installation](../installation) guide to install OpenHands.
|
||||
|
||||
2. After running the command, access OpenHands at [http://localhost:3000](http://localhost:3000).
|
||||
|
||||
## Interacting with the GUI
|
||||
@@ -23,39 +23,39 @@ This mode offers an intuitive way to set up the environment, manage settings, an
|
||||
|
||||
OpenHands automatically exports a `GITHUB_TOKEN` to the shell environment if it is available. This can happen in two ways:
|
||||
|
||||
- **Locally (OSS)**: The user directly inputs their GitHub token.
|
||||
- **Online (SaaS)**: The token is obtained through GitHub OAuth authentication.
|
||||
1. **Locally (OSS)**: The user directly inputs their GitHub token
|
||||
2. **Online (SaaS)**: The token is obtained through GitHub OAuth authentication
|
||||
|
||||
#### Setting Up a Local GitHub Token
|
||||
|
||||
1. **Generate a Personal Access Token (PAT)**:
|
||||
- Go to GitHub Settings > Developer Settings > Personal Access Tokens > Tokens (classic).
|
||||
- Click "Generate new token (classic)".
|
||||
- Go to GitHub Settings > Developer Settings > Personal Access Tokens > Tokens (classic)
|
||||
- Click "Generate new token (classic)"
|
||||
- Required scopes:
|
||||
- `repo` (Full control of private repositories)
|
||||
- `workflow` (Update GitHub Action workflows)
|
||||
- `read:org` (Read organization data)
|
||||
|
||||
2. **Enter Token in OpenHands**:
|
||||
- Click the Settings button (gear icon) in the top right.
|
||||
- Navigate to the "GitHub" section.
|
||||
- Paste your token in the "GitHub Token" field.
|
||||
- Click "Save" to apply the changes.
|
||||
- Click the Settings button (gear icon) in the top right
|
||||
- Navigate to the "GitHub" section
|
||||
- Paste your token in the "GitHub Token" field
|
||||
- Click "Save" to apply the changes
|
||||
|
||||
#### Organizational Token Policies
|
||||
|
||||
If you're working with organizational repositories, additional setup may be required:
|
||||
|
||||
1. **Check Organization Requirements**:
|
||||
- Organization admins may enforce specific token policies.
|
||||
- Some organizations require tokens to be created with SSO enabled.
|
||||
- Review your organization's [token policy settings](https://docs.github.com/en/organizations/managing-programmatic-access-to-your-organization/setting-a-personal-access-token-policy-for-your-organization).
|
||||
- Organization admins may enforce specific token policies
|
||||
- Some organizations require tokens to be created with SSO enabled
|
||||
- Review your organization's [token policy settings](https://docs.github.com/en/organizations/managing-programmatic-access-to-your-organization/setting-a-personal-access-token-policy-for-your-organization)
|
||||
|
||||
2. **Verify Organization Access**:
|
||||
- Go to your token settings on GitHub.
|
||||
- Look for the organization under "Organization access".
|
||||
- If required, click "Enable SSO" next to your organization.
|
||||
- Complete the SSO authorization process.
|
||||
- Go to your token settings on GitHub
|
||||
- Look for the organization under "Organization access"
|
||||
- If required, click "Enable SSO" next to your organization
|
||||
- Complete the SSO authorization process
|
||||
|
||||
#### OAuth Authentication (Online Mode)
|
||||
|
||||
@@ -67,31 +67,31 @@ When using OpenHands in online mode, the GitHub OAuth flow:
|
||||
- Organization read access
|
||||
|
||||
2. Authentication steps:
|
||||
- Click "Sign in with GitHub" when prompted.
|
||||
- Review the requested permissions.
|
||||
- Authorize OpenHands to access your GitHub account.
|
||||
- If using an organization, authorize organization access if prompted.
|
||||
- Click "Sign in with GitHub" when prompted
|
||||
- Review the requested permissions
|
||||
- Authorize OpenHands to access your GitHub account
|
||||
- If using an organization, authorize organization access if prompted
|
||||
|
||||
#### Troubleshooting
|
||||
|
||||
Common issues and solutions:
|
||||
|
||||
1. **Token Not Recognized**:
|
||||
- Ensure the token is properly saved in settings.
|
||||
- Check that the token hasn't expired.
|
||||
- Verify the token has the required scopes.
|
||||
- Try regenerating the token.
|
||||
- Ensure the token is properly saved in settings
|
||||
- Check that the token hasn't expired
|
||||
- Verify the token has the required scopes
|
||||
- Try regenerating the token
|
||||
|
||||
2. **Organization Access Denied**:
|
||||
- Check if SSO is required but not enabled.
|
||||
- Verify organization membership.
|
||||
- Contact organization admin if token policies are blocking access.
|
||||
- Check if SSO is required but not enabled
|
||||
- Verify organization membership
|
||||
- Contact organization admin if token policies are blocking access
|
||||
|
||||
3. **Verifying Token Works**:
|
||||
- The app will show a green checkmark if the token is valid.
|
||||
- Try accessing a repository to confirm permissions.
|
||||
- Check the browser console for any error messages.
|
||||
- Use the "Test Connection" button in settings if available.
|
||||
- The app will show a green checkmark if the token is valid
|
||||
- Try accessing a repository to confirm permissions
|
||||
- Check the browser console for any error messages
|
||||
- Use the "Test Connection" button in settings if available
|
||||
|
||||
### Advanced Settings
|
||||
|
||||
@@ -103,11 +103,11 @@ Common issues and solutions:
|
||||
|
||||
The main interface consists of several key components:
|
||||
|
||||
- **Chat Window**: The central area where you can view the conversation history with the AI assistant.
|
||||
- **Input Box**: Located at the bottom of the screen, use this to type your messages or commands to the AI.
|
||||
- **Send Button**: Click this to send your message to the AI.
|
||||
- **Settings Button**: A gear icon that opens the settings modal, allowing you to adjust your configuration at any time.
|
||||
- **Workspace Panel**: Displays the files and folders in your workspace, allowing you to navigate and view files, or the agent's past commands or web browsing history.
|
||||
1. **Chat Window**: The central area where you can view the conversation history with the AI assistant.
|
||||
2. **Input Box**: Located at the bottom of the screen, use this to type your messages or commands to the AI.
|
||||
3. **Send Button**: Click this to send your message to the AI.
|
||||
4. **Settings Button**: A gear icon that opens the settings modal, allowing you to adjust your configuration at any time.
|
||||
5. **Workspace Panel**: Displays the files and folders in your workspace, allowing you to navigate and view files, or the agent's past commands or web browsing history.
|
||||
|
||||
### Interacting with the AI
|
||||
|
||||
@@ -118,9 +118,8 @@ The main interface consists of several key components:
|
||||
|
||||
## Tips for Effective Use
|
||||
|
||||
- Be specific in your requests to get the most accurate and helpful responses, as described in the [prompting best practices](../prompting/prompting-best-practices).
|
||||
- Use the workspace panel to explore your project structure.
|
||||
- Use one of the recommended models, as described in the [LLMs section](usage/llms/llms.md).
|
||||
1. Be specific in your requests to get the most accurate and helpful responses, as described in the [prompting best practices](../prompting/prompting-best-practices).
|
||||
2. Use the workspace panel to explore your project structure.
|
||||
3. Use one of the recommended models, as described in the [LLMs section](usage/llms/llms.md).
|
||||
|
||||
Remember, the GUI mode of OpenHands is designed to make your interaction with the AI assistant as smooth and intuitive
|
||||
as possible. Don't hesitate to explore its features to maximize your productivity.
|
||||
Remember, the GUI mode of OpenHands is designed to make your interaction with the AI assistant as smooth and intuitive as possible. Don't hesitate to explore its features to maximize your productivity.
|
||||
|
||||
@@ -23,9 +23,9 @@ To run OpenHands in Headless mode with Docker:
|
||||
|
||||
1. Set the following environmental variables in your terminal:
|
||||
|
||||
- `WORKSPACE_BASE` to the directory you want OpenHands to edit (Ex: `export WORKSPACE_BASE=$(pwd)/workspace`).
|
||||
- `LLM_MODEL` to the model to use (Ex: `export LLM_MODEL="anthropic/claude-3-5-sonnet-20241022"`).
|
||||
- `LLM_API_KEY` to the API key (Ex: `export LLM_API_KEY="sk_test_12345"`).
|
||||
* `WORKSPACE_BASE` to the directory you want OpenHands to edit (Ex: `export WORKSPACE_BASE=$(pwd)/workspace`).
|
||||
* `LLM_MODEL` to the model to use (Ex: `export LLM_MODEL="anthropic/claude-3-5-sonnet-20241022"`).
|
||||
* `LLM_API_KEY` to the API key (Ex: `export LLM_API_KEY="sk_test_12345"`).
|
||||
|
||||
2. Run the following Docker command:
|
||||
|
||||
@@ -53,4 +53,4 @@ To view all available configuration options for headless mode, run the Python co
|
||||
|
||||
### Additional Logs
|
||||
|
||||
For the headless mode to log all the agent actions, in the terminal run: `export LOG_ALL_EVENTS=true`
|
||||
For the headless mode to log all the agent actions, in your terminal run: `export LOG_ALL_EVENTS=true`
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Persisting Session Data
|
||||
|
||||
Using the standard Development Workflow, the session data is stored in memory. Currently, if OpenHands' service is restarted,
|
||||
Using the standard installation, the session data is stored in memory. Currently, if OpenHands' service is restarted,
|
||||
previous sessions become invalid (a new secret is generated) and thus not recoverable.
|
||||
|
||||
## How to Persist Session Data
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
## System Requirements
|
||||
|
||||
- Docker version 26.0.0+ or Docker Desktop 4.31.0+.
|
||||
- You must be using Linux or Mac OS.
|
||||
- If you are on Windows, you must use [WSL](https://learn.microsoft.com/en-us/windows/wsl/install).
|
||||
* Docker version 26.0.0+ or Docker Desktop 4.31.0+.
|
||||
* You must be using Linux or Mac OS.
|
||||
* If you are on Windows, you must use [WSL](https://learn.microsoft.com/en-us/windows/wsl/install).
|
||||
|
||||
## Start the app
|
||||
|
||||
@@ -33,6 +33,8 @@ or run it on tagged issues with [a github action](https://docs.all-hands.dev/mod
|
||||
|
||||
## Setup
|
||||
|
||||
After running the command above, you'll find OpenHands running at [http://localhost:3000](http://localhost:3000).
|
||||
|
||||
Upon launching OpenHands, you'll see a settings modal. You **must** select an `LLM Provider` and `LLM Model` and enter a corresponding `API Key`.
|
||||
These can be changed at any time by selecting the `Settings` button (gear icon) in the UI.
|
||||
|
||||
|
||||
@@ -18,18 +18,17 @@ docker run -it --pull=always \
|
||||
...
|
||||
```
|
||||
|
||||
Then in the OpenHands UI Settings:
|
||||
Then set the following in the OpenHands UI through the Settings:
|
||||
|
||||
:::note
|
||||
You will need your ChatGPT deployment name which can be found on the deployments page in Azure. This is referenced as
|
||||
<deployment-name> below.
|
||||
:::
|
||||
|
||||
1. Enable `Advanced Options`
|
||||
2. Set the following:
|
||||
- `Custom Model` to azure/<deployment-name>
|
||||
- `Base URL` to your Azure API Base URL (e.g. `https://example-endpoint.openai.azure.com`)
|
||||
- `API Key` to your Azure API key
|
||||
* Enable `Advanced Options`
|
||||
* `Custom Model` to azure/<deployment-name>
|
||||
* `Base URL` to your Azure API Base URL (e.g. `https://example-endpoint.openai.azure.com`)
|
||||
* `API Key` to your Azure API key
|
||||
|
||||
## Embeddings
|
||||
|
||||
|
||||
@@ -8,10 +8,10 @@ OpenHands uses LiteLLM to make calls to Google's chat models. You can find their
|
||||
## Gemini - Google AI Studio Configs
|
||||
|
||||
When running OpenHands, you'll need to set the following in the OpenHands UI through the Settings:
|
||||
- `LLM Provider` to `Gemini`
|
||||
- `LLM Model` to the model you will be using.
|
||||
* `LLM Provider` to `Gemini`
|
||||
* `LLM Model` to the model you will be using.
|
||||
If the model is not in the list, toggle `Advanced Options`, and enter it in `Custom Model` (e.g. gemini/<model-name> like `gemini/gemini-1.5-pro`).
|
||||
- `API Key` to your Gemini API key
|
||||
* `API Key` to your Gemini API key
|
||||
|
||||
## VertexAI - Google Cloud Platform Configs
|
||||
|
||||
@@ -25,6 +25,6 @@ VERTEXAI_LOCATION="<your-gcp-location>"
|
||||
```
|
||||
|
||||
Then set the following in the OpenHands UI through the Settings:
|
||||
- `LLM Provider` to `VertexAI`
|
||||
- `LLM Model` to the model you will be using.
|
||||
* `LLM Provider` to `VertexAI`
|
||||
* `LLM Model` to the model you will be using.
|
||||
If the model is not in the list, toggle `Advanced Options`, and enter it in `Custom Model` (e.g. vertex_ai/<model-name>).
|
||||
|
||||
@@ -5,20 +5,19 @@ OpenHands uses LiteLLM to make calls to chat models on Groq. You can find their
|
||||
## Configuration
|
||||
|
||||
When running OpenHands, you'll need to set the following in the OpenHands UI through the Settings:
|
||||
- `LLM Provider` to `Groq`
|
||||
- `LLM Model` to the model you will be using. [Visit here to see the list of
|
||||
* `LLM Provider` to `Groq`
|
||||
* `LLM Model` to the model you will be using. [Visit here to see the list of
|
||||
models that Groq hosts](https://console.groq.com/docs/models). If the model is not in the list, toggle
|
||||
`Advanced Options`, and enter it in `Custom Model` (e.g. groq/<model-name> like `groq/llama3-70b-8192`).
|
||||
- `API key` to your Groq API key. To find or create your Groq API Key, [see here](https://console.groq.com/keys).
|
||||
* `API key` to your Groq API key. To find or create your Groq API Key, [see here](https://console.groq.com/keys).
|
||||
|
||||
|
||||
|
||||
## Using Groq as an OpenAI-Compatible Endpoint
|
||||
|
||||
The Groq endpoint for chat completion is [mostly OpenAI-compatible](https://console.groq.com/docs/openai). Therefore, you can access Groq models as you
|
||||
would access any OpenAI-compatible endpoint. In the OpenHands UI through the Settings:
|
||||
1. Enable `Advanced Options`
|
||||
2. Set the following:
|
||||
- `Custom Model` to the prefix `openai/` + the model you will be using (e.g. `openai/llama3-70b-8192`)
|
||||
- `Base URL` to `https://api.groq.com/openai/v1`
|
||||
- `API Key` to your Groq API key
|
||||
would access any OpenAI-compatible endpoint. You can set the following in the OpenHands UI through the Settings:
|
||||
* Enable `Advanced Options`
|
||||
* `Custom Model` to the prefix `openai/` + the model you will be using (e.g. `openai/llama3-70b-8192`)
|
||||
* `Base URL` to `https://api.groq.com/openai/v1`
|
||||
* `API Key` to your Groq API key
|
||||
|
||||
@@ -4,9 +4,7 @@ OpenHands can connect to any LLM supported by LiteLLM. However, it requires a po
|
||||
|
||||
## Model Recommendations
|
||||
|
||||
Based on our evaluations of language models for coding tasks (using the SWE-bench dataset), we can provide some
|
||||
recommendations for model selection. Some analyses can be found in [this blog article comparing LLMs](https://www.all-hands.dev/blog/evaluation-of-llms-as-coding-agents-on-swe-bench-at-30x-speed) and
|
||||
[this blog article with some more recent results](https://www.all-hands.dev/blog/openhands-codeact-21-an-open-state-of-the-art-software-development-agent).
|
||||
Based on our evaluations of language models for coding tasks (using the SWE-bench dataset), we can provide some recommendations for model selection. Some analyses can be found in [this blog article comparing LLMs](https://www.all-hands.dev/blog/evaluation-of-llms-as-coding-agents-on-swe-bench-at-30x-speed) and [this blog article with some more recent results](https://www.all-hands.dev/blog/openhands-codeact-21-an-open-state-of-the-art-software-development-agent).
|
||||
|
||||
When choosing a model, consider both the quality of outputs and the associated costs. Here's a summary of the findings:
|
||||
|
||||
@@ -71,11 +69,9 @@ We have a few guides for running OpenHands with specific model providers:
|
||||
|
||||
### API retries and rate limits
|
||||
|
||||
LLM providers typically have rate limits, sometimes very low, and may require retries. OpenHands will automatically
|
||||
retry requests if it receives a Rate Limit Error (429 error code), API connection error, or other transient errors.
|
||||
LLM providers typically have rate limits, sometimes very low, and may require retries. OpenHands will automatically retry requests if it receives a Rate Limit Error (429 error code), API connection error, or other transient errors.
|
||||
|
||||
You can customize these options as you need for the provider you're using. Check their documentation, and set the
|
||||
following environment variables to control the number of retries and the time between retries:
|
||||
You can customize these options as you need for the provider you're using. Check their documentation, and set the following environment variables to control the number of retries and the time between retries:
|
||||
|
||||
- `LLM_NUM_RETRIES` (Default of 8)
|
||||
- `LLM_RETRY_MIN_WAIT` (Default of 15 seconds)
|
||||
|
||||
@@ -17,9 +17,8 @@ Just as for OpenAI Chat completions, we use LiteLLM for OpenAI-compatible endpoi
|
||||
|
||||
## Using an OpenAI Proxy
|
||||
|
||||
If you're using an OpenAI proxy, in the OpenHands UI through the Settings:
|
||||
1. Enable `Advanced Options`
|
||||
2. Set the following:
|
||||
- `Custom Model` to openai/<model-name> (e.g. `openai/gpt-4o` or openai/<proxy-prefix>/<model-name>)
|
||||
- `Base URL` to the URL of your OpenAI proxy
|
||||
- `API Key` to your OpenAI API key
|
||||
If you're using an OpenAI proxy, you'll need to set the following in the OpenHands UI through the Settings:
|
||||
* Enable `Advanced Options`
|
||||
* `Custom Model` to openai/<model-name> (e.g. `openai/gpt-4o` or openai/<proxy-prefix>/<model-name>)
|
||||
* `Base URL` to the URL of your OpenAI proxy
|
||||
* `API Key` to your OpenAI API key
|
||||
|
||||
@@ -9,11 +9,11 @@ You can customize OpenHands' behavior for your repository by creating a `.openha
|
||||
be given to the agent every time it works with this repository.
|
||||
|
||||
We suggest including the following information:
|
||||
- **Repository Overview**: A brief description of your project's purpose and architecture.
|
||||
- **Directory Structure**: Key directories and their purposes.
|
||||
- **Development Guidelines**: Project-specific coding standards and practices.
|
||||
- **Testing Requirements**: How to run tests and what types of tests are required.
|
||||
- **Setup Instructions**: Steps needed to build and run the project.
|
||||
1. **Repository Overview**: A brief description of your project's purpose and architecture
|
||||
2. **Directory Structure**: Key directories and their purposes
|
||||
3. **Development Guidelines**: Project-specific coding standards and practices
|
||||
4. **Testing Requirements**: How to run tests and what types of tests are required
|
||||
5. **Setup Instructions**: Steps needed to build and run the project
|
||||
|
||||
### Example Repository Configuration
|
||||
Example `.openhands/microagents/repo.md` file:
|
||||
@@ -39,11 +39,11 @@ Guidelines:
|
||||
|
||||
### Customizing Prompts
|
||||
|
||||
When working with a repository:
|
||||
When working with a customized repository:
|
||||
|
||||
- **Reference Project Standards**: Mention specific coding standards or patterns used in your project.
|
||||
- **Include Context**: Reference relevant documentation or existing implementations.
|
||||
- **Specify Testing Requirements**: Include project-specific testing requirements in your prompts.
|
||||
1. **Reference Project Standards**: Mention specific coding standards or patterns used in your project
|
||||
2. **Include Context**: Reference relevant documentation or existing implementations
|
||||
3. **Specify Testing Requirements**: Include project-specific testing requirements in your prompts
|
||||
|
||||
Example customized prompt:
|
||||
```
|
||||
@@ -54,14 +54,14 @@ The component should use our shared styling from src/styles/components.
|
||||
|
||||
### Best Practices for Repository Customization
|
||||
|
||||
- **Keep Instructions Updated**: Regularly update your `.openhands` directory as your project evolves.
|
||||
- **Be Specific**: Include specific paths, patterns, and requirements unique to your project.
|
||||
- **Document Dependencies**: List all tools and dependencies required for development.
|
||||
- **Include Examples**: Provide examples of good code patterns from your project.
|
||||
- **Specify Conventions**: Document naming conventions, file organization, and code style preferences.
|
||||
1. **Keep Instructions Updated**: Regularly update your `.openhands` directory as your project evolves
|
||||
2. **Be Specific**: Include specific paths, patterns, and requirements unique to your project
|
||||
3. **Document Dependencies**: List all tools and dependencies required for development
|
||||
4. **Include Examples**: Provide examples of good code patterns from your project
|
||||
5. **Specify Conventions**: Document naming conventions, file organization, and code style preferences
|
||||
|
||||
By customizing OpenHands for your repository, you'll get more accurate and consistent results that align with your project's standards and requirements.
|
||||
|
||||
## Other Microagents
|
||||
You can create other instructions in the `.openhands/microagents/` directory
|
||||
that will be sent to the agent if a particular keyword is found, like `test`, `frontend`, or `migration`. See [Micro-Agents](microagents.md) for more information.
|
||||
that will be sent to the agent if a particular keyword is found, like `test`, `frontend`, or `migration`. See [Microagents](microagents.md) for more information.
|
||||
|
||||
@@ -6,10 +6,10 @@ OpenHands uses specialized micro-agents to handle specific tasks and contexts ef
|
||||
|
||||
Micro-agents are defined in markdown files under the `openhands/agenthub/codeact_agent/micro/` directory. Each micro-agent is configured with:
|
||||
|
||||
- A unique name.
|
||||
- The agent type (typically CodeActAgent).
|
||||
- Trigger keywords that activate the agent.
|
||||
- Specific instructions and capabilities.
|
||||
- A unique name
|
||||
- The agent type (typically CodeActAgent)
|
||||
- Trigger keywords that activate the agent
|
||||
- Specific instructions and capabilities
|
||||
|
||||
## Available Micro-Agents
|
||||
|
||||
@@ -18,10 +18,10 @@ Micro-agents are defined in markdown files under the `openhands/agenthub/codeact
|
||||
**Triggers**: `github`, `git`
|
||||
|
||||
The GitHub agent specializes in GitHub API interactions and repository management. It:
|
||||
- Has access to a `GITHUB_TOKEN` for API authentication.
|
||||
- Follows strict guidelines for repository interactions.
|
||||
- Handles branch management and pull requests.
|
||||
- Uses the GitHub API instead of web browser interactions.
|
||||
- Has access to a `GITHUB_TOKEN` for API authentication
|
||||
- Follows strict guidelines for repository interactions
|
||||
- Handles branch management and pull requests
|
||||
- Uses the GitHub API instead of web browser interactions
|
||||
|
||||
Key features:
|
||||
- Branch protection (prevents direct pushes to main/master)
|
||||
@@ -34,14 +34,13 @@ Key features:
|
||||
**Triggers**: `npm`
|
||||
|
||||
Specializes in handling npm package management with specific focus on:
|
||||
- Non-interactive shell operations.
|
||||
- Automated confirmation handling using Unix 'yes' command.
|
||||
- Package installation automation.
|
||||
- Non-interactive shell operations
|
||||
- Automated confirmation handling using Unix 'yes' command
|
||||
- Package installation automation
|
||||
|
||||
### Custom Micro-Agents
|
||||
|
||||
You can create your own micro-agents by adding new markdown files to the micro-agents directory.
|
||||
Each file should follow this structure:
|
||||
You can create your own micro-agents by adding new markdown files to the micro-agents directory. Each file should follow this structure:
|
||||
|
||||
```markdown
|
||||
---
|
||||
@@ -58,18 +57,19 @@ Instructions and capabilities for the micro-agent...
|
||||
## Best Practices
|
||||
|
||||
When working with micro-agents:
|
||||
- **Use Appropriate Triggers**: Ensure your commands include the relevant trigger words to activate the correct micro-agent.
|
||||
- **Follow Agent Guidelines**: Each agent has specific instructions and limitations. Respect these for optimal results.
|
||||
- **API-First Approach**: When available, use API endpoints rather than web interfaces.
|
||||
- **Automation Friendly**: Design commands that work well in non-interactive environments.
|
||||
|
||||
1. **Use Appropriate Triggers**: Ensure your commands include the relevant trigger words to activate the correct micro-agent
|
||||
2. **Follow Agent Guidelines**: Each agent has specific instructions and limitations - respect these for optimal results
|
||||
3. **API-First Approach**: When available, use API endpoints rather than web interfaces
|
||||
4. **Automation Friendly**: Design commands that work well in non-interactive environments
|
||||
|
||||
## Integration
|
||||
|
||||
Micro-agents are automatically integrated into OpenHands' workflow. They:
|
||||
- Monitor incoming commands for their trigger words.
|
||||
- Activate when relevant triggers are detected.
|
||||
- Apply their specialized knowledge and capabilities.
|
||||
- Follow their specific guidelines and restrictions.
|
||||
- Monitor incoming commands for their trigger words
|
||||
- Activate when relevant triggers are detected
|
||||
- Apply their specialized knowledge and capabilities
|
||||
- Follow their specific guidelines and restrictions
|
||||
|
||||
## Example Usage
|
||||
|
||||
@@ -105,7 +105,7 @@ Create a new markdown file in `openhands/agenthub/codeact_agent/micro/` with a d
|
||||
|
||||
Your micro-agent file must include:
|
||||
|
||||
- **Front Matter**: YAML metadata at the start of the file:
|
||||
1. **Front Matter**: YAML metadata at the start of the file:
|
||||
```markdown
|
||||
---
|
||||
name: your_agent_name
|
||||
@@ -116,7 +116,7 @@ triggers:
|
||||
---
|
||||
```
|
||||
|
||||
- **Instructions**: Clear, specific guidelines for the agent's behavior:
|
||||
2. **Instructions**: Clear, specific guidelines for the agent's behavior:
|
||||
```markdown
|
||||
You are responsible for [specific task/domain].
|
||||
|
||||
@@ -135,19 +135,19 @@ Examples of usage:
|
||||
|
||||
### 4. Best Practices for Micro-Agent Development
|
||||
|
||||
- **Clear Scope**: Keep the agent focused on a specific domain or task.
|
||||
- **Explicit Instructions**: Provide clear, unambiguous guidelines.
|
||||
- **Useful Examples**: Include practical examples of common use cases.
|
||||
- **Safety First**: Include necessary warnings and constraints.
|
||||
- **Integration Awareness**: Consider how the agent interacts with other components.
|
||||
1. **Clear Scope**: Keep the agent focused on a specific domain or task
|
||||
2. **Explicit Instructions**: Provide clear, unambiguous guidelines
|
||||
3. **Useful Examples**: Include practical examples of common use cases
|
||||
4. **Safety First**: Include necessary warnings and constraints
|
||||
5. **Integration Awareness**: Consider how the agent interacts with other components
|
||||
|
||||
### 5. Testing Your Micro-Agent
|
||||
|
||||
Before submitting:
|
||||
- Test the agent with various prompts.
|
||||
- Verify trigger words activate the agent correctly.
|
||||
- Ensure instructions are clear and comprehensive.
|
||||
- Check for potential conflicts with existing agents.
|
||||
1. Test the agent with various prompts
|
||||
2. Verify trigger words activate the agent correctly
|
||||
3. Ensure instructions are clear and comprehensive
|
||||
4. Check for potential conflicts with existing agents
|
||||
|
||||
### 6. Example Implementation
|
||||
|
||||
@@ -199,12 +199,11 @@ Remember to:
|
||||
|
||||
### 7. Submission Process
|
||||
|
||||
1. Create your micro-agent file in the correct directory.
|
||||
2. Test thoroughly.
|
||||
1. Create your micro-agent file in the correct directory
|
||||
2. Test thoroughly
|
||||
3. Submit a pull request with:
|
||||
- The new micro-agent file.
|
||||
- Updated documentation if needed.
|
||||
- Description of the agent's purpose and capabilities.
|
||||
- The new micro-agent file
|
||||
- Updated documentation if needed
|
||||
- Description of the agent's purpose and capabilities
|
||||
|
||||
Remember that micro-agents are a powerful way to extend OpenHands' capabilities in specific domains. Well-designed
|
||||
agents can significantly improve the system's ability to handle specialized tasks.
|
||||
Remember that micro-agents are a powerful way to extend OpenHands' capabilities in specific domains. Well-designed agents can significantly improve the system's ability to handle specialized tasks.
|
||||
|
||||
@@ -6,31 +6,35 @@ When working with OpenHands AI software developer, it's crucial to provide clear
|
||||
|
||||
Good prompts are:
|
||||
|
||||
- **Concrete**: They explain exactly what functionality should be added or what error needs to be fixed.
|
||||
- **Location-specific**: If known, they explain the locations in the code base that should be modified.
|
||||
- **Appropriately scoped**: They should be the size of a single feature, typically not exceeding 100 lines of code.
|
||||
1. **Concrete**: They explain exactly what functionality should be added or what error needs to be fixed.
|
||||
2. **Location-specific**: If known, they explain the locations in the code base that should be modified.
|
||||
3. **Appropriately scoped**: They should be the size of a single feature, typically not exceeding 100 lines of code.
|
||||
|
||||
## Examples
|
||||
|
||||
### Good Prompt Examples
|
||||
|
||||
- "Add a function `calculate_average` in `utils/math_operations.py` that takes a list of numbers as input and returns their average."
|
||||
- "Fix the TypeError in `frontend/src/components/UserProfile.tsx` occurring on line 42. The error suggests we're trying to access a property of undefined."
|
||||
- "Implement input validation for the email field in the registration form. Update `frontend/src/components/RegistrationForm.tsx` to check if the email is in a valid format before submission."
|
||||
1. "Add a function `calculate_average` in `utils/math_operations.py` that takes a list of numbers as input and returns their average."
|
||||
|
||||
2. "Fix the TypeError in `frontend/src/components/UserProfile.tsx` occurring on line 42. The error suggests we're trying to access a property of undefined."
|
||||
|
||||
3. "Implement input validation for the email field in the registration form. Update `frontend/src/components/RegistrationForm.tsx` to check if the email is in a valid format before submission."
|
||||
|
||||
### Bad Prompt Examples
|
||||
|
||||
- "Make the code better." (Too vague, not concrete)
|
||||
- "Rewrite the entire backend to use a different framework." (Not appropriately scoped)
|
||||
- "There's a bug somewhere in the user authentication. Can you find and fix it?" (Lacks specificity and location information)
|
||||
1. "Make the code better." (Too vague, not concrete)
|
||||
|
||||
2. "Rewrite the entire backend to use a different framework." (Not appropriately scoped)
|
||||
|
||||
3. "There's a bug somewhere in the user authentication. Can you find and fix it?" (Lacks specificity and location information)
|
||||
|
||||
## Tips for Effective Prompting
|
||||
|
||||
- Be as specific as possible about the desired outcome or the problem to be solved.
|
||||
- Provide context, including relevant file paths and line numbers if available.
|
||||
- Break down large tasks into smaller, manageable prompts.
|
||||
- Include any relevant error messages or logs.
|
||||
- Specify the programming language or framework if it's not obvious from the context.
|
||||
1. Be as specific as possible about the desired outcome or the problem to be solved.
|
||||
2. Provide context, including relevant file paths and line numbers if available.
|
||||
3. Break down large tasks into smaller, manageable prompts.
|
||||
4. Include any relevant error messages or logs.
|
||||
5. Specify the programming language or framework if it's not obvious from the context.
|
||||
|
||||
Remember, the more precise and informative your prompt is, the better the AI can assist you in developing or modifying the OpenHands software.
|
||||
|
||||
|
||||
@@ -26,29 +26,30 @@ that contains our Runtime server, as well as some basic utilities for Python and
|
||||
You can also [build your own runtime image](how-to/custom-sandbox-guide).
|
||||
|
||||
### Connecting to Your filesystem
|
||||
One useful feature here is the ability to connect to your local filesystem. To mount your filesystem into the runtime:
|
||||
1. Set `WORKSPACE_BASE`:
|
||||
One useful feature here is the ability to connect to your local filesystem.
|
||||
|
||||
```bash
|
||||
export WORKSPACE_BASE=/path/to/your/code
|
||||
To mount your filesystem into the runtime, first set WORKSPACE_BASE:
|
||||
```bash
|
||||
export WORKSPACE_BASE=/path/to/your/code
|
||||
|
||||
# Linux and Mac Example
|
||||
# export WORKSPACE_BASE=$HOME/OpenHands
|
||||
# Will set $WORKSPACE_BASE to /home/<username>/OpenHands
|
||||
#
|
||||
# WSL on Windows Example
|
||||
# export WORKSPACE_BASE=/mnt/c/dev/OpenHands
|
||||
# Will set $WORKSPACE_BASE to C:\dev\OpenHands
|
||||
```
|
||||
2. Add the following options to the `docker run` command:
|
||||
# Linux and Mac Example
|
||||
# export WORKSPACE_BASE=$HOME/OpenHands
|
||||
# Will set $WORKSPACE_BASE to /home/<username>/OpenHands
|
||||
#
|
||||
# WSL on Windows Example
|
||||
# export WORKSPACE_BASE=/mnt/c/dev/OpenHands
|
||||
# Will set $WORKSPACE_BASE to C:\dev\OpenHands
|
||||
```
|
||||
|
||||
```bash
|
||||
docker run # ...
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||
-v $WORKSPACE_BASE:/opt/workspace_base \
|
||||
# ...
|
||||
```
|
||||
then add the following options to the `docker run` command:
|
||||
|
||||
```bash
|
||||
docker run # ...
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
|
||||
-v $WORKSPACE_BASE:/opt/workspace_base \
|
||||
# ...
|
||||
```
|
||||
|
||||
Be careful! There's nothing stopping the OpenHands agent from deleting or modifying
|
||||
any files that are mounted into its workspace.
|
||||
@@ -58,7 +59,7 @@ 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).
|
||||
the #remote-runtime-limited-beta channel on Slack ([see the README](https://github.com/All-Hands-AI/OpenHands?tab=readme-ov-file#-join-our-community) for an invite).
|
||||
|
||||
To use the All Hands Runtime, set the following environment variables when
|
||||
starting OpenHands:
|
||||
|
||||
1
docs/static/img/backend_architecture.puml
vendored
1
docs/static/img/backend_architecture.puml
vendored
@@ -123,6 +123,7 @@ class openhands.state.State {
|
||||
updated_info: List[Tuple[Action, Observation]]
|
||||
}
|
||||
class openhands.observation.CmdOutputObservation {
|
||||
command_id: int
|
||||
command: str
|
||||
exit_code: int
|
||||
observation: str
|
||||
|
||||
@@ -75,8 +75,6 @@ def get_config(
|
||||
workspace_mount_path=None,
|
||||
)
|
||||
config.set_llm_config(metadata.llm_config)
|
||||
agent_config = config.get_agent_config(metadata.agent_class)
|
||||
agent_config.use_microagents = False
|
||||
return config
|
||||
|
||||
|
||||
|
||||
@@ -59,8 +59,6 @@ def get_config(
|
||||
workspace_mount_path=None,
|
||||
)
|
||||
config.set_llm_config(metadata.llm_config)
|
||||
agent_config = config.get_agent_config(metadata.agent_class)
|
||||
agent_config.use_microagents = False
|
||||
return config
|
||||
|
||||
|
||||
@@ -137,6 +135,7 @@ def complete_runtime(
|
||||
|
||||
action = CmdRunAction(
|
||||
command=f'chmod +x ./{script_name} && ./{script_name}',
|
||||
keep_prompt=False,
|
||||
)
|
||||
logger.info(action, extra={'msg_type': 'ACTION'})
|
||||
obs = runtime.run_action(action)
|
||||
@@ -163,7 +162,8 @@ def complete_runtime(
|
||||
logger.info(f'Running get ground truth cmd: {script_name}')
|
||||
|
||||
action = CmdRunAction(
|
||||
command=f'chmod +x ./{script_name} && ./{script_name}'
|
||||
command=f'chmod +x ./{script_name} && ./{script_name}',
|
||||
keep_prompt=False,
|
||||
)
|
||||
logger.info(action, extra={'msg_type': 'ACTION'})
|
||||
obs = runtime.run_action(action)
|
||||
|
||||
@@ -67,8 +67,6 @@ def get_config(
|
||||
workspace_mount_path=None,
|
||||
)
|
||||
config.set_llm_config(metadata.llm_config)
|
||||
agent_config = config.get_agent_config(metadata.agent_class)
|
||||
agent_config.use_microagents = False
|
||||
|
||||
# copy 'draft_editor' config if exists
|
||||
config_copy = copy.deepcopy(config)
|
||||
@@ -145,7 +143,10 @@ def complete_runtime(
|
||||
)
|
||||
logger.info(f'Running test file: {script_name}')
|
||||
|
||||
action = CmdRunAction(command=f'python3 -m unittest {script_name}')
|
||||
action = CmdRunAction(
|
||||
command=f'python3 -m unittest {script_name}',
|
||||
keep_prompt=False,
|
||||
)
|
||||
logger.info(action, extra={'msg_type': 'ACTION'})
|
||||
obs = runtime.run_action(action)
|
||||
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
|
||||
|
||||
@@ -73,8 +73,6 @@ def get_config(
|
||||
workspace_mount_path=None,
|
||||
)
|
||||
config.set_llm_config(metadata.llm_config)
|
||||
agent_config = config.get_agent_config(metadata.agent_class)
|
||||
agent_config.use_microagents = False
|
||||
return config
|
||||
|
||||
|
||||
@@ -199,7 +197,7 @@ def complete_runtime(
|
||||
if obs.exit_code == 0:
|
||||
test_result['metadata']['1_copy_change_success'] = True
|
||||
|
||||
action = CmdRunAction(command=f'cat {generated_path}')
|
||||
action = CmdRunAction(command=f'cat {generated_path}', keep_prompt=False)
|
||||
logger.info(action, extra={'msg_type': 'ACTION'})
|
||||
obs = runtime.run_action(action)
|
||||
assert obs.exit_code == 0
|
||||
@@ -223,7 +221,9 @@ def complete_runtime(
|
||||
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
|
||||
assert obs.exit_code == 0
|
||||
|
||||
action = CmdRunAction(command='cat /testing_files/results_biocoder.json')
|
||||
action = CmdRunAction(
|
||||
command='cat /testing_files/results_biocoder.json', keep_prompt=False
|
||||
)
|
||||
logger.info(action, extra={'msg_type': 'ACTION'})
|
||||
obs = runtime.run_action(action)
|
||||
if obs.exit_code == 0:
|
||||
|
||||
@@ -127,6 +127,7 @@ For each problem, OpenHands is given a set number of iterations to fix the faili
|
||||
"observation": "run",
|
||||
"content": "california_schools/california_schools.sqlite\r\n[(1.0,)]",
|
||||
"extras": {
|
||||
"command_id": -1,
|
||||
"command": "python3 0.py",
|
||||
"exit_code": 0
|
||||
}
|
||||
|
||||
@@ -86,8 +86,6 @@ def get_config(
|
||||
workspace_mount_path=None,
|
||||
)
|
||||
config.set_llm_config(metadata.llm_config)
|
||||
agent_config = config.get_agent_config(metadata.agent_class)
|
||||
agent_config.use_microagents = False
|
||||
return config
|
||||
|
||||
|
||||
@@ -268,7 +266,10 @@ def initialize_runtime(
|
||||
runtime.copy_to(db_file, '/workspace')
|
||||
|
||||
# Check the database is copied
|
||||
action = CmdRunAction(command='cd /workspace && ls -l')
|
||||
action = CmdRunAction(
|
||||
command='cd /workspace && ls -l',
|
||||
keep_prompt=False,
|
||||
)
|
||||
obs = runtime.run_action(action)
|
||||
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
|
||||
assert obs.exit_code == 0
|
||||
@@ -297,7 +298,10 @@ def complete_runtime(
|
||||
instance_id = instance.instance_id.replace('/', '__')
|
||||
path = os.path.join('/workspace', f'{instance_id}.py')
|
||||
|
||||
action = CmdRunAction(command=f'cat {path}')
|
||||
action = CmdRunAction(
|
||||
command=f'cat {path}',
|
||||
keep_prompt=False,
|
||||
)
|
||||
obs = runtime.run_action(action)
|
||||
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
|
||||
|
||||
|
||||
@@ -50,8 +50,6 @@ def get_config(
|
||||
workspace_mount_path=None,
|
||||
)
|
||||
config.set_llm_config(metadata.llm_config)
|
||||
agent_config = config.get_agent_config(metadata.agent_class)
|
||||
agent_config.use_microagents = False
|
||||
return config
|
||||
|
||||
|
||||
|
||||
@@ -77,8 +77,6 @@ def get_config(
|
||||
workspace_mount_path=None,
|
||||
)
|
||||
config.set_llm_config(metadata.llm_config)
|
||||
agent_config = config.get_agent_config(metadata.agent_class)
|
||||
agent_config.use_microagents = False
|
||||
agent_config = AgentConfig(
|
||||
function_calling=False,
|
||||
codeact_enable_jupyter=True,
|
||||
|
||||
@@ -62,8 +62,6 @@ def get_config(
|
||||
workspace_mount_path=None,
|
||||
)
|
||||
config.set_llm_config(metadata.llm_config)
|
||||
agent_config = config.get_agent_config(metadata.agent_class)
|
||||
agent_config.use_microagents = False
|
||||
return config
|
||||
|
||||
|
||||
|
||||
@@ -55,8 +55,6 @@ def get_config(
|
||||
workspace_mount_path=None,
|
||||
)
|
||||
config.set_llm_config(metadata.llm_config)
|
||||
agent_config = config.get_agent_config(metadata.agent_class)
|
||||
agent_config.use_microagents = False
|
||||
return config
|
||||
|
||||
|
||||
|
||||
@@ -76,8 +76,6 @@ def get_config(
|
||||
workspace_mount_path=None,
|
||||
)
|
||||
config.set_llm_config(metadata.llm_config)
|
||||
agent_config = config.get_agent_config(metadata.agent_class)
|
||||
agent_config.use_microagents = False
|
||||
return config
|
||||
|
||||
|
||||
|
||||
@@ -71,6 +71,7 @@ For each problem, OpenHands is given a set number of iterations to fix the faili
|
||||
"observation": "run",
|
||||
"content": "[File: /workspace/Python__2.py (14 lines total)]\r\n1:def truncate_number(number: float) -> float:\r\n2: return number % 1.0 + 1.0\r\n3:\r\n4:\r\n5:\r\n6:\r\n7:\r\n8:\r\n9:def check(truncate_number):\r\n10: assert truncate_number(3.5) == 0.5\r\n11: assert abs(truncate_number(1.33) - 0.33) < 1e-6\r\n12: assert abs(truncate_number(123.456) - 0.456) < 1e-6\r\n13:\r\n14:check(truncate_number)",
|
||||
"extras": {
|
||||
"command_id": -1,
|
||||
"command": "open Python__2.py",
|
||||
"exit_code": 0
|
||||
}
|
||||
@@ -97,6 +98,7 @@ For each problem, OpenHands is given a set number of iterations to fix the faili
|
||||
"observation": "run",
|
||||
"content": "> > [File: /workspace/Python__2.py (14 lines total)]\r\n1:def truncate_number(number: float) -> float:\r\n2: return number % 1.0\r\n3:\r\n4:\r\n5:\r\n6:\r\n7:\r\n8:\r\n9:def check(truncate_number):\r\n10: assert truncate_number(3.5) == 0.5\r\n11: assert abs(truncate_number(1.33) - 0.33) < 1e-6\r\n12: assert abs(truncate_number(123.456) - 0.456) < 1e-6\r\n13:\r\n14:check(truncate_number)\r\nFile updated. Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.",
|
||||
"extras": {
|
||||
"command_id": -1,
|
||||
"command": "edit 2:2 <<EOF\n return number % 1.0\nEOF",
|
||||
"exit_code": 0
|
||||
}
|
||||
@@ -123,6 +125,7 @@ For each problem, OpenHands is given a set number of iterations to fix the faili
|
||||
"observation": "run",
|
||||
"content": "",
|
||||
"extras": {
|
||||
"command_id": -1,
|
||||
"command": "python3 Python__2.py",
|
||||
"exit_code": 0
|
||||
}
|
||||
|
||||
@@ -97,8 +97,6 @@ def get_config(
|
||||
workspace_mount_path=None,
|
||||
)
|
||||
config.set_llm_config(metadata.llm_config)
|
||||
agent_config = config.get_agent_config(metadata.agent_class)
|
||||
agent_config.use_microagents = False
|
||||
return config
|
||||
|
||||
|
||||
@@ -171,7 +169,9 @@ def complete_runtime(
|
||||
num_workers = LANGUAGE_TO_NUM_WORKERS[language]
|
||||
python_imports = '\n'.join(IMPORT_HELPER[language])
|
||||
|
||||
action = CmdRunAction(command=f'cat /workspace/{_get_instance_id(instance)}.py')
|
||||
action = CmdRunAction(
|
||||
command=f'cat /workspace/{_get_instance_id(instance)}.py', keep_prompt=False
|
||||
)
|
||||
obs = runtime.run_action(action)
|
||||
assert obs.exit_code == 0
|
||||
|
||||
|
||||
@@ -61,8 +61,6 @@ def get_config(
|
||||
workspace_mount_path=None,
|
||||
)
|
||||
config.set_llm_config(metadata.llm_config)
|
||||
agent_config = config.get_agent_config(metadata.agent_class)
|
||||
agent_config.use_microagents = False
|
||||
return config
|
||||
|
||||
|
||||
|
||||
@@ -119,8 +119,6 @@ def get_config(
|
||||
workspace_mount_path=None,
|
||||
)
|
||||
config.set_llm_config(metadata.llm_config)
|
||||
agent_config = config.get_agent_config(metadata.agent_class)
|
||||
agent_config.use_microagents = False
|
||||
return config
|
||||
|
||||
|
||||
|
||||
@@ -92,8 +92,6 @@ def get_config(
|
||||
workspace_mount_path=None,
|
||||
)
|
||||
config.set_llm_config(metadata.llm_config)
|
||||
agent_config = config.get_agent_config(metadata.agent_class)
|
||||
agent_config.use_microagents = False
|
||||
return config
|
||||
|
||||
|
||||
@@ -163,7 +161,7 @@ def complete_runtime(
|
||||
eval_script = os.path.join(task_path, 'run.sh')
|
||||
logger.info(f'Running evaluation script: {eval_script}')
|
||||
|
||||
action = CmdRunAction(command=f'cat {eval_script}')
|
||||
action = CmdRunAction(command=f'cat {eval_script}', keep_prompt=False)
|
||||
logger.info(action, extra={'msg_type': 'ACTION'})
|
||||
obs = runtime.run_action(action)
|
||||
if obs.exit_code == 0:
|
||||
|
||||
@@ -121,7 +121,10 @@ def initialize_runtime(
|
||||
runtime.copy_to(dataset_dir, '/workspace/benchmark/datasets', recursive=True)
|
||||
|
||||
# Check the dataset exists
|
||||
action = CmdRunAction(command='cd /workspace/benchmark/datasets && ls')
|
||||
action = CmdRunAction(
|
||||
command='cd /workspace/benchmark/datasets && ls',
|
||||
keep_prompt=False,
|
||||
)
|
||||
obs = runtime.run_action(action)
|
||||
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
|
||||
assert obs.exit_code == 0
|
||||
@@ -151,7 +154,10 @@ def complete_runtime(
|
||||
|
||||
assert obs.exit_code == 0
|
||||
|
||||
action = CmdRunAction(command=f'cat pred_programs/{instance.pred_program_name}')
|
||||
action = CmdRunAction(
|
||||
command=f'cat pred_programs/{instance.pred_program_name}',
|
||||
keep_prompt=False,
|
||||
)
|
||||
logger.info(action, extra={'msg_type': 'ACTION'})
|
||||
obs = runtime.run_action(action)
|
||||
|
||||
|
||||
@@ -204,7 +204,7 @@ Then, in a separate Python environment with `streamlit` library, you can run the
|
||||
```bash
|
||||
# Make sure you are inside the cloned `evaluation` repo
|
||||
conda activate streamlit # if you follow the optional conda env setup above
|
||||
streamlit run app.py --server.port 8501 --server.address 0.0.0.0
|
||||
streamlit app.py --server.port 8501 --server.address 0.0.0.0
|
||||
```
|
||||
|
||||
Then you can access the SWE-Bench trajectory visualizer at `localhost:8501`.
|
||||
|
||||
@@ -98,7 +98,6 @@ def process_instance(
|
||||
metadata: EvalMetadata,
|
||||
reset_logger: bool = True,
|
||||
log_dir: str | None = None,
|
||||
runtime_failure_count: int = 0,
|
||||
) -> EvalOutput:
|
||||
"""
|
||||
Evaluate agent performance on a SWE-bench problem instance.
|
||||
@@ -147,16 +146,6 @@ def process_instance(
|
||||
metadata=metadata,
|
||||
)
|
||||
|
||||
# Increase resource_factor with increasing attempt_id
|
||||
if runtime_failure_count > 0:
|
||||
config.sandbox.remote_runtime_resource_factor = min(
|
||||
config.sandbox.remote_runtime_resource_factor * (2**runtime_failure_count),
|
||||
4, # hardcode maximum resource factor to 4
|
||||
)
|
||||
logger.warning(
|
||||
f'This is the second attempt for instance {instance.instance_id}, setting resource factor to {config.sandbox.remote_runtime_resource_factor}'
|
||||
)
|
||||
|
||||
runtime = create_runtime(config)
|
||||
call_async_from_sync(runtime.connect)
|
||||
# Get patch and save it to /tmp/patch.diff
|
||||
@@ -188,7 +177,7 @@ def process_instance(
|
||||
"(patch --batch --fuzz=5 -p1 -i /tmp/patch.diff && echo 'APPLY_PATCH_PASS' || "
|
||||
"echo 'APPLY_PATCH_FAIL')))"
|
||||
)
|
||||
action = CmdRunAction(command=exec_command)
|
||||
action = CmdRunAction(command=exec_command, keep_prompt=False)
|
||||
action.timeout = 600
|
||||
obs = runtime.run_action(action)
|
||||
assert isinstance(obs, CmdOutputObservation)
|
||||
@@ -211,7 +200,9 @@ def process_instance(
|
||||
|
||||
# Run eval script in background and save output to log file
|
||||
log_file = '/tmp/eval_output.log'
|
||||
action = CmdRunAction(command=f'/tmp/eval.sh > {log_file} 2>&1 & echo $!')
|
||||
action = CmdRunAction(
|
||||
command=f'/tmp/eval.sh > {log_file} 2>&1 & echo $!', keep_prompt=False
|
||||
)
|
||||
action.timeout = 60 # Short timeout just to get the process ID
|
||||
obs = runtime.run_action(action)
|
||||
|
||||
@@ -233,7 +224,7 @@ def process_instance(
|
||||
instance['test_result']['report']['test_timeout'] = True
|
||||
break
|
||||
check_action = CmdRunAction(
|
||||
command=f'ps -p {pid} > /dev/null; echo $?'
|
||||
command=f'ps -p {pid} > /dev/null; echo $?', keep_prompt=False
|
||||
)
|
||||
check_action.timeout = 60
|
||||
check_obs = runtime.run_action(check_action)
|
||||
@@ -251,7 +242,7 @@ def process_instance(
|
||||
time.sleep(30) # Wait for 30 seconds before checking again
|
||||
|
||||
# Read the log file
|
||||
cat_action = CmdRunAction(command=f'cat {log_file}')
|
||||
cat_action = CmdRunAction(command=f'cat {log_file}', keep_prompt=False)
|
||||
cat_action.timeout = 300
|
||||
cat_obs = runtime.run_action(cat_action)
|
||||
|
||||
|
||||
@@ -282,16 +282,6 @@ def initialize_runtime(
|
||||
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
|
||||
assert_and_raise(obs.exit_code == 0, f'Failed to remove git remotes: {str(obs)}')
|
||||
|
||||
action = CmdRunAction(command='which python')
|
||||
action.timeout = 600
|
||||
logger.info(action, extra={'msg_type': 'ACTION'})
|
||||
obs = runtime.run_action(action)
|
||||
logger.info(obs, extra={'msg_type': 'OBSERVATION'})
|
||||
assert_and_raise(
|
||||
obs.exit_code == 0 and 'testbed' in obs.content,
|
||||
f'Expected to find python interpreter from testbed, but got: {str(obs)}',
|
||||
)
|
||||
|
||||
logger.info('-' * 30)
|
||||
logger.info('END Runtime Initialization Fn')
|
||||
logger.info('-' * 30)
|
||||
@@ -347,7 +337,8 @@ def complete_runtime(
|
||||
git_patch = None
|
||||
while n_retries < 5:
|
||||
action = CmdRunAction(
|
||||
command=f'git diff --no-color --cached {instance["base_commit"]}'
|
||||
command=f'git diff --no-color --cached {instance["base_commit"]}',
|
||||
keep_prompt=False,
|
||||
)
|
||||
action.timeout = 600 + 100 * n_retries
|
||||
logger.info(action, extra={'msg_type': 'ACTION'})
|
||||
@@ -394,7 +385,7 @@ def process_instance(
|
||||
if runtime_failure_count > 0:
|
||||
config.sandbox.remote_runtime_resource_factor = min(
|
||||
config.sandbox.remote_runtime_resource_factor * (2**runtime_failure_count),
|
||||
8,
|
||||
2, # hardcode maximum resource factor to 2
|
||||
)
|
||||
logger.warning(
|
||||
f'This is the second attempt for instance {instance.instance_id}, setting resource factor to {config.sandbox.remote_runtime_resource_factor}'
|
||||
@@ -544,5 +535,4 @@ if __name__ == '__main__':
|
||||
args.eval_num_workers,
|
||||
process_instance,
|
||||
timeout_seconds=120 * 60, # 2 hour PER instance should be more than enough
|
||||
max_retries=5,
|
||||
)
|
||||
|
||||
@@ -1,20 +1,13 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import os
|
||||
|
||||
import pandas as pd
|
||||
from termcolor import colored
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Compare two swe_bench output JSONL files and print the resolved diff'
|
||||
)
|
||||
parser.add_argument('input_file_1', type=str)
|
||||
parser.add_argument('input_file_2', type=str)
|
||||
parser.add_argument(
|
||||
'--show-paths',
|
||||
action='store_true',
|
||||
help='Show visualization paths for failed instances',
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
df1 = pd.read_json(args.input_file_1, orient='records', lines=True)
|
||||
@@ -65,60 +58,10 @@ df_diff_y_only = df_diff[~df_diff['resolved_x'] & df_diff['resolved_y']].sort_va
|
||||
print(f'# y resolved but x not={df_diff_y_only.shape[0]}')
|
||||
print(df_diff_y_only[['instance_id', 'report_x', 'report_y']])
|
||||
# get instance_id from df_diff_y_only
|
||||
|
||||
x_only_by_repo = {}
|
||||
for instance_id in df_diff_x_only['instance_id'].tolist():
|
||||
repo = instance_id.split('__')[0]
|
||||
x_only_by_repo.setdefault(repo, []).append(instance_id)
|
||||
y_only_by_repo = {}
|
||||
for instance_id in df_diff_y_only['instance_id'].tolist():
|
||||
repo = instance_id.split('__')[0]
|
||||
y_only_by_repo.setdefault(repo, []).append(instance_id)
|
||||
print('-' * 100)
|
||||
print('Instances that x resolved but y not:')
|
||||
print(df_diff_x_only['instance_id'].tolist())
|
||||
|
||||
print('-' * 100)
|
||||
print(
|
||||
colored('Repository comparison (x resolved vs y resolved):', 'cyan', attrs=['bold'])
|
||||
)
|
||||
all_repos = sorted(set(list(x_only_by_repo.keys()) + list(y_only_by_repo.keys())))
|
||||
|
||||
# Calculate diffs and sort repos by diff magnitude
|
||||
repo_diffs = []
|
||||
for repo in all_repos:
|
||||
x_count = len(x_only_by_repo.get(repo, []))
|
||||
y_count = len(y_only_by_repo.get(repo, []))
|
||||
diff = abs(x_count - y_count)
|
||||
repo_diffs.append((repo, diff))
|
||||
|
||||
# Sort by diff (descending) and then by repo name
|
||||
repo_diffs.sort(key=lambda x: (-x[1], x[0]))
|
||||
threshold = max(
|
||||
3, sum(d[1] for d in repo_diffs) / len(repo_diffs) * 1.5 if repo_diffs else 0
|
||||
)
|
||||
|
||||
x_input_file_folder = os.path.join(os.path.dirname(args.input_file_1), 'output.viz')
|
||||
|
||||
for repo, diff in repo_diffs:
|
||||
x_instances = x_only_by_repo.get(repo, [])
|
||||
y_instances = y_only_by_repo.get(repo, [])
|
||||
|
||||
# Determine if this repo has a significant diff
|
||||
is_significant = diff >= threshold
|
||||
repo_color = 'red' if is_significant else 'yellow'
|
||||
|
||||
print(f"\n{colored(repo, repo_color, attrs=['bold'])}:")
|
||||
print(colored(f'Difference: {diff} instances!', repo_color, attrs=['bold']))
|
||||
print(colored(f'X resolved but Y failed: ({len(x_instances)} instances)', 'green'))
|
||||
if x_instances:
|
||||
print(' ' + str(x_instances))
|
||||
print(colored(f'Y resolved but X failed: ({len(y_instances)} instances)', 'red'))
|
||||
if y_instances:
|
||||
print(' ' + str(y_instances))
|
||||
if args.show_paths:
|
||||
print(
|
||||
colored(' Visualization path for X failed:', 'cyan', attrs=['bold'])
|
||||
)
|
||||
for instance_id in y_instances:
|
||||
instance_file = os.path.join(
|
||||
x_input_file_folder, f'false.{instance_id}.md'
|
||||
)
|
||||
print(f' {instance_file}')
|
||||
print('Instances that y resolved but x not:')
|
||||
print(df_diff_y_only['instance_id'].tolist())
|
||||
|
||||
@@ -20,13 +20,6 @@ output_md_folder = args.oh_output_file.replace('.jsonl', '.viz')
|
||||
print(f'Converting {args.oh_output_file} to markdown files in {output_md_folder}')
|
||||
|
||||
oh_format = pd.read_json(args.oh_output_file, orient='records', lines=True)
|
||||
|
||||
swebench_eval_file = args.oh_output_file.replace('.jsonl', '.swebench_eval.jsonl')
|
||||
if os.path.exists(swebench_eval_file):
|
||||
eval_output_df = pd.read_json(swebench_eval_file, orient='records', lines=True)
|
||||
else:
|
||||
eval_output_df = None
|
||||
|
||||
# model name is the folder name of oh_output_file
|
||||
model_name = os.path.basename(os.path.dirname(args.oh_output_file))
|
||||
|
||||
@@ -57,7 +50,7 @@ def convert_history_to_str(history):
|
||||
return ret
|
||||
|
||||
|
||||
def write_row_to_md_file(row, instance_id_to_test_result):
|
||||
def write_row_to_md_file(row):
|
||||
if 'git_patch' in row:
|
||||
model_patch = row['git_patch']
|
||||
elif 'test_result' in row and 'git_patch' in row['test_result']:
|
||||
@@ -65,21 +58,8 @@ def write_row_to_md_file(row, instance_id_to_test_result):
|
||||
else:
|
||||
raise ValueError(f'Row {row} does not have a git_patch')
|
||||
|
||||
test_output = None
|
||||
if row['instance_id'] in instance_id_to_test_result:
|
||||
report = instance_id_to_test_result[row['instance_id']].get('report', {})
|
||||
resolved = report.get('resolved', False)
|
||||
test_output = instance_id_to_test_result[row['instance_id']].get(
|
||||
'test_output', None
|
||||
)
|
||||
elif 'report' in row and row['report'] is not None:
|
||||
if not isinstance(row['report'], dict):
|
||||
resolved = None
|
||||
print(
|
||||
f'ERROR: Report is not a dict, but a {type(row["report"])}. Row: {row}'
|
||||
)
|
||||
else:
|
||||
resolved = row['report'].get('resolved', False)
|
||||
if 'report' in row:
|
||||
resolved = row['report'].get('resolved', False)
|
||||
else:
|
||||
resolved = None
|
||||
|
||||
@@ -104,18 +84,5 @@ def write_row_to_md_file(row, instance_id_to_test_result):
|
||||
f.write('## Model Patch\n')
|
||||
f.write(f'{process_git_patch(model_patch)}\n')
|
||||
|
||||
f.write('## Test Output\n')
|
||||
f.write(str(test_output))
|
||||
|
||||
|
||||
instance_id_to_test_result = {}
|
||||
if eval_output_df is not None:
|
||||
instance_id_to_test_result = (
|
||||
eval_output_df[['instance_id', 'test_result']]
|
||||
.set_index('instance_id')['test_result']
|
||||
.to_dict()
|
||||
)
|
||||
|
||||
oh_format.progress_apply(
|
||||
write_row_to_md_file, axis=1, instance_id_to_test_result=instance_id_to_test_result
|
||||
)
|
||||
oh_format.progress_apply(write_row_to_md_file, axis=1)
|
||||
|
||||
@@ -111,11 +111,6 @@ elif os.path.exists(openhands_remote_report_jsonl):
|
||||
instance_id_to_status[row['instance_id']] = row['test_result']['report']
|
||||
df['report'] = df.apply(apply_report, axis=1)
|
||||
|
||||
report_is_dict = df['report'].apply(lambda x: isinstance(x, dict))
|
||||
if not report_is_dict.all():
|
||||
print(df[~report_is_dict])
|
||||
raise ValueError(f'Report is not a dict, but a {type(row["report"])}')
|
||||
|
||||
_n_instances = len(df)
|
||||
_n_resolved = len(df[df['report'].apply(lambda x: x.get('resolved', False))])
|
||||
_n_unresolved = _n_instances - _n_resolved
|
||||
|
||||
@@ -56,8 +56,6 @@ def get_config(
|
||||
workspace_mount_path=None,
|
||||
)
|
||||
config.set_llm_config(metadata.llm_config)
|
||||
agent_config = config.get_agent_config(metadata.agent_class)
|
||||
agent_config.use_microagents = False
|
||||
return config
|
||||
|
||||
|
||||
|
||||
@@ -77,8 +77,6 @@ def get_config(
|
||||
workspace_mount_path=None,
|
||||
)
|
||||
config.set_llm_config(metadata.llm_config)
|
||||
agent_config = config.get_agent_config(metadata.agent_class)
|
||||
agent_config.use_microagents = False
|
||||
return config
|
||||
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ class Test(BaseIntegrationTest):
|
||||
@classmethod
|
||||
def verify_result(cls, runtime: Runtime, histories: list[Event]) -> TestResult:
|
||||
# check if the file /workspace/bad.txt has been fixed
|
||||
action = CmdRunAction(command='cat /workspace/bad.txt')
|
||||
action = CmdRunAction(command='cat /workspace/bad.txt', keep_prompt=False)
|
||||
obs = runtime.run_action(action)
|
||||
if obs.exit_code != 0:
|
||||
return TestResult(
|
||||
|
||||
@@ -10,14 +10,14 @@ class Test(BaseIntegrationTest):
|
||||
|
||||
@classmethod
|
||||
def initialize_runtime(cls, runtime: Runtime) -> None:
|
||||
action = CmdRunAction(command='mkdir -p /workspace')
|
||||
action = CmdRunAction(command='mkdir -p /workspace', keep_prompt=False)
|
||||
obs = runtime.run_action(action)
|
||||
assert_and_raise(obs.exit_code == 0, f'Failed to run command: {obs.content}')
|
||||
|
||||
@classmethod
|
||||
def verify_result(cls, runtime: Runtime, histories: list[Event]) -> TestResult:
|
||||
# check if the file /workspace/hello.sh exists
|
||||
action = CmdRunAction(command='cat /workspace/hello.sh')
|
||||
action = CmdRunAction(command='cat /workspace/hello.sh', keep_prompt=False)
|
||||
obs = runtime.run_action(action)
|
||||
if obs.exit_code != 0:
|
||||
return TestResult(
|
||||
@@ -26,7 +26,7 @@ class Test(BaseIntegrationTest):
|
||||
)
|
||||
|
||||
# execute the script
|
||||
action = CmdRunAction(command='bash /workspace/hello.sh')
|
||||
action = CmdRunAction(command='bash /workspace/hello.sh', keep_prompt=False)
|
||||
obs = runtime.run_action(action)
|
||||
if obs.exit_code != 0:
|
||||
return TestResult(
|
||||
|
||||
@@ -10,14 +10,14 @@ class Test(BaseIntegrationTest):
|
||||
|
||||
@classmethod
|
||||
def initialize_runtime(cls, runtime: Runtime) -> None:
|
||||
action = CmdRunAction(command='mkdir -p /workspace')
|
||||
action = CmdRunAction(command='mkdir -p /workspace', keep_prompt=False)
|
||||
obs = runtime.run_action(action)
|
||||
assert_and_raise(obs.exit_code == 0, f'Failed to run command: {obs.content}')
|
||||
|
||||
@classmethod
|
||||
def verify_result(cls, runtime: Runtime, histories: list[Event]) -> TestResult:
|
||||
# check if the file /workspace/hello.sh exists
|
||||
action = CmdRunAction(command='cat /workspace/test.txt')
|
||||
action = CmdRunAction(command='cat /workspace/test.txt', keep_prompt=False)
|
||||
obs = runtime.run_action(action)
|
||||
if obs.exit_code != 0:
|
||||
return TestResult(
|
||||
@@ -26,7 +26,7 @@ class Test(BaseIntegrationTest):
|
||||
)
|
||||
|
||||
# execute the script
|
||||
action = CmdRunAction(command='cat /workspace/test.txt')
|
||||
action = CmdRunAction(command='cat /workspace/test.txt', keep_prompt=False)
|
||||
obs = runtime.run_action(action)
|
||||
|
||||
if obs.exit_code != 0:
|
||||
|
||||
@@ -10,29 +10,31 @@ class Test(BaseIntegrationTest):
|
||||
|
||||
@classmethod
|
||||
def initialize_runtime(cls, runtime: Runtime) -> None:
|
||||
action = CmdRunAction(command='mkdir -p /workspace')
|
||||
action = CmdRunAction(command='mkdir -p /workspace', keep_prompt=False)
|
||||
obs = runtime.run_action(action)
|
||||
assert_and_raise(obs.exit_code == 0, f'Failed to run command: {obs.content}')
|
||||
|
||||
# git init
|
||||
action = CmdRunAction(command='git init')
|
||||
action = CmdRunAction(command='git init', keep_prompt=False)
|
||||
obs = runtime.run_action(action)
|
||||
assert_and_raise(obs.exit_code == 0, f'Failed to run command: {obs.content}')
|
||||
|
||||
# create README.md
|
||||
action = CmdRunAction(command='echo \'print("hello world")\' > hello.py')
|
||||
action = CmdRunAction(
|
||||
command='echo \'print("hello world")\' > hello.py', keep_prompt=False
|
||||
)
|
||||
obs = runtime.run_action(action)
|
||||
assert_and_raise(obs.exit_code == 0, f'Failed to run command: {obs.content}')
|
||||
|
||||
# git add README.md
|
||||
action = CmdRunAction(command='git add hello.py')
|
||||
action = CmdRunAction(command='git add hello.py', keep_prompt=False)
|
||||
obs = runtime.run_action(action)
|
||||
assert_and_raise(obs.exit_code == 0, f'Failed to run command: {obs.content}')
|
||||
|
||||
@classmethod
|
||||
def verify_result(cls, runtime: Runtime, histories: list[Event]) -> TestResult:
|
||||
# check if the file /workspace/hello.py exists
|
||||
action = CmdRunAction(command='cat /workspace/hello.py')
|
||||
action = CmdRunAction(command='cat /workspace/hello.py', keep_prompt=False)
|
||||
obs = runtime.run_action(action)
|
||||
if obs.exit_code != 0:
|
||||
return TestResult(
|
||||
@@ -41,7 +43,7 @@ class Test(BaseIntegrationTest):
|
||||
)
|
||||
|
||||
# check if the staging area is empty
|
||||
action = CmdRunAction(command='git status')
|
||||
action = CmdRunAction(command='git status', keep_prompt=False)
|
||||
obs = runtime.run_action(action)
|
||||
if obs.exit_code != 0:
|
||||
return TestResult(
|
||||
|
||||
@@ -83,11 +83,11 @@ class Test(BaseIntegrationTest):
|
||||
|
||||
@classmethod
|
||||
def initialize_runtime(cls, runtime: Runtime) -> None:
|
||||
action = CmdRunAction(command='mkdir -p /workspace')
|
||||
action = CmdRunAction(command='mkdir -p /workspace', keep_prompt=False)
|
||||
obs = runtime.run_action(action)
|
||||
assert_and_raise(obs.exit_code == 0, f'Failed to run command: {obs.content}')
|
||||
|
||||
action = CmdRunAction(command='mkdir -p /tmp/server')
|
||||
action = CmdRunAction(command='mkdir -p /tmp/server', keep_prompt=False)
|
||||
obs = runtime.run_action(action)
|
||||
assert_and_raise(obs.exit_code == 0, f'Failed to run command: {obs.content}')
|
||||
|
||||
@@ -101,7 +101,8 @@ class Test(BaseIntegrationTest):
|
||||
|
||||
# create README.md
|
||||
action = CmdRunAction(
|
||||
command='cd /tmp/server && nohup python3 -m http.server 8000 &'
|
||||
command='cd /tmp/server && nohup python3 -m http.server 8000 &',
|
||||
keep_prompt=False,
|
||||
)
|
||||
obs = runtime.run_action(action)
|
||||
|
||||
|
||||
@@ -376,12 +376,7 @@ def _process_instance_wrapper(
|
||||
+ '\n'
|
||||
)
|
||||
if isinstance(
|
||||
e,
|
||||
(
|
||||
AgentRuntimeDisconnectedError,
|
||||
AgentRuntimeUnavailableError,
|
||||
AgentRuntimeNotFoundError,
|
||||
),
|
||||
e, (AgentRuntimeDisconnectedError, AgentRuntimeUnavailableError)
|
||||
):
|
||||
runtime_failure_count += 1
|
||||
msg += f'Runtime disconnected error detected for instance {instance.instance_id}, runtime failure count: {runtime_failure_count}'
|
||||
|
||||
@@ -73,7 +73,6 @@
|
||||
}
|
||||
}
|
||||
],
|
||||
"react/prop-types": "off",
|
||||
"react/no-array-index-key": "off",
|
||||
"react-hooks/exhaustive-deps": "off",
|
||||
"import/no-extraneous-dependencies": "off",
|
||||
|
||||
@@ -218,30 +218,4 @@ describe("ChatInput", () => {
|
||||
// Verify image paste was handled
|
||||
expect(onImagePaste).toHaveBeenCalledWith([file]);
|
||||
});
|
||||
|
||||
it("should not submit when Enter is pressed during IME composition", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<ChatInput onSubmit={onSubmitMock} />);
|
||||
const textarea = screen.getByRole("textbox");
|
||||
|
||||
await user.type(textarea, "こんにちは");
|
||||
|
||||
// Simulate Enter during IME composition
|
||||
fireEvent.keyDown(textarea, {
|
||||
key: "Enter",
|
||||
isComposing: true,
|
||||
nativeEvent: { isComposing: true },
|
||||
});
|
||||
|
||||
expect(onSubmitMock).not.toHaveBeenCalled();
|
||||
|
||||
// Simulate normal Enter after composition is done
|
||||
fireEvent.keyDown(textarea, {
|
||||
key: "Enter",
|
||||
isComposing: false,
|
||||
nativeEvent: { isComposing: false },
|
||||
});
|
||||
|
||||
expect(onSubmitMock).toHaveBeenCalledWith("こんにちは");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,7 +3,6 @@ import { afterEach, describe, expect, it, test, vi } from "vitest";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { formatTimeDelta } from "#/utils/format-time-delta";
|
||||
import { ConversationCard } from "#/components/features/conversation-panel/conversation-card";
|
||||
import { clickOnEditButton } from "./utils";
|
||||
|
||||
describe("ConversationCard", () => {
|
||||
const onClick = vi.fn();
|
||||
@@ -20,9 +19,9 @@ describe("ConversationCard", () => {
|
||||
onDelete={onDelete}
|
||||
onClick={onClick}
|
||||
onChangeTitle={onChangeTitle}
|
||||
title="Conversation 1"
|
||||
selectedRepository={null}
|
||||
lastUpdatedAt="2021-10-01T12:00:00Z"
|
||||
name="Conversation 1"
|
||||
repo={null}
|
||||
lastUpdated="2021-10-01T12:00:00Z"
|
||||
/>,
|
||||
);
|
||||
const expectedDate = `${formatTimeDelta(new Date("2021-10-01T12:00:00Z"))} ago`;
|
||||
@@ -34,20 +33,20 @@ describe("ConversationCard", () => {
|
||||
within(card).getByText(expectedDate);
|
||||
});
|
||||
|
||||
it("should render the selectedRepository if available", () => {
|
||||
it("should render the repo if available", () => {
|
||||
const { rerender } = render(
|
||||
<ConversationCard
|
||||
onDelete={onDelete}
|
||||
onClick={onClick}
|
||||
onChangeTitle={onChangeTitle}
|
||||
title="Conversation 1"
|
||||
selectedRepository={null}
|
||||
lastUpdatedAt="2021-10-01T12:00:00Z"
|
||||
name="Conversation 1"
|
||||
repo={null}
|
||||
lastUpdated="2021-10-01T12:00:00Z"
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.queryByTestId("conversation-card-selected-repository"),
|
||||
screen.queryByTestId("conversation-card-repo"),
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
rerender(
|
||||
@@ -55,13 +54,13 @@ describe("ConversationCard", () => {
|
||||
onDelete={onDelete}
|
||||
onClick={onClick}
|
||||
onChangeTitle={onChangeTitle}
|
||||
title="Conversation 1"
|
||||
selectedRepository="org/selectedRepository"
|
||||
lastUpdatedAt="2021-10-01T12:00:00Z"
|
||||
name="Conversation 1"
|
||||
repo="org/repo"
|
||||
lastUpdated="2021-10-01T12:00:00Z"
|
||||
/>,
|
||||
);
|
||||
|
||||
screen.getByTestId("conversation-card-selected-repository");
|
||||
screen.getByTestId("conversation-card-repo");
|
||||
});
|
||||
|
||||
it("should call onClick when the card is clicked", async () => {
|
||||
@@ -71,9 +70,9 @@ describe("ConversationCard", () => {
|
||||
onDelete={onDelete}
|
||||
onClick={onClick}
|
||||
onChangeTitle={onChangeTitle}
|
||||
title="Conversation 1"
|
||||
selectedRepository={null}
|
||||
lastUpdatedAt="2021-10-01T12:00:00Z"
|
||||
name="Conversation 1"
|
||||
repo={null}
|
||||
lastUpdated="2021-10-01T12:00:00Z"
|
||||
/>,
|
||||
);
|
||||
|
||||
@@ -90,9 +89,9 @@ describe("ConversationCard", () => {
|
||||
onDelete={onDelete}
|
||||
onClick={onClick}
|
||||
onChangeTitle={onChangeTitle}
|
||||
title="Conversation 1"
|
||||
selectedRepository={null}
|
||||
lastUpdatedAt="2021-10-01T12:00:00Z"
|
||||
name="Conversation 1"
|
||||
repo={null}
|
||||
lastUpdated="2021-10-01T12:00:00Z"
|
||||
/>,
|
||||
);
|
||||
|
||||
@@ -115,9 +114,9 @@ describe("ConversationCard", () => {
|
||||
onClick={onClick}
|
||||
onDelete={onDelete}
|
||||
onChangeTitle={onChangeTitle}
|
||||
title="Conversation 1"
|
||||
selectedRepository={null}
|
||||
lastUpdatedAt="2021-10-01T12:00:00Z"
|
||||
name="Conversation 1"
|
||||
repo={null}
|
||||
lastUpdated="2021-10-01T12:00:00Z"
|
||||
/>,
|
||||
);
|
||||
|
||||
@@ -132,23 +131,21 @@ describe("ConversationCard", () => {
|
||||
expect(onDelete).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("clicking the selectedRepository should not trigger the onClick handler", async () => {
|
||||
test("clicking the repo should not trigger the onClick handler", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<ConversationCard
|
||||
onClick={onClick}
|
||||
onDelete={onDelete}
|
||||
onChangeTitle={onChangeTitle}
|
||||
title="Conversation 1"
|
||||
selectedRepository="org/selectedRepository"
|
||||
lastUpdatedAt="2021-10-01T12:00:00Z"
|
||||
name="Conversation 1"
|
||||
repo="org/repo"
|
||||
lastUpdated="2021-10-01T12:00:00Z"
|
||||
/>,
|
||||
);
|
||||
|
||||
const selectedRepository = screen.getByTestId(
|
||||
"conversation-card-selected-repository",
|
||||
);
|
||||
await user.click(selectedRepository);
|
||||
const repo = screen.getByTestId("conversation-card-repo");
|
||||
await user.click(repo);
|
||||
|
||||
expect(onClick).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -159,22 +156,14 @@ describe("ConversationCard", () => {
|
||||
<ConversationCard
|
||||
onClick={onClick}
|
||||
onDelete={onDelete}
|
||||
title="Conversation 1"
|
||||
selectedRepository={null}
|
||||
lastUpdatedAt="2021-10-01T12:00:00Z"
|
||||
name="Conversation 1"
|
||||
repo={null}
|
||||
lastUpdated="2021-10-01T12:00:00Z"
|
||||
onChangeTitle={onChangeTitle}
|
||||
/>,
|
||||
);
|
||||
|
||||
const title = screen.getByTestId("conversation-card-title");
|
||||
expect(title).toBeDisabled();
|
||||
|
||||
await clickOnEditButton(user);
|
||||
|
||||
expect(title).toBeEnabled();
|
||||
expect(screen.queryByTestId("context-menu")).not.toBeInTheDocument();
|
||||
// expect to be focused
|
||||
expect(document.activeElement).toBe(title);
|
||||
|
||||
await user.clear(title);
|
||||
await user.type(title, "New Conversation Name ");
|
||||
@@ -182,7 +171,6 @@ describe("ConversationCard", () => {
|
||||
|
||||
expect(onChangeTitle).toHaveBeenCalledWith("New Conversation Name");
|
||||
expect(title).toHaveValue("New Conversation Name");
|
||||
expect(title).toBeDisabled();
|
||||
});
|
||||
|
||||
it("should reset title and not call onChangeTitle when the title is empty", async () => {
|
||||
@@ -192,14 +180,12 @@ describe("ConversationCard", () => {
|
||||
onClick={onClick}
|
||||
onDelete={onDelete}
|
||||
onChangeTitle={onChangeTitle}
|
||||
title="Conversation 1"
|
||||
selectedRepository={null}
|
||||
lastUpdatedAt="2021-10-01T12:00:00Z"
|
||||
name="Conversation 1"
|
||||
repo={null}
|
||||
lastUpdated="2021-10-01T12:00:00Z"
|
||||
/>,
|
||||
);
|
||||
|
||||
await clickOnEditButton(user);
|
||||
|
||||
const title = screen.getByTestId("conversation-card-title");
|
||||
|
||||
await user.clear(title);
|
||||
@@ -216,9 +202,9 @@ describe("ConversationCard", () => {
|
||||
onClick={onClick}
|
||||
onDelete={onDelete}
|
||||
onChangeTitle={onChangeTitle}
|
||||
title="Conversation 1"
|
||||
selectedRepository={null}
|
||||
lastUpdatedAt="2021-10-01T12:00:00Z"
|
||||
name="Conversation 1"
|
||||
repo={null}
|
||||
lastUpdated="2021-10-01T12:00:00Z"
|
||||
/>,
|
||||
);
|
||||
|
||||
@@ -235,9 +221,9 @@ describe("ConversationCard", () => {
|
||||
onClick={onClick}
|
||||
onDelete={onDelete}
|
||||
onChangeTitle={onChangeTitle}
|
||||
title="Conversation 1"
|
||||
selectedRepository={null}
|
||||
lastUpdatedAt="2021-10-01T12:00:00Z"
|
||||
name="Conversation 1"
|
||||
repo={null}
|
||||
lastUpdated="2021-10-01T12:00:00Z"
|
||||
/>,
|
||||
);
|
||||
|
||||
@@ -253,19 +239,19 @@ describe("ConversationCard", () => {
|
||||
});
|
||||
|
||||
describe("state indicator", () => {
|
||||
it("should render the 'STOPPED' indicator by default", () => {
|
||||
it("should render the 'cold' indicator by default", () => {
|
||||
render(
|
||||
<ConversationCard
|
||||
onClick={onClick}
|
||||
onDelete={onDelete}
|
||||
onChangeTitle={onChangeTitle}
|
||||
title="Conversation 1"
|
||||
selectedRepository={null}
|
||||
lastUpdatedAt="2021-10-01T12:00:00Z"
|
||||
name="Conversation 1"
|
||||
repo={null}
|
||||
lastUpdated="2021-10-01T12:00:00Z"
|
||||
/>,
|
||||
);
|
||||
|
||||
screen.getByTestId("STOPPED-indicator");
|
||||
screen.getByTestId("cold-indicator");
|
||||
});
|
||||
|
||||
it("should render the other indicators when provided", () => {
|
||||
@@ -274,15 +260,15 @@ describe("ConversationCard", () => {
|
||||
onClick={onClick}
|
||||
onDelete={onDelete}
|
||||
onChangeTitle={onChangeTitle}
|
||||
title="Conversation 1"
|
||||
selectedRepository={null}
|
||||
lastUpdatedAt="2021-10-01T12:00:00Z"
|
||||
status="RUNNING"
|
||||
name="Conversation 1"
|
||||
repo={null}
|
||||
lastUpdated="2021-10-01T12:00:00Z"
|
||||
state="warm"
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.queryByTestId("STOPPED-indicator")).not.toBeInTheDocument();
|
||||
screen.getByTestId("RUNNING-indicator");
|
||||
expect(screen.queryByTestId("cold-indicator")).not.toBeInTheDocument();
|
||||
screen.getByTestId("warm-indicator");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,7 +9,6 @@ import userEvent from "@testing-library/user-event";
|
||||
import { ConversationPanel } from "#/components/features/conversation-panel/conversation-panel";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import { AuthProvider } from "#/context/auth-context";
|
||||
import { clickOnEditButton } from "./utils";
|
||||
|
||||
describe("ConversationPanel", () => {
|
||||
const onCloseMock = vi.fn();
|
||||
@@ -53,8 +52,6 @@ describe("ConversationPanel", () => {
|
||||
renderConversationPanel();
|
||||
const cards = await screen.findAllByTestId("conversation-card");
|
||||
|
||||
// NOTE that we filter out conversations that don't have a created_at property
|
||||
// (mock data has 4 conversations, but only 3 have a created_at property)
|
||||
expect(cards).toHaveLength(3);
|
||||
});
|
||||
|
||||
@@ -172,15 +169,13 @@ describe("ConversationPanel", () => {
|
||||
const cards = await screen.findAllByTestId("conversation-card");
|
||||
const title = within(cards[0]).getByTestId("conversation-card-title");
|
||||
|
||||
await clickOnEditButton(user);
|
||||
|
||||
await user.clear(title);
|
||||
await user.type(title, "Conversation 1 Renamed");
|
||||
await user.tab();
|
||||
|
||||
// Ensure the conversation is renamed
|
||||
expect(updateUserConversationSpy).toHaveBeenCalledWith("3", {
|
||||
title: "Conversation 1 Renamed",
|
||||
name: "Conversation 1 Renamed",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -201,8 +196,6 @@ describe("ConversationPanel", () => {
|
||||
// Ensure the conversation is not renamed
|
||||
expect(updateUserConversationSpy).not.toHaveBeenCalled();
|
||||
|
||||
await clickOnEditButton(user);
|
||||
|
||||
await user.type(title, "Conversation 1");
|
||||
await user.click(title);
|
||||
await user.tab();
|
||||
@@ -224,4 +217,51 @@ describe("ConversationPanel", () => {
|
||||
|
||||
expect(onCloseMock).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
describe("New Conversation Button", () => {
|
||||
it("should display a confirmation modal when clicking", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderConversationPanel();
|
||||
|
||||
expect(
|
||||
screen.queryByTestId("confirm-new-conversation-modal"),
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
const newProjectButton = screen.getByTestId("new-conversation-button");
|
||||
await user.click(newProjectButton);
|
||||
|
||||
const modal = screen.getByTestId("confirm-new-conversation-modal");
|
||||
expect(modal).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should call endSession and close panel after confirming", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderConversationPanel();
|
||||
|
||||
const newProjectButton = screen.getByTestId("new-conversation-button");
|
||||
await user.click(newProjectButton);
|
||||
|
||||
const confirmButton = screen.getByText("Confirm");
|
||||
await user.click(confirmButton);
|
||||
|
||||
expect(endSessionMock).toHaveBeenCalledOnce();
|
||||
expect(onCloseMock).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("should close the modal when cancelling", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderConversationPanel();
|
||||
|
||||
const newProjectButton = screen.getByTestId("new-conversation-button");
|
||||
await user.click(newProjectButton);
|
||||
|
||||
const cancelButton = screen.getByText("Cancel");
|
||||
await user.click(cancelButton);
|
||||
|
||||
expect(endSessionMock).not.toHaveBeenCalled();
|
||||
expect(
|
||||
screen.queryByTestId("confirm-new-conversation-modal"),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import { screen, within } from "@testing-library/react";
|
||||
import { UserEvent } from "@testing-library/user-event";
|
||||
|
||||
export const clickOnEditButton = async (user: UserEvent) => {
|
||||
const ellipsisButton = screen.getByTestId("ellipsis-button");
|
||||
await user.click(ellipsisButton);
|
||||
|
||||
const menu = screen.getByTestId("context-menu");
|
||||
const editButton = within(menu).getByTestId("edit-button");
|
||||
|
||||
await user.click(editButton);
|
||||
};
|
||||
@@ -1,79 +0,0 @@
|
||||
import { screen } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { renderWithProviders } from "test-utils";
|
||||
import { GitHubRepositorySelector } from "#/components/features/github/github-repo-selector";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import * as GitHubAPI from "#/api/github";
|
||||
|
||||
describe("GitHubRepositorySelector", () => {
|
||||
const onInputChangeMock = vi.fn();
|
||||
const onSelectMock = vi.fn();
|
||||
|
||||
it("should render the search input", () => {
|
||||
renderWithProviders(
|
||||
<GitHubRepositorySelector
|
||||
onInputChange={onInputChangeMock}
|
||||
onSelect={onSelectMock}
|
||||
publicRepositories={[]}
|
||||
userRepositories={[]}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.getByPlaceholderText("Select a GitHub project"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show the GitHub login button in OSS mode", () => {
|
||||
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
|
||||
getConfigSpy.mockResolvedValue({
|
||||
APP_MODE: "oss",
|
||||
APP_SLUG: "openhands",
|
||||
GITHUB_CLIENT_ID: "test-client-id",
|
||||
POSTHOG_CLIENT_KEY: "test-posthog-key",
|
||||
});
|
||||
|
||||
renderWithProviders(
|
||||
<GitHubRepositorySelector
|
||||
onInputChange={onInputChangeMock}
|
||||
onSelect={onSelectMock}
|
||||
publicRepositories={[]}
|
||||
userRepositories={[]}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("github-repo-selector")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show the search results", () => {
|
||||
const mockSearchedRepos = [
|
||||
{
|
||||
id: 1,
|
||||
full_name: "test/repo1",
|
||||
stargazers_count: 100,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
full_name: "test/repo2",
|
||||
stargazers_count: 200,
|
||||
},
|
||||
];
|
||||
|
||||
const searchPublicRepositoriesSpy = vi.spyOn(
|
||||
GitHubAPI,
|
||||
"searchPublicRepositories",
|
||||
);
|
||||
searchPublicRepositoriesSpy.mockResolvedValue(mockSearchedRepos);
|
||||
|
||||
renderWithProviders(
|
||||
<GitHubRepositorySelector
|
||||
onInputChange={onInputChangeMock}
|
||||
onSelect={onSelectMock}
|
||||
publicRepositories={[]}
|
||||
userRepositories={[]}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByTestId("github-repo-selector")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -4,7 +4,7 @@ import { describe, expect, it } from "vitest";
|
||||
import { renderWithProviders } from "test-utils";
|
||||
import { createRoutesStub } from "react-router";
|
||||
import { Sidebar } from "#/components/features/sidebar/sidebar";
|
||||
import { MULTI_CONVERSATION_UI } from "#/utils/feature-flags";
|
||||
import { MULTI_CONVO_UI_IS_ENABLED } from "#/utils/constants";
|
||||
|
||||
const renderSidebar = () => {
|
||||
const RouterStub = createRoutesStub([
|
||||
@@ -18,7 +18,7 @@ const renderSidebar = () => {
|
||||
};
|
||||
|
||||
describe("Sidebar", () => {
|
||||
it.skipIf(!MULTI_CONVERSATION_UI)(
|
||||
it.skipIf(!MULTI_CONVO_UI_IS_ENABLED)(
|
||||
"should have the conversation panel open by default",
|
||||
() => {
|
||||
renderSidebar();
|
||||
@@ -26,7 +26,7 @@ describe("Sidebar", () => {
|
||||
},
|
||||
);
|
||||
|
||||
it.skipIf(!MULTI_CONVERSATION_UI)(
|
||||
it.skipIf(!MULTI_CONVO_UI_IS_ENABLED)(
|
||||
"should toggle the conversation panel",
|
||||
async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import { screen } from "@testing-library/react";
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { renderWithProviders } from "test-utils";
|
||||
import { RuntimeSizeSelector } from "#/components/shared/modals/settings/runtime-size-selector";
|
||||
|
||||
const renderRuntimeSizeSelector = () =>
|
||||
renderWithProviders(<RuntimeSizeSelector isDisabled={false} />);
|
||||
|
||||
describe("RuntimeSizeSelector", () => {
|
||||
it("should show both runtime size options", () => {
|
||||
renderRuntimeSizeSelector();
|
||||
// The options are in the hidden select element
|
||||
const select = screen.getByRole("combobox", { hidden: true });
|
||||
expect(select).toHaveValue("1");
|
||||
expect(select).toHaveDisplayValue("1x (2 core, 8G)");
|
||||
expect(select.children).toHaveLength(3); // Empty option + 2 size options
|
||||
});
|
||||
|
||||
it("should show the full description text for disabled options", async () => {
|
||||
renderRuntimeSizeSelector();
|
||||
|
||||
// Click the button to open the dropdown
|
||||
const button = screen.getByRole("button", {
|
||||
name: "1x (2 core, 8G) SETTINGS_FORM$RUNTIME_SIZE_LABEL",
|
||||
});
|
||||
button.click();
|
||||
|
||||
// Wait for the dropdown to open and find the description text
|
||||
const description = await screen.findByText(
|
||||
"Runtime sizes over 1 are disabled by default, please contact contact@all-hands.dev to get access to larger runtimes.",
|
||||
);
|
||||
expect(description).toBeInTheDocument();
|
||||
expect(description).toHaveClass("whitespace-normal", "break-words");
|
||||
});
|
||||
});
|
||||
@@ -1,45 +0,0 @@
|
||||
import { screen, fireEvent } from "@testing-library/react";
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { renderWithProviders } from "test-utils";
|
||||
import { createRoutesStub } from "react-router";
|
||||
import { DEFAULT_SETTINGS } from "#/services/settings";
|
||||
import { SettingsForm } from "#/components/shared/modals/settings/settings-form";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
|
||||
describe("SettingsForm", () => {
|
||||
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
|
||||
getConfigSpy.mockResolvedValue({
|
||||
APP_MODE: "saas",
|
||||
GITHUB_CLIENT_ID: "123",
|
||||
POSTHOG_CLIENT_KEY: "123",
|
||||
});
|
||||
|
||||
const RouterStub = createRoutesStub([
|
||||
{
|
||||
Component: () => (
|
||||
<SettingsForm
|
||||
settings={DEFAULT_SETTINGS}
|
||||
models={[]}
|
||||
agents={[]}
|
||||
securityAnalyzers={[]}
|
||||
onClose={() => {}}
|
||||
/>
|
||||
),
|
||||
path: "/",
|
||||
},
|
||||
]);
|
||||
|
||||
it("should not show runtime size selector by default", () => {
|
||||
renderWithProviders(<RouterStub />);
|
||||
expect(screen.queryByText("Runtime Size")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should show runtime size selector when advanced options are enabled", async () => {
|
||||
renderWithProviders(<RouterStub />);
|
||||
const advancedSwitch = screen.getByRole("switch", {
|
||||
name: "SETTINGS_FORM$ADVANCED_OPTIONS_LABEL",
|
||||
});
|
||||
fireEvent.click(advancedSwitch);
|
||||
await screen.findByText("SETTINGS_FORM$RUNTIME_SIZE_LABEL");
|
||||
});
|
||||
});
|
||||
@@ -5,7 +5,7 @@ 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 { MULTI_CONVERSATION_UI } from "#/utils/feature-flags";
|
||||
import { MULTI_CONVO_UI_IS_ENABLED } from "#/utils/constants";
|
||||
|
||||
describe("App", () => {
|
||||
const RouteStub = createRoutesStub([
|
||||
@@ -35,7 +35,7 @@ describe("App", () => {
|
||||
await screen.findByTestId("app-route");
|
||||
});
|
||||
|
||||
it.skipIf(!MULTI_CONVERSATION_UI)(
|
||||
it.skipIf(!MULTI_CONVO_UI_IS_ENABLED)(
|
||||
"should call endSession if the user does not have permission to view conversation",
|
||||
async () => {
|
||||
const errorToastSpy = vi.spyOn(toast, "error");
|
||||
@@ -59,11 +59,10 @@ describe("App", () => {
|
||||
|
||||
getConversationSpy.mockResolvedValue({
|
||||
conversation_id: "9999",
|
||||
last_updated_at: "",
|
||||
created_at: "",
|
||||
title: "",
|
||||
selected_repository: "",
|
||||
status: "STOPPED",
|
||||
lastUpdated: "",
|
||||
name: "",
|
||||
repo: "",
|
||||
state: "cold",
|
||||
});
|
||||
const { rerender } = renderWithProviders(
|
||||
<RouteStub initialEntries={["/conversation/9999"]} />,
|
||||
|
||||
59
frontend/package-lock.json
generated
59
frontend/package-lock.json
generated
@@ -14,7 +14,7 @@
|
||||
"@react-router/serve": "^7.1.1",
|
||||
"@react-types/shared": "^3.25.0",
|
||||
"@reduxjs/toolkit": "^2.5.0",
|
||||
"@tanstack/react-query": "^5.62.12",
|
||||
"@tanstack/react-query": "^5.62.11",
|
||||
"@vitejs/plugin-react": "^4.3.2",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/xterm": "^5.4.0",
|
||||
@@ -27,11 +27,11 @@
|
||||
"isbot": "^5.1.19",
|
||||
"jose": "^5.9.4",
|
||||
"monaco-editor": "^0.52.2",
|
||||
"posthog-js": "^1.203.3",
|
||||
"posthog-js": "^1.203.2",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-highlight": "^0.15.0",
|
||||
"react-hot-toast": "^2.5.1",
|
||||
"react-hot-toast": "^2.4.1",
|
||||
"react-i18next": "^15.4.0",
|
||||
"react-icons": "^5.4.0",
|
||||
"react-markdown": "^9.0.1",
|
||||
@@ -52,11 +52,11 @@
|
||||
"@playwright/test": "^1.49.1",
|
||||
"@react-router/dev": "^7.1.1",
|
||||
"@tailwindcss/typography": "^0.5.15",
|
||||
"@tanstack/eslint-plugin-query": "^5.62.16",
|
||||
"@tanstack/eslint-plugin-query": "^5.62.9",
|
||||
"@testing-library/jest-dom": "^6.6.1",
|
||||
"@testing-library/react": "^16.1.0",
|
||||
"@testing-library/user-event": "^14.5.2",
|
||||
"@types/node": "^22.10.5",
|
||||
"@types/node": "^22.10.2",
|
||||
"@types/react": "^19.0.2",
|
||||
"@types/react-dom": "^19.0.2",
|
||||
"@types/react-highlight": "^0.12.8",
|
||||
@@ -83,7 +83,7 @@
|
||||
"postcss": "^8.4.47",
|
||||
"prettier": "^3.4.2",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "^5.7.2",
|
||||
"typescript": "^5.6.3",
|
||||
"vite-plugin-svgr": "^4.2.0",
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"vitest": "^1.6.0"
|
||||
@@ -5344,10 +5344,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/eslint-plugin-query": {
|
||||
"version": "5.62.16",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/eslint-plugin-query/-/eslint-plugin-query-5.62.16.tgz",
|
||||
"integrity": "sha512-VhnHSQ/hc62olLzGhlLJ4BJGWynwjs3cDMsByasKJ3zjW1YZ+6raxOv0gHHISm+VEnAY42pkMowmSWrXfL4NTw==",
|
||||
"version": "5.62.9",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/eslint-plugin-query/-/eslint-plugin-query-5.62.9.tgz",
|
||||
"integrity": "sha512-F3onhTcpBj7zQDo0NVtZwZQKRFx8BwpSabMJybl9no3+dFHUurvNMrH5M/6KNpkdDCf3zyHWadruZL6636B8Fw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/utils": "^8.18.1"
|
||||
},
|
||||
@@ -5360,20 +5361,22 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/query-core": {
|
||||
"version": "5.62.12",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.62.12.tgz",
|
||||
"integrity": "sha512-6igFeBgymHkCxVgaEk+yiLwkMf9haui/EQLmI3o9CatOyDThEoFKe8toLWvWliZC/Jf+h7NwHi/zjfyLArr1ow==",
|
||||
"version": "5.62.9",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.62.9.tgz",
|
||||
"integrity": "sha512-lwePd8hNYhyQ4nM/iRQ+Wz2cDtspGeZZHFZmCzHJ7mfKXt+9S301fULiY2IR2byJYY6Z03T427E5PoVfMexHjw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/react-query": {
|
||||
"version": "5.62.12",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.62.12.tgz",
|
||||
"integrity": "sha512-yt8p7l5MlHA3QCt6xF1Cu9dw1Anf93yTK+DMDJQ64h/mshAymVAtcwj8TpsyyBrZNWAAZvza/m76bnTSR79ZtQ==",
|
||||
"version": "5.62.11",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.62.11.tgz",
|
||||
"integrity": "sha512-Xb1nw0cYMdtFmwkvH9+y5yYFhXvLRCnXoqlzSw7UkqtCVFq3cG8q+rHZ2Yz1XrC+/ysUaTqbLKJqk95mCgC1oQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/query-core": "5.62.12"
|
||||
"@tanstack/query-core": "5.62.9"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
@@ -5624,10 +5627,11 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.10.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.5.tgz",
|
||||
"integrity": "sha512-F8Q+SeGimwOo86fiovQh8qiXfFEh2/ocYv7tU5pJ3EXMSSxk1Joj5wefpFK2fHTf/N6HKGSxIDBT9f3gCxXPkQ==",
|
||||
"version": "22.10.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz",
|
||||
"integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.20.0"
|
||||
}
|
||||
@@ -13806,9 +13810,10 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/posthog-js": {
|
||||
"version": "1.203.3",
|
||||
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.203.3.tgz",
|
||||
"integrity": "sha512-DTK6LfL87xC7PPleKDParEIfkXl7hXtuDeSOPfhcyCXLuVspq0z7YyRB5dQE9Pbalf3yoGqUKvomYFp/BGVfQg==",
|
||||
"version": "1.203.2",
|
||||
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.203.2.tgz",
|
||||
"integrity": "sha512-3aLpEhM4i9sQQtobRmDttJ3rTW1+gwQ9HL7QiOeDueE2T7CguYibYS7weY1UhXMerx5lh1A7+szlOJTTibifLQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"core-js": "^3.38.1",
|
||||
"fflate": "^0.4.8",
|
||||
@@ -14139,12 +14144,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-hot-toast": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.5.1.tgz",
|
||||
"integrity": "sha512-54Gq1ZD1JbmAb4psp9bvFHjS7lje+8ubboUmvKZkCsQBLH6AOpZ9JemfRvIdHcfb9AZXRaFLrb3qUobGYDJhFQ==",
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.4.1.tgz",
|
||||
"integrity": "sha512-j8z+cQbWIM5LY37pR6uZR6D4LfseplqnuAO4co4u8917hBUvXlEqyP1ZzqVLcqoyUesZZv/ImreoCeHVDpE5pQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"csstype": "^3.1.3",
|
||||
"goober": "^2.1.16"
|
||||
"goober": "^2.1.10"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"@react-router/serve": "^7.1.1",
|
||||
"@react-types/shared": "^3.25.0",
|
||||
"@reduxjs/toolkit": "^2.5.0",
|
||||
"@tanstack/react-query": "^5.62.12",
|
||||
"@tanstack/react-query": "^5.62.11",
|
||||
"@vitejs/plugin-react": "^4.3.2",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/xterm": "^5.4.0",
|
||||
@@ -26,11 +26,11 @@
|
||||
"isbot": "^5.1.19",
|
||||
"jose": "^5.9.4",
|
||||
"monaco-editor": "^0.52.2",
|
||||
"posthog-js": "^1.203.3",
|
||||
"posthog-js": "^1.203.2",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-highlight": "^0.15.0",
|
||||
"react-hot-toast": "^2.5.1",
|
||||
"react-hot-toast": "^2.4.1",
|
||||
"react-i18next": "^15.4.0",
|
||||
"react-icons": "^5.4.0",
|
||||
"react-markdown": "^9.0.1",
|
||||
@@ -79,11 +79,11 @@
|
||||
"@playwright/test": "^1.49.1",
|
||||
"@react-router/dev": "^7.1.1",
|
||||
"@tailwindcss/typography": "^0.5.15",
|
||||
"@tanstack/eslint-plugin-query": "^5.62.16",
|
||||
"@tanstack/eslint-plugin-query": "^5.62.9",
|
||||
"@testing-library/jest-dom": "^6.6.1",
|
||||
"@testing-library/react": "^16.1.0",
|
||||
"@testing-library/user-event": "^14.5.2",
|
||||
"@types/node": "^22.10.5",
|
||||
"@types/node": "^22.10.2",
|
||||
"@types/react": "^19.0.2",
|
||||
"@types/react-dom": "^19.0.2",
|
||||
"@types/react-highlight": "^0.12.8",
|
||||
@@ -110,7 +110,7 @@
|
||||
"postcss": "^8.4.47",
|
||||
"prettier": "^3.4.2",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "^5.7.2",
|
||||
"typescript": "^5.6.3",
|
||||
"vite-plugin-svgr": "^4.2.0",
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"vitest": "^1.6.0"
|
||||
|
||||
@@ -104,26 +104,6 @@ export const retrieveGitHubUser = async () => {
|
||||
return user;
|
||||
};
|
||||
|
||||
export const searchPublicRepositories = async (
|
||||
query: string,
|
||||
per_page = 5,
|
||||
sort: "" | "updated" | "stars" | "forks" = "stars",
|
||||
order: "desc" | "asc" = "desc",
|
||||
): Promise<GitHubRepository[]> => {
|
||||
const response = await github.get<{ items: GitHubRepository[] }>(
|
||||
"/search/repositories",
|
||||
{
|
||||
params: {
|
||||
q: query,
|
||||
per_page,
|
||||
sort,
|
||||
order,
|
||||
},
|
||||
},
|
||||
);
|
||||
return response.data.items;
|
||||
};
|
||||
|
||||
export const retrieveLatestGitHubCommit = async (
|
||||
repository: string,
|
||||
): Promise<GitHubCommit | null> => {
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
GetVSCodeUrlResponse,
|
||||
AuthenticateResponse,
|
||||
Conversation,
|
||||
ResultSet,
|
||||
} from "./open-hands.types";
|
||||
import { openHands } from "./open-hands-axios";
|
||||
import { ApiSettings } from "#/services/settings";
|
||||
@@ -223,10 +222,8 @@ class OpenHands {
|
||||
}
|
||||
|
||||
static async getUserConversations(): Promise<Conversation[]> {
|
||||
const { data } = await openHands.get<ResultSet<Conversation>>(
|
||||
"/api/conversations?limit=9",
|
||||
);
|
||||
return data.results;
|
||||
const { data } = await openHands.get<Conversation[]>("/api/conversations");
|
||||
return data;
|
||||
}
|
||||
|
||||
static async deleteUserConversation(conversationId: string): Promise<void> {
|
||||
@@ -235,9 +232,9 @@ class OpenHands {
|
||||
|
||||
static async updateUserConversation(
|
||||
conversationId: string,
|
||||
conversation: Partial<Omit<Conversation, "conversation_id">>,
|
||||
conversation: Partial<Omit<Conversation, "id">>,
|
||||
): Promise<void> {
|
||||
await openHands.patch(`/api/conversations/${conversationId}`, conversation);
|
||||
await openHands.put(`/api/conversations/${conversationId}`, conversation);
|
||||
}
|
||||
|
||||
static async createConversation(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ProjectStatus } from "#/components/features/conversation-panel/conversation-state-indicator";
|
||||
import { ProjectState } from "#/components/features/conversation-panel/conversation-state-indicator";
|
||||
|
||||
export interface ErrorResponse {
|
||||
error: string;
|
||||
@@ -62,14 +62,8 @@ export interface AuthenticateResponse {
|
||||
|
||||
export interface Conversation {
|
||||
conversation_id: string;
|
||||
title: string;
|
||||
selected_repository: string | null;
|
||||
last_updated_at: string;
|
||||
created_at: string;
|
||||
status: ProjectStatus;
|
||||
}
|
||||
|
||||
export interface ResultSet<T> {
|
||||
results: T[];
|
||||
next_page_id: string | null;
|
||||
name: string;
|
||||
repo: string | null;
|
||||
lastUpdated: string;
|
||||
state: ProjectState;
|
||||
}
|
||||
|
||||
@@ -61,8 +61,4 @@ export const AGENT_STATUS_MAP: {
|
||||
message: I18nKey.CHAT_INTERFACE$AGENT_ACTION_USER_REJECTED_MESSAGE,
|
||||
indicator: IndicatorColor.RED,
|
||||
},
|
||||
[AgentState.RATE_LIMITED]: {
|
||||
message: I18nKey.CHAT_INTERFACE$AGENT_RATE_LIMITED_MESSAGE,
|
||||
indicator: IndicatorColor.YELLOW,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -94,12 +94,7 @@ export function ChatInput({
|
||||
};
|
||||
|
||||
const handleKeyPress = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (
|
||||
event.key === "Enter" &&
|
||||
!event.shiftKey &&
|
||||
!disabled &&
|
||||
!event.nativeEvent.isComposing
|
||||
) {
|
||||
if (event.key === "Enter" && !event.shiftKey && !disabled) {
|
||||
event.preventDefault();
|
||||
handleSubmitMessage();
|
||||
}
|
||||
|
||||
@@ -154,8 +154,7 @@ export function ChatInterface() {
|
||||
onStop={handleStop}
|
||||
isDisabled={
|
||||
curAgentState === AgentState.LOADING ||
|
||||
curAgentState === AgentState.AWAITING_USER_CONFIRMATION ||
|
||||
curAgentState === AgentState.RATE_LIMITED
|
||||
curAgentState === AgentState.AWAITING_USER_CONFIRMATION
|
||||
}
|
||||
mode={curAgentState === AgentState.RUNNING ? "stop" : "submit"}
|
||||
value={messageToSend ?? undefined}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from "react";
|
||||
import { ChatMessage } from "#/components/features/chat/chat-message";
|
||||
import { ConfirmationButtons } from "#/components/shared/buttons/confirmation-buttons";
|
||||
import { ImageCarousel } from "../images/image-carousel";
|
||||
@@ -9,36 +8,32 @@ interface MessagesProps {
|
||||
isAwaitingUserConfirmation: boolean;
|
||||
}
|
||||
|
||||
export const Messages: React.FC<MessagesProps> = React.memo(
|
||||
({ messages, isAwaitingUserConfirmation }) =>
|
||||
messages.map((message, index) => {
|
||||
if (message.type === "error" || message.type === "action") {
|
||||
return (
|
||||
<ExpandableMessage
|
||||
key={index}
|
||||
type={message.type}
|
||||
id={message.translationID}
|
||||
message={message.content}
|
||||
success={message.success}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function Messages({
|
||||
messages,
|
||||
isAwaitingUserConfirmation,
|
||||
}: MessagesProps) {
|
||||
return messages.map((message, index) => {
|
||||
if (message.type === "error" || message.type === "action") {
|
||||
return (
|
||||
<ChatMessage
|
||||
<ExpandableMessage
|
||||
key={index}
|
||||
type={message.sender}
|
||||
type={message.type}
|
||||
id={message.translationID}
|
||||
message={message.content}
|
||||
>
|
||||
{message.imageUrls && message.imageUrls.length > 0 && (
|
||||
<ImageCarousel size="small" images={message.imageUrls} />
|
||||
)}
|
||||
{messages.length - 1 === index &&
|
||||
message.sender === "assistant" &&
|
||||
isAwaitingUserConfirmation && <ConfirmationButtons />}
|
||||
</ChatMessage>
|
||||
success={message.success}
|
||||
/>
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
Messages.displayName = "Messages";
|
||||
return (
|
||||
<ChatMessage key={index} type={message.sender} message={message.content}>
|
||||
{message.imageUrls && message.imageUrls.length > 0 && (
|
||||
<ImageCarousel size="small" images={message.imageUrls} />
|
||||
)}
|
||||
{messages.length - 1 === index &&
|
||||
message.sender === "assistant" &&
|
||||
isAwaitingUserConfirmation && <ConfirmationButtons />}
|
||||
</ChatMessage>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ export function ContextMenu({
|
||||
<ul
|
||||
data-testid={testId}
|
||||
ref={ref}
|
||||
className={cn("bg-[#404040] rounded-md w-[140px]", className)}
|
||||
className={cn("bg-[#404040] rounded-md w-[224px]", className)}
|
||||
>
|
||||
{children}
|
||||
</ul>
|
||||
|
||||
@@ -25,14 +25,10 @@ export function ConfirmDeleteModal({
|
||||
<div className="flex flex-col gap-2 w-full">
|
||||
<ModalButton
|
||||
onClick={onConfirm}
|
||||
className="bg-danger font-bold"
|
||||
className="bg-[#4465DB]"
|
||||
text="Confirm"
|
||||
/>
|
||||
<ModalButton
|
||||
onClick={onCancel}
|
||||
className="bg-neutral-500 font-bold"
|
||||
text="Cancel"
|
||||
/>
|
||||
<ModalButton onClick={onCancel} className="bg-danger" text="Cancel" />
|
||||
</div>
|
||||
</ModalBody>
|
||||
</ModalBackdrop>
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import { useClickOutsideElement } from "#/hooks/use-click-outside-element";
|
||||
import { ContextMenu } from "../context-menu/context-menu";
|
||||
import { ContextMenuListItem } from "../context-menu/context-menu-list-item";
|
||||
|
||||
interface ConversationCardContextMenuProps {
|
||||
onClose: () => void;
|
||||
onDelete: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
onEdit: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
}
|
||||
|
||||
export function ConversationCardContextMenu({
|
||||
onClose,
|
||||
onDelete,
|
||||
onEdit,
|
||||
}: ConversationCardContextMenuProps) {
|
||||
const ref = useClickOutsideElement<HTMLUListElement>(onClose);
|
||||
|
||||
return (
|
||||
<ContextMenu
|
||||
ref={ref}
|
||||
testId="context-menu"
|
||||
className="left-full float-right"
|
||||
>
|
||||
<ContextMenuListItem testId="delete-button" onClick={onDelete}>
|
||||
Delete
|
||||
</ContextMenuListItem>
|
||||
<ContextMenuListItem testId="edit-button" onClick={onEdit}>
|
||||
Edit Title
|
||||
</ContextMenuListItem>
|
||||
</ContextMenu>
|
||||
);
|
||||
}
|
||||
@@ -2,33 +2,33 @@ import React from "react";
|
||||
import { formatTimeDelta } from "#/utils/format-time-delta";
|
||||
import { ConversationRepoLink } from "./conversation-repo-link";
|
||||
import {
|
||||
ProjectStatus,
|
||||
ProjectState,
|
||||
ConversationStateIndicator,
|
||||
} from "./conversation-state-indicator";
|
||||
import { ContextMenu } from "../context-menu/context-menu";
|
||||
import { ContextMenuListItem } from "../context-menu/context-menu-list-item";
|
||||
import { EllipsisButton } from "./ellipsis-button";
|
||||
import { ConversationCardContextMenu } from "./conversation-card-context-menu";
|
||||
|
||||
interface ConversationCardProps {
|
||||
interface ProjectCardProps {
|
||||
onClick: () => void;
|
||||
onDelete: () => void;
|
||||
onChangeTitle: (title: string) => void;
|
||||
title: string;
|
||||
selectedRepository: string | null;
|
||||
lastUpdatedAt: string; // ISO 8601
|
||||
status?: ProjectStatus;
|
||||
name: string;
|
||||
repo: string | null;
|
||||
lastUpdated: string; // ISO 8601
|
||||
state?: ProjectState;
|
||||
}
|
||||
|
||||
export function ConversationCard({
|
||||
onClick,
|
||||
onDelete,
|
||||
onChangeTitle,
|
||||
title,
|
||||
selectedRepository,
|
||||
lastUpdatedAt,
|
||||
status = "STOPPED",
|
||||
}: ConversationCardProps) {
|
||||
name,
|
||||
repo,
|
||||
lastUpdated,
|
||||
state = "cold",
|
||||
}: ProjectCardProps) {
|
||||
const [contextMenuVisible, setContextMenuVisible] = React.useState(false);
|
||||
const [titleMode, setTitleMode] = React.useState<"view" | "edit">("view");
|
||||
const inputRef = React.useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleBlur = () => {
|
||||
@@ -38,15 +38,7 @@ export function ConversationCard({
|
||||
inputRef.current!.value = trimmed;
|
||||
} else {
|
||||
// reset the value if it's empty
|
||||
inputRef.current!.value = title;
|
||||
}
|
||||
|
||||
setTitleMode("view");
|
||||
};
|
||||
|
||||
const handleKeyUp = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (event.key === "Enter") {
|
||||
event.currentTarget.blur();
|
||||
inputRef.current!.value = name;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -59,62 +51,51 @@ export function ConversationCard({
|
||||
onDelete();
|
||||
};
|
||||
|
||||
const handleEdit = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
event.stopPropagation();
|
||||
setTitleMode("edit");
|
||||
setContextMenuVisible(false);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (titleMode === "edit") {
|
||||
inputRef.current?.focus();
|
||||
}
|
||||
}, [titleMode]);
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid="conversation-card"
|
||||
onClick={onClick}
|
||||
className="h-[100px] w-full px-[18px] py-4 border-b border-neutral-600 cursor-pointer"
|
||||
className="h-[100px] w-full px-[18px] py-4 border-b border-neutral-600"
|
||||
>
|
||||
<div className="flex items-center justify-between space-x-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<input
|
||||
data-testid="conversation-card-title"
|
||||
ref={inputRef}
|
||||
disabled={titleMode === "view"}
|
||||
data-testid="conversation-card-title"
|
||||
onClick={handleInputClick}
|
||||
onBlur={handleBlur}
|
||||
onKeyUp={handleKeyUp}
|
||||
type="text"
|
||||
defaultValue={title}
|
||||
className="text-sm leading-6 font-semibold bg-transparent w-full"
|
||||
defaultValue={name}
|
||||
className="text-sm leading-6 font-semibold bg-transparent"
|
||||
/>
|
||||
|
||||
<div className="flex items-center gap-2 relative">
|
||||
<ConversationStateIndicator status={status} />
|
||||
<ConversationStateIndicator state={state} />
|
||||
<EllipsisButton
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
setContextMenuVisible((prev) => !prev);
|
||||
}}
|
||||
/>
|
||||
{contextMenuVisible && (
|
||||
<ContextMenu testId="context-menu" className="absolute left-full">
|
||||
<ContextMenuListItem
|
||||
testId="delete-button"
|
||||
onClick={handleDelete}
|
||||
>
|
||||
Delete
|
||||
</ContextMenuListItem>
|
||||
</ContextMenu>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{contextMenuVisible && (
|
||||
<ConversationCardContextMenu
|
||||
onClose={() => setContextMenuVisible(false)}
|
||||
onDelete={handleDelete}
|
||||
onEdit={handleEdit}
|
||||
/>
|
||||
)}
|
||||
{selectedRepository && (
|
||||
{repo && (
|
||||
<ConversationRepoLink
|
||||
selectedRepository={selectedRepository}
|
||||
repo={repo}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
)}
|
||||
<p className="text-xs text-neutral-400">
|
||||
<time>{formatTimeDelta(new Date(lastUpdatedAt))} ago</time>
|
||||
<time>{formatTimeDelta(new Date(lastUpdated))} ago</time>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
import ReactDOM from "react-dom";
|
||||
|
||||
interface ConversationPanelWrapperProps {
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
export function ConversationPanelWrapper({
|
||||
isOpen,
|
||||
children,
|
||||
}: React.PropsWithChildren<ConversationPanelWrapperProps>) {
|
||||
if (!isOpen) return null;
|
||||
|
||||
const portalTarget = document.getElementById("root-outlet");
|
||||
if (!portalTarget) return null;
|
||||
|
||||
return ReactDOM.createPortal(
|
||||
<div className="absolute h-full w-full left-0 top-0 z-20 bg-black/80 rounded-xl">
|
||||
{children}
|
||||
</div>,
|
||||
portalTarget,
|
||||
);
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
import React from "react";
|
||||
import { useNavigate, useParams } from "react-router";
|
||||
import { useLocation, useNavigate, useParams } from "react-router";
|
||||
import { ConversationCard } from "./conversation-card";
|
||||
import { useUserConversations } from "#/hooks/query/use-user-conversations";
|
||||
import { useDeleteConversation } from "#/hooks/mutation/use-delete-conversation";
|
||||
import { ConfirmDeleteModal } from "./confirm-delete-modal";
|
||||
import { NewConversationButton } from "./new-conversation-button";
|
||||
import { LoadingSpinner } from "#/components/shared/loading-spinner";
|
||||
import { useUpdateConversation } from "#/hooks/mutation/use-update-conversation";
|
||||
import { useEndSession } from "#/hooks/use-end-session";
|
||||
import { ExitConversationModal } from "./exit-conversation-modal";
|
||||
import { useClickOutsideElement } from "#/hooks/use-click-outside-element";
|
||||
|
||||
interface ConversationPanelProps {
|
||||
onClose: () => void;
|
||||
@@ -17,8 +17,9 @@ interface ConversationPanelProps {
|
||||
export function ConversationPanel({ onClose }: ConversationPanelProps) {
|
||||
const { conversationId: cid } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
const endSession = useEndSession();
|
||||
const ref = useClickOutsideElement<HTMLDivElement>(onClose);
|
||||
|
||||
const [confirmDeleteModalVisible, setConfirmDeleteModalVisible] =
|
||||
React.useState(false);
|
||||
@@ -59,7 +60,7 @@ export function ConversationPanel({ onClose }: ConversationPanelProps) {
|
||||
if (oldTitle !== newTitle)
|
||||
updateConversation({
|
||||
id: conversationId,
|
||||
conversation: { title: newTitle },
|
||||
conversation: { name: newTitle },
|
||||
});
|
||||
};
|
||||
|
||||
@@ -70,11 +71,15 @@ export function ConversationPanel({ onClose }: ConversationPanelProps) {
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
data-testid="conversation-panel"
|
||||
className="w-[350px] h-full border border-neutral-700 bg-neutral-800 rounded-xl overflow-y-auto"
|
||||
className="w-[350px] h-full border border-neutral-700 bg-neutral-800 rounded-xl"
|
||||
>
|
||||
<div className="pt-4 px-4 flex items-center justify-between">
|
||||
{location.pathname.startsWith("/conversation") && (
|
||||
<NewConversationButton
|
||||
onClick={() => setConfirmExitConversationModalVisible(true)}
|
||||
/>
|
||||
)}
|
||||
{isFetching && <LoadingSpinner size="small" />}
|
||||
</div>
|
||||
{error && (
|
||||
@@ -93,12 +98,12 @@ export function ConversationPanel({ onClose }: ConversationPanelProps) {
|
||||
onClick={() => handleClickCard(project.conversation_id)}
|
||||
onDelete={() => handleDeleteProject(project.conversation_id)}
|
||||
onChangeTitle={(title) =>
|
||||
handleChangeTitle(project.conversation_id, project.title, title)
|
||||
handleChangeTitle(project.conversation_id, project.name, title)
|
||||
}
|
||||
title={project.title}
|
||||
selectedRepository={project.selected_repository}
|
||||
lastUpdatedAt={project.last_updated_at}
|
||||
status={project.status}
|
||||
name={project.name}
|
||||
repo={project.repo}
|
||||
lastUpdated={project.lastUpdated}
|
||||
state={project.state}
|
||||
/>
|
||||
))}
|
||||
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
interface ConversationRepoLinkProps {
|
||||
selectedRepository: string;
|
||||
repo: string;
|
||||
onClick?: (event: React.MouseEvent<HTMLAnchorElement>) => void;
|
||||
}
|
||||
|
||||
export function ConversationRepoLink({
|
||||
selectedRepository,
|
||||
repo,
|
||||
onClick,
|
||||
}: ConversationRepoLinkProps) {
|
||||
return (
|
||||
<a
|
||||
data-testid="conversation-card-selected-repository"
|
||||
href={`https://github.com/${selectedRepository}`}
|
||||
data-testid="conversation-card-repo"
|
||||
href={`https://github.com/${repo}`}
|
||||
target="_blank noopener noreferrer"
|
||||
onClick={onClick}
|
||||
className="text-xs text-neutral-400 hover:text-neutral-200"
|
||||
>
|
||||
{selectedRepository}
|
||||
{repo}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,25 +1,39 @@
|
||||
import ColdIcon from "./state-indicators/cold.svg?react";
|
||||
import CoolingIcon from "./state-indicators/cooling.svg?react";
|
||||
import FinishedIcon from "./state-indicators/finished.svg?react";
|
||||
import RunningIcon from "./state-indicators/running.svg?react";
|
||||
import WaitingIcon from "./state-indicators/waiting.svg?react";
|
||||
import WarmIcon from "./state-indicators/warm.svg?react";
|
||||
|
||||
type SVGIcon = React.FunctionComponent<React.SVGProps<SVGSVGElement>>;
|
||||
export type ProjectStatus = "RUNNING" | "STOPPED";
|
||||
export type ProjectState =
|
||||
| "cold"
|
||||
| "cooling"
|
||||
| "finished"
|
||||
| "running"
|
||||
| "waiting"
|
||||
| "warm";
|
||||
|
||||
const INDICATORS: Record<ProjectStatus, SVGIcon> = {
|
||||
STOPPED: ColdIcon,
|
||||
RUNNING: RunningIcon,
|
||||
const INDICATORS: Record<ProjectState, SVGIcon> = {
|
||||
cold: ColdIcon,
|
||||
cooling: CoolingIcon,
|
||||
finished: FinishedIcon,
|
||||
running: RunningIcon,
|
||||
waiting: WaitingIcon,
|
||||
warm: WarmIcon,
|
||||
};
|
||||
|
||||
interface ConversationStateIndicatorProps {
|
||||
status: ProjectStatus;
|
||||
state: ProjectState;
|
||||
}
|
||||
|
||||
export function ConversationStateIndicator({
|
||||
status,
|
||||
state,
|
||||
}: ConversationStateIndicatorProps) {
|
||||
const StateIcon = INDICATORS[status];
|
||||
const StateIcon = INDICATORS[state];
|
||||
|
||||
return (
|
||||
<div data-testid={`${status}-indicator`}>
|
||||
<div data-testid={`${state}-indicator`}>
|
||||
<StateIcon />
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,43 +1,40 @@
|
||||
import React from "react";
|
||||
import {
|
||||
Autocomplete,
|
||||
AutocompleteItem,
|
||||
AutocompleteSection,
|
||||
} from "@nextui-org/react";
|
||||
import { Autocomplete, AutocompleteItem } from "@nextui-org/react";
|
||||
import { useDispatch } from "react-redux";
|
||||
import posthog from "posthog-js";
|
||||
import { setSelectedRepository } from "#/state/initial-query-slice";
|
||||
import { useConfig } from "#/hooks/query/use-config";
|
||||
import { sanitizeQuery } from "#/utils/sanitize-query";
|
||||
|
||||
interface GitHubRepositorySelectorProps {
|
||||
onInputChange: (value: string) => void;
|
||||
onSelect: () => void;
|
||||
userRepositories: GitHubRepository[];
|
||||
publicRepositories: GitHubRepository[];
|
||||
repositories: GitHubRepository[];
|
||||
}
|
||||
|
||||
export function GitHubRepositorySelector({
|
||||
onInputChange,
|
||||
onSelect,
|
||||
userRepositories,
|
||||
publicRepositories,
|
||||
repositories,
|
||||
}: GitHubRepositorySelectorProps) {
|
||||
const { data: config } = useConfig();
|
||||
const [selectedKey, setSelectedKey] = React.useState<string | null>(null);
|
||||
|
||||
const allRepositories: GitHubRepository[] = [
|
||||
...publicRepositories.filter(
|
||||
(repo) => !publicRepositories.find((r) => r.id === repo.id),
|
||||
),
|
||||
...userRepositories,
|
||||
];
|
||||
// Add option to install app onto more repos
|
||||
const finalRepositories =
|
||||
config?.APP_MODE === "saas"
|
||||
? [{ id: -1000, full_name: "Add more repositories..." }, ...repositories]
|
||||
: repositories;
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handleRepoSelection = (id: string | null) => {
|
||||
const repo = allRepositories.find((r) => r.id.toString() === id);
|
||||
if (repo) {
|
||||
const repo = finalRepositories.find((r) => r.id.toString() === id);
|
||||
if (id === "-1000") {
|
||||
if (config?.APP_SLUG)
|
||||
window.open(
|
||||
`https://github.com/apps/${config.APP_SLUG}/installations/new`,
|
||||
"_blank",
|
||||
);
|
||||
} else if (repo) {
|
||||
// set query param
|
||||
dispatch(setSelectedRepository(repo.full_name));
|
||||
posthog.capture("repository_selected");
|
||||
onSelect();
|
||||
@@ -46,10 +43,22 @@ export function GitHubRepositorySelector({
|
||||
};
|
||||
|
||||
const handleClearSelection = () => {
|
||||
// clear query param
|
||||
dispatch(setSelectedRepository(null));
|
||||
};
|
||||
|
||||
const emptyContent = "No results found.";
|
||||
const emptyContent = config?.APP_SLUG ? (
|
||||
<a
|
||||
href={`https://github.com/apps/${config.APP_SLUG}/installations/new`}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
className="underline"
|
||||
>
|
||||
Add more repositories...
|
||||
</a>
|
||||
) : (
|
||||
"No results found."
|
||||
);
|
||||
|
||||
return (
|
||||
<Autocomplete
|
||||
@@ -57,7 +66,6 @@ export function GitHubRepositorySelector({
|
||||
name="repo"
|
||||
aria-label="GitHub Repository"
|
||||
placeholder="Select a GitHub project"
|
||||
isVirtualized={false}
|
||||
selectedKey={selectedKey}
|
||||
inputProps={{
|
||||
classNames: {
|
||||
@@ -66,63 +74,20 @@ export function GitHubRepositorySelector({
|
||||
},
|
||||
}}
|
||||
onSelectionChange={(id) => handleRepoSelection(id?.toString() ?? null)}
|
||||
onInputChange={onInputChange}
|
||||
clearButtonProps={{ onClick: handleClearSelection }}
|
||||
listboxProps={{
|
||||
emptyContent,
|
||||
}}
|
||||
defaultFilter={(textValue, inputValue) =>
|
||||
!inputValue ||
|
||||
sanitizeQuery(textValue).includes(sanitizeQuery(inputValue))
|
||||
}
|
||||
>
|
||||
{config?.APP_MODE === "saas" &&
|
||||
config?.APP_SLUG &&
|
||||
((
|
||||
<AutocompleteItem key="install">
|
||||
<a
|
||||
href={`https://github.com/apps/${config.APP_SLUG}/installations/new`}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
Add more repositories...
|
||||
</a>
|
||||
</AutocompleteItem> // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
) as any)}
|
||||
{userRepositories.length > 0 && (
|
||||
<AutocompleteSection showDivider title="Your Repos">
|
||||
{userRepositories.map((repo) => (
|
||||
<AutocompleteItem
|
||||
data-testid="github-repo-item"
|
||||
key={repo.id}
|
||||
value={repo.id}
|
||||
className="data-[selected=true]:bg-default-100"
|
||||
textValue={repo.full_name}
|
||||
>
|
||||
{repo.full_name}
|
||||
</AutocompleteItem>
|
||||
))}
|
||||
</AutocompleteSection>
|
||||
)}
|
||||
{publicRepositories.length > 0 && (
|
||||
<AutocompleteSection showDivider title="Public Repos">
|
||||
{publicRepositories.map((repo) => (
|
||||
<AutocompleteItem
|
||||
data-testid="github-repo-item"
|
||||
key={repo.id}
|
||||
value={repo.id}
|
||||
className="data-[selected=true]:bg-default-100"
|
||||
textValue={repo.full_name}
|
||||
>
|
||||
{repo.full_name}
|
||||
<span className="ml-1 text-gray-400">
|
||||
({repo.stargazers_count || 0}⭐)
|
||||
</span>
|
||||
</AutocompleteItem>
|
||||
))}
|
||||
</AutocompleteSection>
|
||||
)}
|
||||
{finalRepositories.map((repo) => (
|
||||
<AutocompleteItem
|
||||
data-testid="github-repo-item"
|
||||
key={repo.id}
|
||||
value={repo.id}
|
||||
>
|
||||
{repo.full_name}
|
||||
</AutocompleteItem>
|
||||
))}
|
||||
</Autocomplete>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,39 +6,22 @@ import { ModalButton } from "#/components/shared/buttons/modal-button";
|
||||
import { ConnectToGitHubModal } from "#/components/shared/modals/connect-to-github-modal";
|
||||
import { ModalBackdrop } from "#/components/shared/modals/modal-backdrop";
|
||||
import { isGitHubErrorReponse } from "#/api/github-axios-instance";
|
||||
import { useAppRepositories } from "#/hooks/query/use-app-repositories";
|
||||
import { useSearchRepositories } from "#/hooks/query/use-search-repositories";
|
||||
import { useUserRepositories } from "#/hooks/query/use-user-repositories";
|
||||
import { sanitizeQuery } from "#/utils/sanitize-query";
|
||||
import { useDebounce } from "#/hooks/use-debounce";
|
||||
|
||||
interface GitHubRepositoriesSuggestionBoxProps {
|
||||
handleSubmit: () => void;
|
||||
repositories: GitHubRepository[];
|
||||
gitHubAuthUrl: string | null;
|
||||
user: GitHubErrorReponse | GitHubUser | null;
|
||||
}
|
||||
|
||||
export function GitHubRepositoriesSuggestionBox({
|
||||
handleSubmit,
|
||||
repositories,
|
||||
gitHubAuthUrl,
|
||||
user,
|
||||
}: GitHubRepositoriesSuggestionBoxProps) {
|
||||
const [connectToGitHubModalOpen, setConnectToGitHubModalOpen] =
|
||||
React.useState(false);
|
||||
const [searchQuery, setSearchQuery] = React.useState<string>("");
|
||||
const debouncedSearchQuery = useDebounce(searchQuery, 300);
|
||||
|
||||
// TODO: Use `useQueries` to fetch all repositories in parallel
|
||||
const { data: appRepositories } = useAppRepositories();
|
||||
const { data: userRepositories } = useUserRepositories();
|
||||
const { data: searchedRepos } = useSearchRepositories(
|
||||
sanitizeQuery(debouncedSearchQuery),
|
||||
);
|
||||
|
||||
const repositories =
|
||||
userRepositories?.pages.flatMap((page) => page.data) ||
|
||||
appRepositories?.pages.flatMap((page) => page.data) ||
|
||||
[];
|
||||
|
||||
const handleConnectToGitHub = () => {
|
||||
if (gitHubAuthUrl) {
|
||||
@@ -57,10 +40,8 @@ export function GitHubRepositoriesSuggestionBox({
|
||||
content={
|
||||
isLoggedIn ? (
|
||||
<GitHubRepositorySelector
|
||||
onInputChange={setSearchQuery}
|
||||
onSelect={handleSubmit}
|
||||
publicRepositories={searchedRepos}
|
||||
userRepositories={repositories}
|
||||
repositories={repositories}
|
||||
/>
|
||||
) : (
|
||||
<ModalButton
|
||||
|
||||
@@ -16,24 +16,16 @@ export function ProjectMenuDetails({
|
||||
}: ProjectMenuDetailsProps) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="flex flex-col min-w-0">
|
||||
<div className="flex flex-col">
|
||||
<a
|
||||
href={`https://github.com/${repoName}`}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
className="flex items-center gap-2 min-w-0"
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
{avatar && (
|
||||
<img
|
||||
src={avatar}
|
||||
alt=""
|
||||
className="w-4 h-4 rounded-full flex-shrink-0"
|
||||
/>
|
||||
)}
|
||||
<span className="text-sm leading-6 font-semibold truncate flex-1">
|
||||
{repoName}
|
||||
</span>
|
||||
<ExternalLinkIcon width={16} height={16} className="flex-shrink-0" />
|
||||
{avatar && <img src={avatar} alt="" className="w-4 h-4 rounded-full" />}
|
||||
<span className="text-sm leading-6 font-semibold">{repoName}</span>
|
||||
<ExternalLinkIcon width={16} height={16} />
|
||||
</a>
|
||||
<a
|
||||
href={lastCommit.html_url}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
import { FaListUl } from "react-icons/fa";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { useLocation } from "react-router";
|
||||
import FolderIcon from "#/icons/docs.svg?react";
|
||||
import { useAuth } from "#/context/auth-context";
|
||||
import { useGitHubUser } from "#/hooks/query/use-github-user";
|
||||
import { useIsAuthed } from "#/hooks/query/use-is-authed";
|
||||
@@ -11,20 +11,16 @@ import { ExitProjectButton } from "#/components/shared/buttons/exit-project-butt
|
||||
import { SettingsButton } from "#/components/shared/buttons/settings-button";
|
||||
import { LoadingSpinner } from "#/components/shared/loading-spinner";
|
||||
import { AccountSettingsModal } from "#/components/shared/modals/account-settings/account-settings-modal";
|
||||
import { ExitProjectConfirmationModal } from "#/components/shared/modals/exit-project-confirmation-modal";
|
||||
import { SettingsModal } from "#/components/shared/modals/settings/settings-modal";
|
||||
import { useSettingsUpToDate } from "#/context/settings-up-to-date-context";
|
||||
import { useSettings } from "#/hooks/query/use-settings";
|
||||
import { ConversationPanel } from "../conversation-panel/conversation-panel";
|
||||
import { MULTI_CONVERSATION_UI } from "#/utils/feature-flags";
|
||||
import { useEndSession } from "#/hooks/use-end-session";
|
||||
import { setCurrentAgentState } from "#/state/agent-slice";
|
||||
import { AgentState } from "#/types/agent-state";
|
||||
import { TooltipButton } from "#/components/shared/buttons/tooltip-button";
|
||||
import { ConversationPanelWrapper } from "../conversation-panel/conversation-panel-wrapper";
|
||||
import { cn } from "#/utils/utils";
|
||||
import { MULTI_CONVO_UI_IS_ENABLED } from "#/utils/constants";
|
||||
|
||||
export function Sidebar() {
|
||||
const dispatch = useDispatch();
|
||||
const endSession = useEndSession();
|
||||
const location = useLocation();
|
||||
const user = useGitHubUser();
|
||||
const { data: isAuthed } = useIsAuthed();
|
||||
const { logout } = useAuth();
|
||||
@@ -34,9 +30,11 @@ export function Sidebar() {
|
||||
const [accountSettingsModalOpen, setAccountSettingsModalOpen] =
|
||||
React.useState(false);
|
||||
const [settingsModalIsOpen, setSettingsModalIsOpen] = React.useState(false);
|
||||
|
||||
const [conversationPanelIsOpen, setConversationPanelIsOpen] =
|
||||
const [startNewProjectModalIsOpen, setStartNewProjectModalIsOpen] =
|
||||
React.useState(false);
|
||||
const [conversationPanelIsOpen, setConversationPanelIsOpen] = React.useState(
|
||||
MULTI_CONVO_UI_IS_ENABLED,
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
// If the github token is invalid, open the account settings modal again
|
||||
@@ -45,11 +43,6 @@ export function Sidebar() {
|
||||
}
|
||||
}, [user.isError]);
|
||||
|
||||
const handleEndSession = () => {
|
||||
dispatch(setCurrentAgentState(AgentState.LOADING));
|
||||
endSession();
|
||||
};
|
||||
|
||||
const handleAccountSettingsModalClose = () => {
|
||||
// If the user closes the modal without connecting to GitHub,
|
||||
// we need to log them out to clear the invalid token from the
|
||||
@@ -58,30 +51,22 @@ export function Sidebar() {
|
||||
setAccountSettingsModalOpen(false);
|
||||
};
|
||||
|
||||
const handleClickLogo = () => {
|
||||
if (location.pathname.startsWith("/conversations/"))
|
||||
setStartNewProjectModalIsOpen(true);
|
||||
};
|
||||
|
||||
const showSettingsModal =
|
||||
isAuthed && (!settingsAreUpToDate || settingsModalIsOpen);
|
||||
|
||||
return (
|
||||
<>
|
||||
<aside className="h-[40px] md:h-auto px-1 flex flex-row md:flex-col gap-1">
|
||||
<aside className="h-[40px] md:h-auto px-1 flex flex-row md:flex-col gap-1 relative">
|
||||
<nav className="flex flex-row md:flex-col items-center gap-[18px]">
|
||||
<div className="w-[34px] h-[34px] flex items-center justify-center mb-7">
|
||||
<AllHandsLogoButton onClick={handleEndSession} />
|
||||
<div className="w-[34px] h-[34px] flex items-center justify-center">
|
||||
<AllHandsLogoButton onClick={handleClickLogo} />
|
||||
</div>
|
||||
{user.isLoading && <LoadingSpinner size="small" />}
|
||||
<ExitProjectButton onClick={handleEndSession} />
|
||||
{MULTI_CONVERSATION_UI && (
|
||||
<TooltipButton
|
||||
data-testid="toggle-conversation-panel"
|
||||
tooltip="Conversations"
|
||||
ariaLabel="Conversations"
|
||||
onClick={() => setConversationPanelIsOpen((prev) => !prev)}
|
||||
>
|
||||
<FaListUl size={22} />
|
||||
</TooltipButton>
|
||||
)}
|
||||
<DocsButton />
|
||||
<SettingsButton onClick={() => setSettingsModalIsOpen(true)} />
|
||||
{!user.isLoading && (
|
||||
<UserActions
|
||||
user={
|
||||
@@ -91,14 +76,33 @@ export function Sidebar() {
|
||||
onClickAccountSettings={() => setAccountSettingsModalOpen(true)}
|
||||
/>
|
||||
)}
|
||||
<SettingsButton onClick={() => setSettingsModalIsOpen(true)} />
|
||||
{MULTI_CONVO_UI_IS_ENABLED && (
|
||||
<button
|
||||
data-testid="toggle-conversation-panel"
|
||||
type="button"
|
||||
onClick={() => setConversationPanelIsOpen((prev) => !prev)}
|
||||
className={cn(
|
||||
conversationPanelIsOpen ? "border-b-2 border-[#FFE165]" : "",
|
||||
)}
|
||||
>
|
||||
<FolderIcon width={28} height={28} />
|
||||
</button>
|
||||
)}
|
||||
<DocsButton />
|
||||
<ExitProjectButton
|
||||
onClick={() => setStartNewProjectModalIsOpen(true)}
|
||||
/>
|
||||
</nav>
|
||||
|
||||
{conversationPanelIsOpen && (
|
||||
<ConversationPanelWrapper isOpen={conversationPanelIsOpen}>
|
||||
<div
|
||||
className="absolute h-full left-[calc(100%+12px)] top-0 z-20" // 12px padding (sidebar parent)
|
||||
>
|
||||
<ConversationPanel
|
||||
onClose={() => setConversationPanelIsOpen(false)}
|
||||
/>
|
||||
</ConversationPanelWrapper>
|
||||
</div>
|
||||
)}
|
||||
</aside>
|
||||
|
||||
@@ -112,6 +116,11 @@ export function Sidebar() {
|
||||
onClose={() => setSettingsModalIsOpen(false)}
|
||||
/>
|
||||
))}
|
||||
{startNewProjectModalIsOpen && (
|
||||
<ExitProjectConfirmationModal
|
||||
onClose={() => setStartNewProjectModalIsOpen(false)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Tooltip } from "@nextui-org/react";
|
||||
import { LoadingSpinner } from "#/components/shared/loading-spinner";
|
||||
import DefaultUserAvatar from "#/icons/default-user.svg?react";
|
||||
import { cn } from "#/utils/utils";
|
||||
import { Avatar } from "./avatar";
|
||||
import { TooltipButton } from "#/components/shared/buttons/tooltip-button";
|
||||
|
||||
interface UserAvatarProps {
|
||||
onClick: () => void;
|
||||
@@ -11,11 +11,10 @@ interface UserAvatarProps {
|
||||
}
|
||||
|
||||
export function UserAvatar({ onClick, avatarUrl, isLoading }: UserAvatarProps) {
|
||||
return (
|
||||
<TooltipButton
|
||||
testId="user-avatar"
|
||||
tooltip="Account settings"
|
||||
ariaLabel="Account settings"
|
||||
const buttonContent = (
|
||||
<button
|
||||
data-testid="user-avatar"
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className={cn(
|
||||
"w-8 h-8 rounded-full flex items-center justify-center border-2 border-gray-200",
|
||||
@@ -31,6 +30,12 @@ export function UserAvatar({ onClick, avatarUrl, isLoading }: UserAvatarProps) {
|
||||
/>
|
||||
)}
|
||||
{isLoading && <LoadingSpinner size="small" />}
|
||||
</TooltipButton>
|
||||
</button>
|
||||
);
|
||||
|
||||
return (
|
||||
<Tooltip content="Account settings" closeDelay={100}>
|
||||
{buttonContent}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ export function AllHandsLogoButton({ onClick }: AllHandsLogoButtonProps) {
|
||||
ariaLabel="All Hands Logo"
|
||||
onClick={onClick}
|
||||
>
|
||||
<AllHandsLogo width={44} height={30} />
|
||||
<AllHandsLogo width={34} height={23} />
|
||||
</TooltipButton>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ export function ExitProjectButton({ onClick }: ExitProjectButtonProps) {
|
||||
onClick={onClick}
|
||||
testId="new-project-button"
|
||||
>
|
||||
<NewProjectIcon width={26} height={26} />
|
||||
<NewProjectIcon width={28} height={28} />
|
||||
</TooltipButton>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FaCog } from "react-icons/fa";
|
||||
import CogTooth from "#/assets/cog-tooth";
|
||||
import { TooltipButton } from "./tooltip-button";
|
||||
|
||||
interface SettingsButtonProps {
|
||||
@@ -13,7 +13,7 @@ export function SettingsButton({ onClick }: SettingsButtonProps) {
|
||||
ariaLabel="Settings"
|
||||
onClick={onClick}
|
||||
>
|
||||
<FaCog size={24} />
|
||||
<CogTooth />
|
||||
</TooltipButton>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Tooltip } from "@nextui-org/react";
|
||||
import React, { ReactNode } from "react";
|
||||
import { cn } from "#/utils/utils";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
interface TooltipButtonProps {
|
||||
children: ReactNode;
|
||||
@@ -9,7 +8,6 @@ interface TooltipButtonProps {
|
||||
href?: string;
|
||||
ariaLabel: string;
|
||||
testId?: string;
|
||||
className?: React.HTMLAttributes<HTMLButtonElement>["className"];
|
||||
}
|
||||
|
||||
export function TooltipButton({
|
||||
@@ -19,7 +17,6 @@ export function TooltipButton({
|
||||
href,
|
||||
ariaLabel,
|
||||
testId,
|
||||
className,
|
||||
}: TooltipButtonProps) {
|
||||
const buttonContent = (
|
||||
<button
|
||||
@@ -27,7 +24,7 @@ export function TooltipButton({
|
||||
aria-label={ariaLabel}
|
||||
data-testid={testId}
|
||||
onClick={onClick}
|
||||
className={cn("hover:opacity-80", className)}
|
||||
className="w-8 h-8 rounded-full hover:opacity-80 flex items-center justify-center"
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
@@ -38,7 +35,7 @@ export function TooltipButton({
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
className={cn("hover:opacity-80", className)}
|
||||
className="w-8 h-8 rounded-full hover:opacity-80 flex items-center justify-center"
|
||||
aria-label={ariaLabel}
|
||||
>
|
||||
{children}
|
||||
@@ -48,7 +45,7 @@ export function TooltipButton({
|
||||
);
|
||||
|
||||
return (
|
||||
<Tooltip content={tooltip} closeDelay={100} placement="right">
|
||||
<Tooltip content={tooltip} closeDelay={100}>
|
||||
{content}
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
@@ -20,7 +20,7 @@ export function AdvancedOptionSwitch({
|
||||
<Switch
|
||||
isDisabled={isDisabled}
|
||||
name="use-advanced-options"
|
||||
defaultSelected={showAdvancedOptions}
|
||||
isSelected={showAdvancedOptions}
|
||||
onValueChange={setShowAdvancedOptions}
|
||||
classNames={{
|
||||
thumb: cn(
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Select, SelectItem } from "@nextui-org/react";
|
||||
|
||||
interface RuntimeSizeSelectorProps {
|
||||
isDisabled: boolean;
|
||||
defaultValue?: number;
|
||||
}
|
||||
|
||||
export function RuntimeSizeSelector({
|
||||
isDisabled,
|
||||
defaultValue,
|
||||
}: RuntimeSizeSelectorProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<fieldset className="flex flex-col gap-2">
|
||||
<label
|
||||
htmlFor="runtime-size"
|
||||
className="font-[500] text-[#A3A3A3] text-xs"
|
||||
>
|
||||
{t("SETTINGS_FORM$RUNTIME_SIZE_LABEL")}
|
||||
</label>
|
||||
<Select
|
||||
id="runtime-size"
|
||||
name="runtime-size"
|
||||
defaultSelectedKeys={[String(defaultValue || 1)]}
|
||||
isDisabled={isDisabled}
|
||||
aria-label={t("SETTINGS_FORM$RUNTIME_SIZE_LABEL")}
|
||||
classNames={{
|
||||
trigger: "bg-[#27272A] rounded-md text-sm px-3 py-[10px]",
|
||||
}}
|
||||
>
|
||||
<SelectItem key="1" value={1}>
|
||||
1x (2 core, 8G)
|
||||
</SelectItem>
|
||||
<SelectItem
|
||||
key="2"
|
||||
value={2}
|
||||
isDisabled
|
||||
classNames={{
|
||||
description:
|
||||
"whitespace-normal break-words min-w-[300px] max-w-[300px]",
|
||||
base: "min-w-[300px] max-w-[300px]",
|
||||
}}
|
||||
description="Runtime sizes over 1 are disabled by default, please contact contact@all-hands.dev to get access to larger runtimes."
|
||||
>
|
||||
2x (4 core, 16G)
|
||||
</SelectItem>
|
||||
</Select>
|
||||
</fieldset>
|
||||
);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user