Compare commits

..

24 Commits

Author SHA1 Message Date
amanape
b1d46cf20f Merge 2025-04-08 22:55:41 +04:00
amanape
872f5ae8ae Add comments 2025-04-08 22:13:33 +04:00
dependabot[bot]
d1851cc3ee chore(deps): bump the version-all group in /docs with 2 updates (#7659)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-08 18:09:11 +00:00
dependabot[bot]
883bf5847b chore(deps): bump the version-all group with 4 updates (#7759)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-08 16:14:07 +00:00
sp.wack
1c8ab4d835 hotfix(frontend): Update route file names (#7528) 2025-04-08 15:41:35 +00:00
sp.wack
255e209886 Refactor file service (#7533) 2025-04-08 15:41:22 +00:00
Graham Neubig
c8904e4672 Add more extensive typing to openhands/integrations directory (#7730)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-04-08 10:54:07 -04:00
OpenHands
dcfc2da428 Fix issue #6037: [Bug]: [Resolver] crashes on main (#6284)
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-04-08 14:06:02 +00:00
Graham Neubig
84e28234e5 Add extensive typing to openhands/security directory (#7732)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-04-08 13:51:05 +00:00
Panduka Muditha
60e8b5841c feat: Add basic support for prompt-toolkit in the CLI (#7709)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Bashwara Undupitiya <bashwarau@verdentra.com>
2025-04-08 08:47:11 -04:00
Carlos Freund
dd03d9adce feat(frontend):Display path of file ops and cmd in headline (#7530)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Carlos Freund <carlosfreund@gmail.com>
2025-04-08 08:44:42 -04:00
Engel Nyst
9fa211bc27 Fix API version in the resolver (#7756)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-04-08 09:01:23 +00:00
Engel Nyst
22cf5144cc Fix integration test (#7747) 2025-04-07 22:31:50 -04:00
Ray Myers
99e493b3a4 feature - Build alternate ubuntu images (#7691) 2025-04-07 19:04:17 -05:00
mamoodi
e4d92d6f56 Update docs with updated info (#7714)
Co-authored-by: Robert Brennan <accounts@rbren.io>
2025-04-07 21:12:30 +00:00
Graham Neubig
a9d08852db Update llms.md to update recommended models (#7750) 2025-04-07 20:29:44 +00:00
Calvin Smith
408ad1ff2b Enables condensation by default (#7746)
Co-authored-by: Calvin Smith <calvin@all-hands.dev>
2025-04-07 11:47:51 -06:00
dependabot[bot]
69d4a8df7c chore(deps): bump the version-all group with 7 updates (#7743)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-07 17:20:59 +00:00
mamoodi
02f92164de Update issue templates (#7740) 2025-04-07 13:17:03 -04:00
Robert Brennan
1549927fa3 Update conversation metadata with cost and token metrics (#7561)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-04-07 17:09:50 +00:00
amanape
0a06af5d17 Refactor reset settings 2025-03-29 01:00:05 +04:00
amanape
e672a438df Refactor save settings 2025-03-29 00:34:01 +04:00
amanape
f1335dab04 Refactor handlers 2025-03-28 23:16:38 +04:00
amanape
1285404349 Refactor get settings 2025-03-28 23:01:36 +04:00
111 changed files with 1820 additions and 1643 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -50,47 +50,3 @@ docker run -it \
```
This command will start an interactive session in Docker where you can input tasks and receive responses from OpenHands.
## Examples of CLI Commands and Expected Outputs
Here are some examples of CLI commands and their expected outputs:
### Example 1: Simple Task
```bash
>> Write a Python script that prints "Hello, World!"
```
Expected Output:
```bash
🤖 Sure! Here is a Python script that prints "Hello, World!":
print("Hello, World!")
```
### Example 2: Bash Command
```bash
>> Create a directory named "test_dir"
```
Expected Output:
```bash
🤖 Creating a directory named "test_dir":
mkdir test_dir
```
### Example 3: Error Handling
```bash
>> Delete a non-existent file
```
Expected Output:
```bash
🤖 An error occurred. Please try again.
```

View File

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

View File

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

View File

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

View File

@@ -116,8 +116,6 @@ We use SemVer so `0.9` will automatically point to the latest `0.9.x` release, a
- For the most up-to-date development version, replace $VERSION in `openhands:$VERSION` and `runtime:$VERSION`, with `main`.
This version is unstable and is recommended for testing or development purposes only.
You can choose the tag that best suits your needs based on stability requirements and desired features.
For the development workflow, see [Development.md](https://github.com/All-Hands-AI/OpenHands/blob/main/Development.md).
Are you having trouble? Check out our [Troubleshooting Guide](https://docs.all-hands.dev/modules/usage/troubleshooting).

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,9 @@
# Runtime Configuration
:::note
This section is for users that would like to use a runtime other than Docker for OpenHands.
:::
A Runtime is an environment where the OpenHands agent can edit files and run
commands.

View File

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

26
docs/package-lock.json generated
View File

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

View File

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

View File

@@ -18,6 +18,23 @@ const sidebars: SidebarsConfig = {
label: 'Key Features',
id: 'usage/key-features',
},
{
type: 'category',
label: 'OpenHands Cloud',
items: [
{
type: 'doc',
label: 'Openhands Cloud',
id: 'usage/cloud/openhands-cloud',
},
{
type: 'doc',
label: 'Cloud GitHub Resolver',
id: 'usage/cloud/cloud-github-resolver',
},
],
},
{
type: 'category',
label: 'Prompting',
@@ -27,6 +44,17 @@ const sidebars: SidebarsConfig = {
label: 'Best Practices',
id: 'usage/prompting/prompting-best-practices',
},
],
},
{
type: 'category',
label: 'Customization',
items: [
{
type: 'doc',
label: 'Repository Customization',
id: 'usage/customization/repository',
},
{
type: 'category',
label: 'Microagents',
@@ -55,17 +83,6 @@ const sidebars: SidebarsConfig = {
},
],
},
{
type: 'category',
label: 'Customization',
items: [
{
type: 'doc',
label: 'Repository Customization',
id: 'usage/customization/repository',
},
],
},
{
type: 'category',
label: 'Usage Methods',
@@ -90,23 +107,6 @@ const sidebars: SidebarsConfig = {
label: 'Github Action',
id: 'usage/how-to/github-action',
},
{
type: 'category',
label: 'Cloud',
items: [
{
type: 'doc',
label: 'Openhands Cloud',
id: 'usage/cloud/openhands-cloud',
},
{
type: 'doc',
label: 'Cloud GitHub Resolver',
id: 'usage/cloud/cloud-github-resolver',
},
],
},
],
},
{

BIN
docs/static/img/oh-features.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,25 +7,26 @@ import OpenHands from "#/api/open-hands";
import { AuthProvider } from "#/context/auth-context";
import SettingsScreen from "#/routes/settings";
import * as AdvancedSettingsUtlls from "#/utils/has-advanced-settings-set";
import { MOCK_DEFAULT_USER_SETTINGS } from "#/mocks/handlers";
import * as ConsentHandlers from "#/utils/handle-capture-consent";
import AccountSettings from "#/routes/account-settings";
import { Provider } from "#/types/settings";
import { DEFAULT_SETTINGS } from "#/services/settings";
import { SettingsService } from "#/api/settings-service/settings-service.api";
import { GitProvider } from "#/api/settings-service/settings-service.types";
const toggleAdvancedSettings = async (user: UserEvent) => {
const advancedSwitch = await screen.findByTestId("advanced-settings-switch");
await user.click(advancedSwitch);
};
const mock_provider_tokens_are_set: Record<Provider, boolean> = {
const MOCK_PROVIDER_TOKENS_ARE_SET: Record<GitProvider, boolean> = {
github: true,
gitlab: false,
};
describe("Settings Screen", () => {
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
const saveSettingsSpy = vi.spyOn(OpenHands, "saveSettings");
const resetSettingsSpy = vi.spyOn(OpenHands, "resetSettings");
const getSettingsSpy = vi.spyOn(SettingsService, "getSettings");
const saveSettingsSpy = vi.spyOn(SettingsService, "saveSettings");
const resetSettingsSpy = vi.spyOn(SettingsService, "resetSettings");
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
const { handleLogoutMock } = vi.hoisted(() => ({
@@ -65,7 +66,9 @@ describe("Settings Screen", () => {
await waitFor(() => {
// Use queryAllByText to handle multiple elements with the same text
expect(screen.queryAllByText("SETTINGS$LLM_SETTINGS")).not.toHaveLength(0);
expect(screen.queryAllByText("SETTINGS$LLM_SETTINGS")).not.toHaveLength(
0,
);
screen.getByText("ACCOUNT_SETTINGS$ADDITIONAL_SETTINGS");
screen.getByText("BUTTON$RESET_TO_DEFAULTS");
screen.getByText("BUTTON$SAVE");
@@ -98,9 +101,7 @@ describe("Settings Screen", () => {
// TODO: Set a better unset indicator
it.skip("should render an indicator if the GitHub token is not set", async () => {
getSettingsSpy.mockResolvedValue({
...MOCK_DEFAULT_USER_SETTINGS,
});
getSettingsSpy.mockResolvedValue(DEFAULT_SETTINGS);
renderSettingsScreen();
@@ -119,8 +120,8 @@ describe("Settings Screen", () => {
it("should set '<hidden>' placeholder if the GitHub token is set", async () => {
getSettingsSpy.mockResolvedValue({
...MOCK_DEFAULT_USER_SETTINGS,
provider_tokens_set: mock_provider_tokens_are_set,
...DEFAULT_SETTINGS,
provider_tokens_set: MOCK_PROVIDER_TOKENS_ARE_SET,
});
renderSettingsScreen();
@@ -133,8 +134,8 @@ describe("Settings Screen", () => {
it("should render an indicator if the GitHub token is set", async () => {
getSettingsSpy.mockResolvedValue({
...MOCK_DEFAULT_USER_SETTINGS,
provider_tokens_set: mock_provider_tokens_are_set,
...DEFAULT_SETTINGS,
provider_tokens_set: MOCK_PROVIDER_TOKENS_ARE_SET,
});
renderSettingsScreen();
@@ -150,8 +151,6 @@ describe("Settings Screen", () => {
}
});
// Tests for DISCONNECT_FROM_GITHUB button removed as the button is no longer included in main
it("should not render the 'Configure GitHub Repositories' button if OSS mode", async () => {
getConfigSpy.mockResolvedValue({
APP_MODE: "oss",
@@ -210,7 +209,7 @@ describe("Settings Screen", () => {
it.skip("should not reset LLM Provider and Model if GitHub token is invalid", async () => {
const user = userEvent.setup();
getSettingsSpy.mockResolvedValue({
...MOCK_DEFAULT_USER_SETTINGS,
...DEFAULT_SETTINGS,
llm_model: "anthropic/claude-3-5-sonnet-20241022",
});
saveSettingsSpy.mockRejectedValueOnce(new Error("Invalid GitHub token"));
@@ -331,8 +330,8 @@ describe("Settings Screen", () => {
// TODO: Set a better unset indicator
it.skip("should render an indicator if the LLM API key is not set", async () => {
getSettingsSpy.mockResolvedValueOnce({
...MOCK_DEFAULT_USER_SETTINGS,
llm_api_key: null,
...DEFAULT_SETTINGS,
llm_api_key_set: null,
});
renderSettingsScreen();
@@ -352,7 +351,7 @@ describe("Settings Screen", () => {
it("should render an indicator if the LLM API key is set", async () => {
getSettingsSpy.mockResolvedValueOnce({
...MOCK_DEFAULT_USER_SETTINGS,
...DEFAULT_SETTINGS,
llm_api_key_set: true,
});
@@ -373,7 +372,7 @@ describe("Settings Screen", () => {
it("should set '<hidden>' placeholder if the LLM API key is set", async () => {
getSettingsSpy.mockResolvedValueOnce({
...MOCK_DEFAULT_USER_SETTINGS,
...DEFAULT_SETTINGS,
llm_api_key_set: true,
});
@@ -388,7 +387,7 @@ describe("Settings Screen", () => {
describe("Basic Model Selector", () => {
it("should set the provider and model", async () => {
getSettingsSpy.mockResolvedValue({
...MOCK_DEFAULT_USER_SETTINGS,
...DEFAULT_SETTINGS,
llm_model: "anthropic/claude-3-5-sonnet-20241022",
});
@@ -455,7 +454,7 @@ describe("Settings Screen", () => {
});
getSettingsSpy.mockResolvedValue({
...MOCK_DEFAULT_USER_SETTINGS,
...DEFAULT_SETTINGS,
remote_runtime_resource_factor: 1,
});
@@ -495,7 +494,7 @@ describe("Settings Screen", () => {
});
getSettingsSpy.mockResolvedValue({
...MOCK_DEFAULT_USER_SETTINGS,
...DEFAULT_SETTINGS,
});
renderSettingsScreen();
@@ -546,7 +545,7 @@ describe("Settings Screen", () => {
const user = userEvent.setup();
getSettingsSpy.mockResolvedValueOnce({
...MOCK_DEFAULT_USER_SETTINGS,
...DEFAULT_SETTINGS,
});
renderSettingsScreen();
@@ -563,7 +562,7 @@ describe("Settings Screen", () => {
// Mock the settings that will be returned after reset
// This should be the default settings with no advanced settings enabled
getSettingsSpy.mockResolvedValueOnce({
...MOCK_DEFAULT_USER_SETTINGS,
...DEFAULT_SETTINGS,
llm_base_url: "",
confirmation_mode: false,
security_analyzer: "",
@@ -614,7 +613,7 @@ describe("Settings Screen", () => {
it("should toggle advanced if user had set a custom model", async () => {
getSettingsSpy.mockResolvedValue({
...MOCK_DEFAULT_USER_SETTINGS,
...DEFAULT_SETTINGS,
llm_model: "some/custom-model",
});
renderSettingsScreen();
@@ -649,7 +648,7 @@ describe("Settings Screen", () => {
it("should have confirmation mode enabled if the user previously had it enabled", async () => {
getSettingsSpy.mockResolvedValue({
...MOCK_DEFAULT_USER_SETTINGS,
...DEFAULT_SETTINGS,
confirmation_mode: true,
});
@@ -666,7 +665,7 @@ describe("Settings Screen", () => {
// FIXME: security analyzer is not found for some reason...
it.skip("should have the values set if the user previously had them set", async () => {
getSettingsSpy.mockResolvedValue({
...MOCK_DEFAULT_USER_SETTINGS,
...DEFAULT_SETTINGS,
language: "no",
user_consents_to_analytics: true,
llm_base_url: "https://test.com",
@@ -701,7 +700,7 @@ describe("Settings Screen", () => {
it("should save the settings when the 'Save Changes' button is clicked", async () => {
const user = userEvent.setup();
getSettingsSpy.mockResolvedValue({
...MOCK_DEFAULT_USER_SETTINGS,
...DEFAULT_SETTINGS,
});
renderSettingsScreen();
@@ -728,7 +727,7 @@ describe("Settings Screen", () => {
it("should properly save basic LLM model settings", async () => {
const user = userEvent.setup();
getSettingsSpy.mockResolvedValue({
...MOCK_DEFAULT_USER_SETTINGS,
...DEFAULT_SETTINGS,
});
renderSettingsScreen();
@@ -764,7 +763,7 @@ describe("Settings Screen", () => {
it("should reset the settings when the 'Reset to defaults' button is clicked", async () => {
const user = userEvent.setup();
getSettingsSpy.mockResolvedValue(MOCK_DEFAULT_USER_SETTINGS);
getSettingsSpy.mockResolvedValue(DEFAULT_SETTINGS);
renderSettingsScreen();
@@ -795,7 +794,7 @@ describe("Settings Screen", () => {
// Mock the settings response after reset
getSettingsSpy.mockResolvedValueOnce({
...MOCK_DEFAULT_USER_SETTINGS,
...DEFAULT_SETTINGS,
llm_base_url: "",
confirmation_mode: false,
security_analyzer: "",
@@ -820,7 +819,7 @@ describe("Settings Screen", () => {
it("should cancel the reset when the 'Cancel' button is clicked", async () => {
const user = userEvent.setup();
getSettingsSpy.mockResolvedValue(MOCK_DEFAULT_USER_SETTINGS);
getSettingsSpy.mockResolvedValue(DEFAULT_SETTINGS);
renderSettingsScreen();
@@ -928,7 +927,7 @@ describe("Settings Screen", () => {
it("should not send an empty LLM API Key if the user submits an empty string but already has it set", async () => {
const user = userEvent.setup();
getSettingsSpy.mockResolvedValue({
...MOCK_DEFAULT_USER_SETTINGS,
...DEFAULT_SETTINGS,
llm_api_key_set: true,
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -11,14 +11,6 @@ interface ActionSuggestionsProps {
onSuggestionsClick: (value: string) => void;
}
// Define button configurations to reduce duplication
interface ButtonConfig {
label: string;
value: string;
eventName: string;
callback?: () => void;
}
export function ActionSuggestions({
onSuggestionsClick,
}: ActionSuggestionsProps) {
@@ -38,37 +30,16 @@ export function ActionSuggestions({
const pr = isGitLab ? "merge request" : "pull request";
const prShort = isGitLab ? "MR" : "PR";
// Define the button configurations
const PUSH_TO_BRANCH: ButtonConfig = {
label: t(I18nKey.ACTION$PUSH_TO_BRANCH),
value: `Please push the changes to a remote branch on ${
const terms = {
pr,
prShort,
pushToBranch: `Please push the changes to a remote branch on ${
isGitLab ? "GitLab" : "GitHub"
}, but do NOT create a ${pr}. Please use the exact SAME branch name as the one you are currently on.`,
eventName: "push_to_branch_button_clicked",
};
const PUSH_AND_CREATE_PR: ButtonConfig = {
label: t(I18nKey.ACTION$PUSH_CREATE_PR),
value: `Please push the changes to ${
createPR: `Please push the changes to ${
isGitLab ? "GitLab" : "GitHub"
} and open a ${pr}. Please create a meaningful branch name that describes the changes. If a ${pr} template exists in the repository, please follow it when creating the ${prShort} description.`,
eventName: "create_pr_button_clicked",
callback: () => setHasPullRequest(true),
};
const PUSH_TO_PR: ButtonConfig = {
label: t(I18nKey.ACTION$PUSH_CHANGES_TO_PR),
value: `Please push the latest changes to the existing ${pr}.`,
eventName: "push_to_pr_button_clicked",
};
// Helper function to handle button clicks
const handleButtonClick = (config: ButtonConfig) => {
posthog.capture(config.eventName);
onSuggestionsClick(config.value);
if (config.callback) {
config.callback();
}
pushToPR: `Please push the latest changes to the existing ${pr}.`,
};
return (
@@ -79,26 +50,36 @@ export function ActionSuggestions({
<>
<SuggestionItem
suggestion={{
label: PUSH_TO_BRANCH.label,
value: PUSH_TO_BRANCH.value,
label: t(I18nKey.ACTION$PUSH_TO_BRANCH),
value: terms.pushToBranch,
}}
onClick={(value) => {
posthog.capture("push_to_branch_button_clicked");
onSuggestionsClick(value);
}}
onClick={() => handleButtonClick(PUSH_TO_BRANCH)}
/>
<SuggestionItem
suggestion={{
label: PUSH_AND_CREATE_PR.label,
value: PUSH_AND_CREATE_PR.value,
label: t(I18nKey.ACTION$PUSH_CREATE_PR),
value: terms.createPR,
}}
onClick={(value) => {
posthog.capture("create_pr_button_clicked");
onSuggestionsClick(value);
setHasPullRequest(true);
}}
onClick={() => handleButtonClick(PUSH_AND_CREATE_PR)}
/>
</>
) : (
<SuggestionItem
suggestion={{
label: PUSH_TO_PR.label,
value: PUSH_TO_PR.value,
label: t(I18nKey.ACTION$PUSH_CHANGES_TO_PR),
value: terms.pushToPR,
}}
onClick={(value) => {
posthog.capture("push_to_pr_button_clicked");
onSuggestionsClick(value);
}}
onClick={() => handleButtonClick(PUSH_TO_PR)}
/>
)}
</div>

View File

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

View File

@@ -26,6 +26,8 @@ export const Messages: React.FC<MessagesProps> = React.memo(
id={message.translationID}
message={message.content}
success={message.success}
observation={message.observation}
action={message.action}
/>
{shouldShowConfirmationButtons && <ConfirmationButtons />}
</div>

View File

@@ -0,0 +1,37 @@
import { ReactNode } from "react";
import EventLogger from "#/utils/event-logger";
const decodeHtmlEntities = (text: string): string => {
const textarea = document.createElement("textarea");
textarea.innerHTML = text;
return textarea.value;
};
function MonoComponent(props: { children?: ReactNode }) {
const { children } = props;
const decodeString = (str: string): string => {
try {
return decodeHtmlEntities(str);
} catch (e) {
EventLogger.error(String(e));
return str;
}
};
if (Array.isArray(children)) {
const processedChildren = children.map((child) =>
typeof child === "string" ? decodeString(child) : child,
);
return <strong className="font-mono">{processedChildren}</strong>;
}
if (typeof children === "string") {
return <strong className="font-mono">{decodeString(children)}</strong>;
}
return <strong className="font-mono">{children}</strong>;
}
export { MonoComponent };

View File

@@ -0,0 +1,67 @@
import { ReactNode } from "react";
import EventLogger from "#/utils/event-logger";
/**
* Decodes HTML entities in a string
* @param text The text to decode
* @returns The decoded text
*/
const decodeHtmlEntities = (text: string): string => {
const textarea = document.createElement("textarea");
textarea.innerHTML = text;
return textarea.value;
};
/**
* Extracts the filename from a path
* @param path The full path
* @returns The filename (last part of the path)
*/
const extractFilename = (path: string): string => {
if (!path) return "";
// Handle both Unix and Windows paths
const parts = path.split(/[/\\]/);
return parts[parts.length - 1];
};
/**
* Component that displays only the filename in the text but shows the full path on hover
* Similar to MonoComponent but with path-specific functionality
*/
function PathComponent(props: { children?: ReactNode }) {
const { children } = props;
const processPath = (path: string) => {
try {
// First decode any HTML entities in the path
const decodedPath = decodeHtmlEntities(path);
// Extract the filename from the decoded path
const filename = extractFilename(decodedPath);
return (
<span className="font-mono" title={decodedPath}>
{filename}
</span>
);
} catch (e) {
// Just log the error without any message to avoid localization issues
EventLogger.error(String(e));
return <span className="font-mono">{path}</span>;
}
};
if (Array.isArray(children)) {
const processedChildren = children.map((child) =>
typeof child === "string" ? processPath(child) : child,
);
return <strong className="font-mono">{processedChildren}</strong>;
}
if (typeof children === "string") {
return <strong>{processPath(children)}</strong>;
}
return <strong className="font-mono">{children}</strong>;
}
export { PathComponent };

View File

@@ -9,15 +9,16 @@ import { extractSettings } from "#/utils/settings-utils";
import { useEndSession } from "#/hooks/use-end-session";
import { ModalBackdrop } from "../modal-backdrop";
import { ModelSelector } from "./model-selector";
import { Settings } from "#/types/settings";
import { BrandButton } from "#/components/features/settings/brand-button";
import { KeyStatusIcon } from "#/components/features/settings/key-status-icon";
import { SettingsInput } from "#/components/features/settings/settings-input";
import { HelpLink } from "#/components/features/settings/help-link";
import { useSaveSettings } from "#/hooks/mutation/use-save-settings";
import { UserSettings } from "#/api/settings-service/settings-service.types";
import { DEFAULT_SETTINGS } from "#/services/settings";
interface SettingsFormProps {
settings: Settings;
settings: UserSettings | undefined;
models: string[];
onClose: () => void;
}
@@ -42,17 +43,16 @@ export function SettingsForm({ settings, models, onClose }: SettingsFormProps) {
const handleFormSubmission = async (formData: FormData) => {
const newSettings = extractSettings(formData);
await saveUserSettings(newSettings, {
saveUserSettings(newSettings, {
onSuccess: () => {
onClose();
resetOngoingSession();
posthog.capture("settings_saved", {
LLM_MODEL: newSettings.LLM_MODEL,
LLM_API_KEY_SET: newSettings.LLM_API_KEY_SET ? "SET" : "UNSET",
LLM_MODEL: newSettings.llm_model,
LLM_API_KEY: newSettings.llm_api_key_set ? "SET" : "UNSET",
REMOTE_RUNTIME_RESOURCE_FACTOR:
newSettings.REMOTE_RUNTIME_RESOURCE_FACTOR,
newSettings.remote_runtime_resource_factor,
});
},
});
@@ -74,7 +74,7 @@ export function SettingsForm({ settings, models, onClose }: SettingsFormProps) {
}
};
const isLLMKeySet = settings.LLM_API_KEY_SET;
const isLLMKeySet = !!settings?.llm_api_key_set;
return (
<div>
@@ -87,7 +87,7 @@ export function SettingsForm({ settings, models, onClose }: SettingsFormProps) {
<div className="flex flex-col gap-4">
<ModelSelector
models={organizeModelsAndProviders(models)}
currentModel={settings.LLM_MODEL}
currentModel={settings?.llm_model || DEFAULT_SETTINGS.llm_model}
/>
<SettingsInput

View File

@@ -5,11 +5,10 @@ import { I18nKey } from "#/i18n/declaration";
import { LoadingSpinner } from "../../loading-spinner";
import { ModalBackdrop } from "../modal-backdrop";
import { SettingsForm } from "./settings-form";
import { Settings } from "#/types/settings";
import { DEFAULT_SETTINGS } from "#/services/settings";
import { UserSettings } from "#/api/settings-service/settings-service.types";
interface SettingsModalProps {
settings?: Settings;
settings: UserSettings | undefined;
onClose: () => void;
}
@@ -47,7 +46,7 @@ export function SettingsModal({ onClose, settings }: SettingsModalProps) {
)}
{aiConfigOptions.data && (
<SettingsForm
settings={settings || DEFAULT_SETTINGS}
settings={settings}
models={aiConfigOptions.data?.models}
onClose={onClose}
/>

View File

@@ -1,37 +1,29 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { DEFAULT_SETTINGS } from "#/services/settings";
import OpenHands from "#/api/open-hands";
import { PostSettings, PostApiSettings } from "#/types/settings";
import { useSettings } from "../query/use-settings";
import { UserSettings } from "#/api/settings-service/settings-service.types";
import { SettingsService } from "#/api/settings-service/settings-service.api";
const saveSettingsMutationFn = async (
settings: Partial<PostSettings> | null,
settings: Partial<UserSettings> | null,
) => {
// If settings is null, we're resetting
if (settings === null) {
await OpenHands.resetSettings();
await SettingsService.resetSettings();
return;
}
const apiSettings: Partial<PostApiSettings> = {
llm_model: settings.LLM_MODEL,
llm_base_url: settings.LLM_BASE_URL,
agent: settings.AGENT || DEFAULT_SETTINGS.AGENT,
language: settings.LANGUAGE || DEFAULT_SETTINGS.LANGUAGE,
confirmation_mode: settings.CONFIRMATION_MODE,
security_analyzer: settings.SECURITY_ANALYZER,
llm_api_key:
settings.llm_api_key === ""
const safeSettings: Partial<UserSettings> = {
...settings,
agent: settings.agent || DEFAULT_SETTINGS.agent,
language: settings.language || DEFAULT_SETTINGS.language,
llm_api_key_set:
settings.llm_api_key_set === ""
? ""
: settings.llm_api_key?.trim() || undefined,
remote_runtime_resource_factor: settings.REMOTE_RUNTIME_RESOURCE_FACTOR,
enable_default_condenser: settings.ENABLE_DEFAULT_CONDENSER,
enable_sound_notifications: settings.ENABLE_SOUND_NOTIFICATIONS,
user_consents_to_analytics: settings.user_consents_to_analytics,
provider_tokens: settings.provider_tokens,
: settings.llm_api_key_set?.trim() || undefined,
};
await OpenHands.saveSettings(apiSettings);
await SettingsService.saveSettings(safeSettings);
};
export const useSaveSettings = () => {
@@ -39,7 +31,7 @@ export const useSaveSettings = () => {
const { data: currentSettings } = useSettings();
return useMutation({
mutationFn: async (settings: Partial<PostSettings> | null) => {
mutationFn: async (settings: Partial<UserSettings> | null) => {
if (settings === null) {
await saveSettingsMutationFn(null);
return;

View File

@@ -1,16 +0,0 @@
import { useMutation } from "@tanstack/react-query";
import OpenHands from "#/api/open-hands";
import { useConversation } from "#/context/conversation-context";
type UploadFilesArgs = {
files: File[];
};
export const useUploadFiles = () => {
const { conversationId } = useConversation();
return useMutation({
mutationFn: ({ files }: UploadFilesArgs) =>
OpenHands.uploadFiles(conversationId, files),
});
};

View File

@@ -1,6 +1,6 @@
import { useQuery } from "@tanstack/react-query";
import OpenHands from "#/api/open-hands";
import { useConversation } from "#/context/conversation-context";
import { FileService } from "#/api/file-service/file-service.api";
interface UseListFileConfig {
path: string;
@@ -9,8 +9,8 @@ interface UseListFileConfig {
export const useListFile = (config: UseListFileConfig) => {
const { conversationId } = useConversation();
return useQuery({
queryKey: ["file", conversationId, config.path],
queryFn: () => OpenHands.getFile(conversationId, config.path),
queryKey: ["files", conversationId, config.path],
queryFn: () => FileService.getFile(conversationId, config.path),
enabled: false, // don't fetch by default, trigger manually via `refetch`
});
};

View File

@@ -1,9 +1,9 @@
import { useQuery } from "@tanstack/react-query";
import { useSelector } from "react-redux";
import OpenHands from "#/api/open-hands";
import { useConversation } from "#/context/conversation-context";
import { RootState } from "#/store";
import { RUNTIME_INACTIVE_STATES } from "#/types/agent-state";
import { FileService } from "#/api/file-service/file-service.api";
interface UseListFilesConfig {
path?: string;
@@ -17,12 +17,12 @@ const DEFAULT_CONFIG: UseListFilesConfig = {
export const useListFiles = (config: UseListFilesConfig = DEFAULT_CONFIG) => {
const { conversationId } = useConversation();
const { curAgentState } = useSelector((state: RootState) => state.agent);
const isActive = !RUNTIME_INACTIVE_STATES.includes(curAgentState);
const runtimeIsActive = !RUNTIME_INACTIVE_STATES.includes(curAgentState);
return useQuery({
queryKey: ["files", conversationId, config?.path],
queryFn: () => OpenHands.getFiles(conversationId, config?.path),
enabled: !!(isActive && config?.enabled),
queryFn: () => FileService.getFiles(conversationId, config?.path),
enabled: runtimeIsActive && !!config?.enabled,
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 15, // 15 minutes
});

View File

@@ -1,29 +1,14 @@
import { useQuery } from "@tanstack/react-query";
import React from "react";
import posthog from "posthog-js";
import OpenHands from "#/api/open-hands";
import { useAuth } from "#/context/auth-context";
import { SettingsService } from "#/api/settings-service/settings-service.api";
import { ClientUserSettings } from "#/api/settings-service/settings-service.types";
import { DEFAULT_SETTINGS } from "#/services/settings";
const getSettingsQueryFn = async () => {
const apiSettings = await OpenHands.getSettings();
return {
LLM_MODEL: apiSettings.llm_model,
LLM_BASE_URL: apiSettings.llm_base_url,
AGENT: apiSettings.agent,
LANGUAGE: apiSettings.language,
CONFIRMATION_MODE: apiSettings.confirmation_mode,
SECURITY_ANALYZER: apiSettings.security_analyzer,
LLM_API_KEY_SET: apiSettings.llm_api_key_set,
REMOTE_RUNTIME_RESOURCE_FACTOR: apiSettings.remote_runtime_resource_factor,
PROVIDER_TOKENS_SET: apiSettings.provider_tokens_set,
ENABLE_DEFAULT_CONDENSER: apiSettings.enable_default_condenser,
ENABLE_SOUND_NOTIFICATIONS: apiSettings.enable_sound_notifications,
USER_CONSENTS_TO_ANALYTICS: apiSettings.user_consents_to_analytics,
PROVIDER_TOKENS: apiSettings.provider_tokens,
IS_NEW_USER: false,
};
const settingsQueryFn = async (): Promise<ClientUserSettings> => {
const settings = await SettingsService.getSettings();
return { ...settings, is_new_user: false };
};
export const useSettings = () => {
@@ -32,7 +17,7 @@ export const useSettings = () => {
const query = useQuery({
queryKey: ["settings", providerTokensSet],
queryFn: getSettingsQueryFn,
queryFn: settingsQueryFn,
// Only retry if the error is not a 404 because we
// would want to show the modal immediately if the
// settings are not found
@@ -45,42 +30,36 @@ export const useSettings = () => {
});
React.useEffect(() => {
if (query.isFetched && query.data?.LLM_API_KEY_SET) {
if (query.isFetched && query.data?.llm_api_key_set) {
posthog.capture("user_activated");
}
}, [query.data?.LLM_API_KEY_SET, query.isFetched]);
}, [query.data?.llm_api_key_set, query.isFetched]);
React.useEffect(() => {
if (query.data?.PROVIDER_TOKENS_SET) {
const providers = query.data.PROVIDER_TOKENS_SET;
if (query.isFetched && query.data?.provider_tokens_set) {
const providers = query.data.provider_tokens_set;
const setProviders = (
Object.keys(providers) as Array<keyof typeof providers>
).filter((key) => providers[key]);
setProviderTokensSet(setProviders);
const atLeastOneSet = Object.values(query.data.PROVIDER_TOKENS_SET).some(
const atLeastOneSet = Object.values(query.data.provider_tokens_set).some(
(value) => value,
);
setProvidersAreSet(atLeastOneSet);
}
}, [query.data?.PROVIDER_TOKENS_SET, query.isFetched]);
}, [query.data?.provider_tokens_set, query.isFetched]);
// We want to return the defaults if the settings aren't found so the user can still see the
// options to make their initial save. We don't set the defaults in `initialData` above because
// that would prepopulate the data to the cache and mess with expectations. Read more:
// https://tanstack.com/query/latest/docs/framework/react/guides/initial-query-data#using-initialdata-to-prepopulate-a-query
if (query.error?.status === 404) {
// Create a new object with only the properties we need, avoiding rest destructuring
// Object rest destructuring on a query will observe all changes to the query, leading to excessive re-renders.
// Only return the specific properties we need to avoid this.
return {
data: DEFAULT_SETTINGS,
error: query.error,
isError: query.isError,
isLoading: query.isLoading,
isFetching: query.isFetching,
isFetched: query.isFetched,
isFetching: query.isFetching,
isSuccess: query.isSuccess,
status: query.status,
fetchStatus: query.fetchStatus,
refetch: query.refetch,
};
}

View File

@@ -5,7 +5,7 @@ import { generateAgentStateChangeEvent } from "#/services/agent-state-service";
import { addErrorMessage } from "#/state/chat-slice";
import { AgentState } from "#/types/agent-state";
import { ErrorObservation } from "#/types/core/observations";
import { useEndSession } from "../../../hooks/use-end-session";
import { useEndSession } from "./use-end-session";
import { displayErrorToast } from "#/utils/custom-toast-handlers";
interface ServerError {

View File

@@ -15,7 +15,7 @@ export const useMigrateUserConsent = () => {
if (userAnalyticsConsent) {
args?.handleAnalyticsWasPresentInLocalStorage();
await saveUserSettings(
saveUserSettings(
{ user_consents_to_analytics: userAnalyticsConsent === "true" },
{
onSuccess: () => {

View File

@@ -26,7 +26,7 @@ export const useNotification = () => {
// 4. Not a settings-related notification
if (
options?.playSound === true && // Must be explicitly true
settings?.ENABLE_SOUND_NOTIFICATIONS &&
settings?.enable_sound_notifications &&
audioRef.current &&
!title.includes("BUTTON$") // Don't play for button/settings actions
) {
@@ -49,7 +49,7 @@ export const useNotification = () => {
return undefined;
},
[settings?.ENABLE_SOUND_NOTIFICATIONS],
[settings?.enable_sound_notifications],
);
return { notify };

View File

@@ -4751,19 +4751,19 @@
"tr": "Çalışma alanını kapat"
},
"ACTION_MESSAGE$RUN": {
"en": "Running a bash command",
"zh-CN": "运行",
"zh-TW": "執行",
"ko-KR": "실행",
"ja": "実行",
"no": "Kjører en bash-kommando",
"ar": "تشغيل أمر باش",
"de": "Führt einen Bash-Befehl aus",
"fr": "Exécution d'une commande bash",
"it": "Esecuzione di un comando bash",
"pt": "Executando um comando bash",
"es": "Ejecutando un comando bash",
"tr": "Bash komutu çalıştırılıyor"
"en": "Running <cmd>{{action.payload.args.command}}</cmd>",
"zh-CN": "运行 <cmd>{{action.payload.args.command}}</cmd>",
"zh-TW": "執行 <cmd>{{action.payload.args.command}}</cmd>",
"ko-KR": "실행 <cmd>{{action.payload.args.command}}</cmd>",
"ja": "実行 <cmd>{{action.payload.args.command}}</cmd>",
"no": "Kjører <cmd>{{action.payload.args.command}}</cmd>",
"ar": "تشغيل <cmd>{{action.payload.args.command}}</cmd>",
"de": "Führt <cmd>{{action.payload.args.command}}</cmd> aus",
"fr": "Exécution de <cmd>{{action.payload.args.command}}</cmd>",
"it": "Esecuzione di <cmd>{{action.payload.args.command}}</cmd>",
"pt": "Executando <cmd>{{action.payload.args.command}}</cmd>",
"es": "Ejecutando <cmd>{{action.payload.args.command}}</cmd>",
"tr": "<cmd>{{action.payload.args.command}}</cmd> çalıştırılıyor"
},
"ACTION_MESSAGE$RUN_IPYTHON": {
"en": "Running a Python command",
@@ -4781,49 +4781,49 @@
"tr": "Python komutu çalıştırılıyor"
},
"ACTION_MESSAGE$READ": {
"en": "Reading the contents of a file",
"zh-CN": "读取",
"zh-TW": "讀取",
"ko-KR": "읽기",
"ja": "読み取り",
"no": "Leser innholdet i en fil",
"ar": "قراءة محتويات ملف",
"de": "Liest den Inhalt einer Datei",
"fr": "Lecture du contenu d'un fichier",
"it": "Lettura del contenuto di un file",
"pt": "Lendo o conteúdo de um arquivo",
"es": "Leyendo el contenido de un archivo",
"tr": "Dosya içeriği okunuyor"
"en": "Reading <path>{{action.payload.args.path}}</path>",
"zh-CN": "读取 <path>{{action.payload.args.path}}</path>",
"zh-TW": "讀取 <path>{{action.payload.args.path}}</path>",
"ko-KR": "읽기 <path>{{action.payload.args.path}}</path>",
"ja": "読み取り <path>{{action.payload.args.path}}</path>",
"no": "Leser <path>{{action.payload.args.path}}</path>",
"ar": "قراءة <path>{{action.payload.args.path}}</path>",
"de": "Liest <path>{{action.payload.args.path}}</path>",
"fr": "Lecture de <path>{{action.payload.args.path}}</path>",
"it": "Lettura di <path>{{action.payload.args.path}}</path>",
"pt": "Lendo <path>{{action.payload.args.path}}</path>",
"es": "Leyendo <path>{{action.payload.args.path}}</path>",
"tr": "<path>{{action.payload.args.path}}</path> okunuyor"
},
"ACTION_MESSAGE$EDIT": {
"en": "Editing the contents of a file",
"zh-CN": "编辑",
"zh-TW": "編輯",
"ko-KR": "편집",
"ja": "編集",
"no": "Redigerer innholdet i en fil",
"ar": "تحرير محتويات ملف",
"de": "Bearbeitet den Inhalt einer Datei",
"fr": "Modification du contenu d'un fichier",
"it": "Modifica del contenuto di un file",
"pt": "Editando o conteúdo de um arquivo",
"es": "Editando el contenido de un archivo",
"tr": "Dosya içeriği düzenleniyor"
"en": "Editing <path>{{action.payload.args.path}}</path>",
"zh-CN": "编辑 <path>{{action.payload.args.path}}</path>",
"zh-TW": "編輯 <path>{{action.payload.args.path}}</path>",
"ko-KR": "편집 <path>{{action.payload.args.path}}</path>",
"ja": "編集 <path>{{action.payload.args.path}}</path>",
"no": "Redigerer <path>{{action.payload.args.path}}</path>",
"ar": "تحرير <path>{{action.payload.args.path}}</path>",
"de": "Bearbeitet <path>{{action.payload.args.path}}</path>",
"fr": "Modification de <path>{{action.payload.args.path}}</path>",
"it": "Modifica di <path>{{action.payload.args.path}}</path>",
"pt": "Editando <path>{{action.payload.args.path}}</path>",
"es": "Editando <path>{{action.payload.args.path}}</path>",
"tr": "<path>{{action.payload.args.path}}</path> düzenleniyor"
},
"ACTION_MESSAGE$WRITE": {
"en": "Writing to a file",
"zh-CN": "写入",
"zh-TW": "寫入",
"ko-KR": "쓰기",
"ja": "書き込み",
"no": "Skriver til en fil",
"ar": "الكتابة إلى ملف",
"de": "Schreibt in eine Datei",
"fr": "Écriture dans un fichier",
"it": "Scrittura su file",
"pt": "Escrevendo em um arquivo",
"es": "Escribiendo en un archivo",
"tr": "Dosyaya yazılıyor"
"en": "Writing to <path>{{action.payload.args.path}}</path>",
"zh-CN": "写入 <path>{{action.payload.args.path}}</path>",
"zh-TW": "寫入 <path>{{action.payload.args.path}}</path>",
"ko-KR": "쓰기 <path>{{action.payload.args.path}}</path>",
"ja": "書き込み <path>{{action.payload.args.path}}</path>",
"no": "Skriver til <path>{{action.payload.args.path}}</path>",
"ar": "الكتابة إلى <path>{{action.payload.args.path}}</path>",
"de": "Schreibt in <path>{{action.payload.args.path}}</path>",
"fr": "Écriture dans <path>{{action.payload.args.path}}</path>",
"it": "Scrittura su <path>{{action.payload.args.path}}</path>",
"pt": "Escrevendo em <path>{{action.payload.args.path}}</path>",
"es": "Escribiendo en <path>{{action.payload.args.path}}</path>",
"tr": "<path>{{action.payload.args.path}}</path> dosyasına yazılıyor"
},
"ACTION_MESSAGE$BROWSE": {
"en": "Browsing the web",
@@ -4871,19 +4871,19 @@
"tr": "Düşünüyor"
},
"OBSERVATION_MESSAGE$RUN": {
"en": "Ran a bash command",
"zh-CN": "运行",
"zh-TW": "執行",
"ko-KR": "실행",
"ja": "実行",
"no": "Kjørte en bash-kommando",
"ar": "تم تشغيل أمر باش",
"de": "Führte einen Bash-Befehl aus",
"fr": "A exécuté une commande bash",
"it": "Ha eseguito un comando bash",
"pt": "Executou um comando bash",
"es": "Ejecutó un comando bash",
"tr": "Bash komutu çalıştırıldı"
"en": "Ran <cmd>{{observation.payload.extras.command}}</cmd>",
"zh-CN": "运行 <cmd>{{observation.payload.extras.command}}</cmd>",
"zh-TW": "執行 <cmd>{{observation.payload.extras.command}}</cmd>",
"ko-KR": "실행 <cmd>{{observation.payload.extras.command}}</cmd>",
"ja": "実行 <cmd>{{observation.payload.extras.command}}</cmd>",
"no": "Kjørte <cmd>{{observation.payload.extras.command}}</cmd>",
"ar": "تم تشغيل <cmd>{{observation.payload.extras.command}}</cmd>",
"de": "Führte <cmd>{{observation.payload.extras.command}}</cmd> aus",
"fr": "A exécuté <cmd>{{observation.payload.extras.command}}</cmd>",
"it": "Ha eseguito <cmd>{{observation.payload.extras.command}}</cmd>",
"pt": "Executou <cmd>{{observation.payload.extras.command}}</cmd>",
"es": "Ejecutó <cmd>{{observation.payload.extras.command}}</cmd>",
"tr": "<cmd>{{observation.payload.extras.command}}</cmd> çalıştırıldı"
},
"OBSERVATION_MESSAGE$RUN_IPYTHON": {
"en": "Ran a Python command",
@@ -4901,49 +4901,49 @@
"tr": "Python komutu çalıştırıldı"
},
"OBSERVATION_MESSAGE$READ": {
"en": "Read the contents of a file",
"zh-CN": "读取",
"zh-TW": "讀取",
"ko-KR": "읽기",
"ja": "読み取り",
"no": "Leste innholdet i en fil",
"ar": "تمت قراءة محتويات ملف",
"de": "Las den Inhalt einer Datei",
"fr": "A lu le contenu d'un fichier",
"it": "Ha letto il contenuto di un file",
"pt": "Leu o conteúdo de um arquivo",
"es": "Leyó el contenido de un archivo",
"tr": "Dosya içeriği okundu"
"en": "Read <path>{{observation.payload.extras.path}}</path>",
"zh-CN": "读取 <path>{{observation.payload.extras.path}}</path>",
"zh-TW": "讀取 <path>{{observation.payload.extras.path}}</path>",
"ko-KR": "읽기 <path>{{observation.payload.extras.path}}</path>",
"ja": "読み取り <path>{{observation.payload.extras.path}}</path>",
"no": "Leste <path>{{observation.payload.extras.path}}</path>",
"ar": "تمت قراءة <path>{{observation.payload.extras.path}}</path>",
"de": "Las <path>{{observation.payload.extras.path}}</path>",
"fr": "A lu <path>{{observation.payload.extras.path}}</path>",
"it": "Ha letto <path>{{observation.payload.extras.path}}</path>",
"pt": "Leu <path>{{observation.payload.extras.path}}</path>",
"es": "Leyó <path>{{observation.payload.extras.path}}</path>",
"tr": "<path>{{observation.payload.extras.path}}</path> okundu"
},
"OBSERVATION_MESSAGE$EDIT": {
"en": "Edited the contents of a file",
"zh-CN": "编辑",
"zh-TW": "編輯",
"ko-KR": "편집",
"ja": "編集",
"no": "Redigerte innholdet i en fil",
"ar": "تم تحرير محتويات ملف",
"de": "Hat den Inhalt einer Datei bearbeitet",
"fr": "A modifié le contenu d'un fichier",
"it": "Ha modificato il contenuto di un file",
"pt": "Editou o conteúdo de um arquivo",
"es": "Editó el contenido de un archivo",
"tr": "Dosya içeriği düzenlendi"
"en": "Edited <path>{{observation.payload.extras.path}}</path>",
"zh-CN": "编辑 <path>{{observation.payload.extras.path}}</path>",
"zh-TW": "編輯 <path>{{observation.payload.extras.path}}</path>",
"ko-KR": "편집 <path>{{observation.payload.extras.path}}</path>",
"ja": "編集 <path>{{observation.payload.extras.path}}</path>",
"no": "Redigerte <path>{{observation.payload.extras.path}}</path>",
"ar": "تم تحرير <path>{{observation.payload.extras.path}}</path>",
"de": "Hat <path>{{observation.payload.extras.path}}</path> bearbeitet",
"fr": "A modifié <path>{{observation.payload.extras.path}}</path>",
"it": "Ha modificato <path>{{observation.payload.extras.path}}</path>",
"pt": "Editou <path>{{observation.payload.extras.path}}</path>",
"es": "Editó <path>{{observation.payload.extras.path}}</path>",
"tr": "<path>{{observation.payload.extras.path}}</path> düzenlendi"
},
"OBSERVATION_MESSAGE$WRITE": {
"en": "Wrote to a file",
"zh-CN": "写入",
"zh-TW": "寫入",
"ko-KR": "쓰기",
"ja": "書き込み",
"no": "Skrev til en fil",
"ar": "تمت الكتابة إلى ملف",
"de": "Hat in eine Datei geschrieben",
"fr": "A écrit dans un fichier",
"it": "Ha scritto su un file",
"pt": "Escreveu em um arquivo",
"es": "Escribió en un archivo",
"tr": "Dosyaya yazıldı"
"en": "Wrote to <path>{{observation.payload.extras.path}}</path>",
"zh-CN": "写入 <path>{{observation.payload.extras.path}}</path>",
"zh-TW": "寫入 <path>{{observation.payload.extras.path}}</path>",
"ko-KR": "쓰기 <path>{{observation.payload.extras.path}}</path>",
"ja": "書き込み <path>{{observation.payload.extras.path}}</path>",
"no": "Skrev til <path>{{observation.payload.extras.path}}</path>",
"ar": "تمت الكتابة إلى <path>{{observation.payload.extras.path}}</path>",
"de": "Hat in <path>{{observation.payload.extras.path}}</path> geschrieben",
"fr": "A écrit dans <path>{{observation.payload.extras.path}}</path>",
"it": "Ha scritto su <path>{{observation.payload.extras.path}}</path>",
"pt": "Escreveu em <path>{{observation.payload.extras.path}}</path>",
"es": "Escribió en <path>{{observation.payload.extras.path}}</path>",
"tr": "<path>{{observation.payload.extras.path}}</path> dosyasına yazıldı"
},
"OBSERVATION_MESSAGE$BROWSE": {
"en": "Browsing completed",

View File

@@ -1,3 +1,7 @@
import { PayloadAction } from "@reduxjs/toolkit";
import { OpenHandsObservation } from "./types/core/observations";
import { OpenHandsAction } from "./types/core/actions";
export type Message = {
sender: "user" | "assistant";
content: string;
@@ -8,4 +12,6 @@ export type Message = {
pending?: boolean;
translationID?: string;
eventID?: number;
observation?: PayloadAction<OpenHandsObservation>;
action?: PayloadAction<OpenHandsAction>;
};

View File

@@ -0,0 +1,39 @@
import { delay, http, HttpResponse } from "msw";
export const FILE_VARIANTS_1 = ["file1.txt", "file2.txt", "file3.txt"];
export const FILE_VARIANTS_2 = [
"reboot_skynet.exe",
"target_list.txt",
"terminator_blueprint.txt",
];
export const FILE_SERVICE_HANDLERS = [
http.get(
"/api/conversations/:conversationId/list-files",
async ({ params }) => {
await delay();
const cid = params.conversationId?.toString();
if (!cid) return HttpResponse.json(null, { status: 400 });
return cid === "test-conversation-id-2"
? HttpResponse.json(FILE_VARIANTS_2)
: HttpResponse.json(FILE_VARIANTS_1);
},
),
http.get(
"/api/conversations/:conversationId/select-file",
async ({ request }) => {
await delay();
const url = new URL(request.url);
const file = url.searchParams.get("file")?.toString();
if (file) {
return HttpResponse.json({ code: `Content of ${file}` });
}
return HttpResponse.json(null, { status: 404 });
},
),
];

View File

@@ -4,35 +4,11 @@ import {
Conversation,
ResultSet,
} from "#/api/open-hands.types";
import { DEFAULT_SETTINGS } from "#/services/settings";
import { STRIPE_BILLING_HANDLERS } from "./billing-handlers";
import { ApiSettings, PostApiSettings } from "#/types/settings";
import { SETTINGS_HANDLERS } from "./settings-handlers";
import { FILE_SERVICE_HANDLERS } from "./file-service-handlers";
import { GitUser } from "#/types/git";
export const MOCK_DEFAULT_USER_SETTINGS: ApiSettings | PostApiSettings = {
llm_model: DEFAULT_SETTINGS.LLM_MODEL,
llm_base_url: DEFAULT_SETTINGS.LLM_BASE_URL,
llm_api_key: null,
llm_api_key_set: DEFAULT_SETTINGS.LLM_API_KEY_SET,
agent: DEFAULT_SETTINGS.AGENT,
language: DEFAULT_SETTINGS.LANGUAGE,
confirmation_mode: DEFAULT_SETTINGS.CONFIRMATION_MODE,
security_analyzer: DEFAULT_SETTINGS.SECURITY_ANALYZER,
remote_runtime_resource_factor:
DEFAULT_SETTINGS.REMOTE_RUNTIME_RESOURCE_FACTOR,
provider_tokens_set: DEFAULT_SETTINGS.PROVIDER_TOKENS_SET,
enable_default_condenser: DEFAULT_SETTINGS.ENABLE_DEFAULT_CONDENSER,
enable_sound_notifications: DEFAULT_SETTINGS.ENABLE_SOUND_NOTIFICATIONS,
user_consents_to_analytics: DEFAULT_SETTINGS.USER_CONSENTS_TO_ANALYTICS,
provider_tokens: DEFAULT_SETTINGS.PROVIDER_TOKENS,
};
const MOCK_USER_PREFERENCES: {
settings: ApiSettings | PostApiSettings | null;
} = {
settings: null,
};
const conversations: Conversation[] = [
{
conversation_id: "1",
@@ -91,52 +67,6 @@ const openHandsHandlers = [
HttpResponse.json(["mock-invariant"]),
),
http.get(
"http://localhost:3001/api/conversations/:conversationId/list-files",
async ({ params }) => {
await delay();
const cid = params.conversationId?.toString();
if (!cid) return HttpResponse.json([], { status: 404 });
let data = ["file1.txt", "file2.txt", "file3.txt"];
if (cid === "3") {
data = [
"reboot_skynet.exe",
"target_list.txt",
"terminator_blueprint.txt",
];
}
return HttpResponse.json(data);
},
),
http.post("http://localhost:3001/api/save-file", () =>
HttpResponse.json(null, { status: 200 }),
),
http.get("http://localhost:3001/api/select-file", async ({ request }) => {
await delay();
const token = request.headers
.get("Authorization")
?.replace("Bearer", "")
.trim();
if (!token) {
return HttpResponse.json([], { status: 401 });
}
const url = new URL(request.url);
const file = url.searchParams.get("file")?.toString();
if (file) {
return HttpResponse.json({ code: `Content of ${file}` });
}
return HttpResponse.json(null, { status: 404 });
}),
http.post("http://localhost:3001/api/submit-feedback", async () => {
await delay(1200);
@@ -149,6 +79,8 @@ const openHandsHandlers = [
export const handlers = [
...STRIPE_BILLING_HANDLERS,
...SETTINGS_HANDLERS,
...FILE_SERVICE_HANDLERS,
...openHandsHandlers,
http.get("/api/user/repositories", () =>
HttpResponse.json([
@@ -190,40 +122,6 @@ export const handlers = [
return HttpResponse.json(config);
}),
http.get("/api/settings", async () => {
await delay();
const { settings } = MOCK_USER_PREFERENCES;
if (!settings) return HttpResponse.json(null, { status: 404 });
if (Object.keys(settings.provider_tokens_set).length > 0)
settings.provider_tokens_set = { github: false, gitlab: false };
return HttpResponse.json(settings);
}),
http.post("/api/settings", async ({ request }) => {
const body = await request.json();
if (body) {
let newSettings: Partial<PostApiSettings> = {};
if (typeof body === "object") {
newSettings = { ...body };
}
const fullSettings = {
...MOCK_DEFAULT_USER_SETTINGS,
...MOCK_USER_PREFERENCES.settings,
...newSettings,
};
MOCK_USER_PREFERENCES.settings = fullSettings;
return HttpResponse.json(null, { status: 200 });
}
return HttpResponse.json(null, { status: 400 });
}),
http.post("/api/authenticate", async () =>
HttpResponse.json({ message: "Authenticated" }),
),
@@ -304,10 +202,4 @@ export const handlers = [
}),
http.post("/api/logout", () => HttpResponse.json(null, { status: 200 })),
http.post("/api/reset-settings", async () => {
await delay();
MOCK_USER_PREFERENCES.settings = { ...MOCK_DEFAULT_USER_SETTINGS };
return HttpResponse.json(null, { status: 200 });
}),
];

View File

@@ -0,0 +1,52 @@
import { delay, http, HttpResponse } from "msw";
import { UserSettings } from "#/api/settings-service/settings-service.types";
import { DEFAULT_SETTINGS } from "#/services/settings";
const MOCK_USER_PREFERENCES: {
settings: UserSettings | null;
} = {
settings: null,
};
export const SETTINGS_HANDLERS = [
http.get("/api/settings", async () => {
await delay();
const { settings } = MOCK_USER_PREFERENCES;
if (!settings) return HttpResponse.json(null, { status: 404 });
if (Object.keys(settings.provider_tokens_set).length > 0)
settings.github_token_is_set = true;
return HttpResponse.json(settings);
}),
http.post("/api/settings", async ({ request }) => {
const body = await request.json();
if (body) {
let newSettings: Partial<UserSettings> = {};
if (typeof body === "object") {
newSettings = { ...body };
}
const fullSettings: UserSettings = {
...DEFAULT_SETTINGS,
...MOCK_USER_PREFERENCES.settings,
...newSettings,
};
MOCK_USER_PREFERENCES.settings = fullSettings;
return HttpResponse.json(null, { status: 200 });
}
return HttpResponse.json(null, { status: 400 });
}),
http.post("/api/settings/reset", async () => {
await delay();
MOCK_USER_PREFERENCES.settings = { ...DEFAULT_SETTINGS };
return HttpResponse.json(null, { status: 200 });
}),
];

View File

@@ -6,17 +6,17 @@ import {
} from "@react-router/dev/routes";
export default [
layout("routes/_oh/route.tsx", [
index("routes/_oh._index/route.tsx"),
layout("routes/root-layout.tsx", [
index("routes/home.tsx"),
route("settings", "routes/settings.tsx", [
index("routes/account-settings.tsx"),
route("billing", "routes/billing.tsx"),
]),
route("conversations/:conversationId", "routes/_oh.app/route.tsx", [
index("routes/_oh.app._index/route.tsx"),
route("browser", "routes/_oh.app.browser.tsx"),
route("jupyter", "routes/_oh.app.jupyter.tsx"),
route("served", "routes/app.tsx"),
route("conversations/:conversationId", "routes/conversation.tsx", [
index("routes/editor-tab.tsx"),
route("browser", "routes/browser-tab.tsx"),
route("jupyter", "routes/jupyter-tab.tsx"),
route("served", "routes/served-tab.tsx"),
]),
]),
] satisfies RouteConfig;

View File

@@ -27,6 +27,7 @@ import {
displayErrorToast,
displaySuccessToast,
} from "#/utils/custom-toast-handlers";
import { ServerUserSettings } from "#/api/settings-service/settings-service.types";
import { ProviderOptions } from "#/types/settings";
import { useAuth } from "#/context/auth-context";
@@ -67,9 +68,10 @@ function AccountSettings() {
if (isSuccess) {
return (
isCustomModel(resources.models, settings.LLM_MODEL) ||
isCustomModel(resources.models, settings.llm_model) ||
hasAdvancedSettingsSet({
...settings,
provider_tokens_set: settings.provider_tokens_set || {},
})
);
}
@@ -78,12 +80,12 @@ function AccountSettings() {
};
const hasAppSlug = !!config?.APP_SLUG;
const isLLMKeySet = settings?.llm_api_key_set === "**********";
const isAnalyticsEnabled = settings?.user_consents_to_analytics;
const isGitHubTokenSet =
providerTokensSet.includes(ProviderOptions.github) || false;
const isGitLabTokenSet =
providerTokensSet.includes(ProviderOptions.gitlab) || false;
const isLLMKeySet = settings?.LLM_API_KEY_SET;
const isAnalyticsEnabled = settings?.USER_CONSENTS_TO_ANALYTICS;
const isAdvancedSettingsSet = determineWhetherToToggleAdvancedSettings();
const modelsAndProviders = organizeModelsAndProviders(
@@ -94,7 +96,7 @@ function AccountSettings() {
"basic" | "advanced"
>(isAdvancedSettingsSet ? "advanced" : "basic");
const [confirmationModeIsEnabled, setConfirmationModeIsEnabled] =
React.useState(!!settings?.SECURITY_ANALYZER);
React.useState(!!settings?.security_analyzer);
const [resetSettingsModalIsOpen, setResetSettingsModalIsOpen] =
React.useState(false);
@@ -117,6 +119,10 @@ function AccountSettings() {
const remoteRuntimeResourceFactor = REMOTE_RUNTIME_OPTIONS.find(
({ label }) => label === rawRemoteRuntimeResourceFactor,
)?.key;
const remoteRuntimeResourceFactorValue = parseInt(
remoteRuntimeResourceFactor || "",
10,
);
const userConsentsToAnalytics =
formData.get("enable-analytics-switch")?.toString() === "on";
@@ -142,7 +148,7 @@ function AccountSettings() {
: llmBaseUrl;
const finalLlmApiKey = shouldHandleSpecialSaasCase ? undefined : llmApiKey;
const newSettings = {
const newSettings: Partial<ServerUserSettings> = {
provider_tokens:
githubToken || gitlabToken
? {
@@ -150,21 +156,20 @@ function AccountSettings() {
gitlab: gitlabToken || "",
}
: undefined,
LANGUAGE: languageValue,
language: languageValue,
user_consents_to_analytics: userConsentsToAnalytics,
ENABLE_DEFAULT_CONDENSER: enableMemoryCondenser,
ENABLE_SOUND_NOTIFICATIONS: enableSoundNotifications,
LLM_MODEL: finalLlmModel,
LLM_BASE_URL: finalLlmBaseUrl,
llm_api_key: finalLlmApiKey,
AGENT: formData.get("agent-input")?.toString(),
SECURITY_ANALYZER:
enable_default_condenser: enableMemoryCondenser,
enable_sound_notifications: enableSoundNotifications,
llm_model: finalLlmModel,
llm_base_url: finalLlmBaseUrl,
llm_api_key_set: finalLlmApiKey,
agent: formData.get("agent-input")?.toString(),
security_analyzer:
formData.get("security-analyzer-input")?.toString() || "",
REMOTE_RUNTIME_RESOURCE_FACTOR:
remoteRuntimeResourceFactor !== null
? Number(remoteRuntimeResourceFactor)
: DEFAULT_SETTINGS.REMOTE_RUNTIME_RESOURCE_FACTOR,
CONFIRMATION_MODE: confirmationModeIsEnabled,
remote_runtime_resource_factor:
remoteRuntimeResourceFactorValue ||
DEFAULT_SETTINGS.remote_runtime_resource_factor,
confirmation_mode: confirmationModeIsEnabled,
};
saveSettings(newSettings, {
@@ -205,7 +210,7 @@ function AccountSettings() {
setLlmConfigMode(isToggled ? "advanced" : "basic");
if (!isToggled) {
// reset advanced state
setConfirmationModeIsEnabled(!!settings?.SECURITY_ANALYZER);
setConfirmationModeIsEnabled(!!settings?.security_analyzer);
}
};
@@ -249,7 +254,7 @@ function AccountSettings() {
{llmConfigMode === "basic" && !shouldHandleSpecialSaasCase && (
<ModelSelector
models={modelsAndProviders}
currentModel={settings.LLM_MODEL}
currentModel={settings.llm_model}
/>
)}
@@ -258,7 +263,7 @@ function AccountSettings() {
testId="llm-custom-model-input"
name="llm-custom-model-input"
label={t(I18nKey.SETTINGS$CUSTOM_MODEL)}
defaultValue={settings.LLM_MODEL}
defaultValue={settings.llm_model}
placeholder="anthropic/claude-3-5-sonnet-20241022"
type="text"
className="w-[680px]"
@@ -269,7 +274,7 @@ function AccountSettings() {
testId="base-url-input"
name="base-url-input"
label={t(I18nKey.SETTINGS$BASE_URL)}
defaultValue={settings.LLM_BASE_URL}
defaultValue={settings.llm_base_url}
placeholder="https://api.openai.com"
type="text"
className="w-[680px]"
@@ -310,7 +315,7 @@ function AccountSettings() {
label: agent,
})) || []
}
defaultSelectedKey={settings.AGENT}
defaultSelectedKey={settings.agent}
isClearable={false}
/>
)}
@@ -329,7 +334,7 @@ function AccountSettings() {
</>
}
items={REMOTE_RUNTIME_OPTIONS}
defaultSelectedKey={settings.REMOTE_RUNTIME_RESOURCE_FACTOR?.toString()}
defaultSelectedKey={settings.remote_runtime_resource_factor?.toString()}
isDisabled
isClearable={false}
/>
@@ -339,7 +344,7 @@ function AccountSettings() {
<SettingsSwitch
testId="enable-confirmation-mode-switch"
onToggle={setConfirmationModeIsEnabled}
defaultIsToggled={!!settings.CONFIRMATION_MODE}
defaultIsToggled={!!settings.confirmation_mode}
isBeta
>
{t(I18nKey.SETTINGS$CONFIRMATION_MODE)}
@@ -350,7 +355,7 @@ function AccountSettings() {
<SettingsSwitch
testId="enable-memory-condenser-switch"
name="enable-memory-condenser-switch"
defaultIsToggled={!!settings.ENABLE_DEFAULT_CONDENSER}
defaultIsToggled={!!settings.enable_default_condenser}
>
{t(I18nKey.SETTINGS$ENABLE_MEMORY_CONDENSATION)}
</SettingsSwitch>
@@ -368,7 +373,7 @@ function AccountSettings() {
label: analyzer,
})) || []
}
defaultSelectedKey={settings.SECURITY_ANALYZER}
defaultSelectedKey={settings.security_analyzer}
isClearable
showOptionalTag
/>
@@ -501,7 +506,7 @@ function AccountSettings() {
key: language.value,
label: language.label,
}))}
defaultSelectedKey={settings.LANGUAGE}
defaultSelectedKey={settings.language}
isClearable={false}
/>
@@ -516,7 +521,7 @@ function AccountSettings() {
<SettingsSwitch
testId="enable-sound-notifications-switch"
name="enable-sound-notifications-switch"
defaultIsToggled={!!settings.ENABLE_SOUND_NOTIFICATIONS}
defaultIsToggled={!!settings.enable_sound_notifications}
>
{t(I18nKey.SETTINGS$SOUND_NOTIFICATIONS)}
</SettingsSwitch>

View File

@@ -18,9 +18,9 @@ import GlobeIcon from "#/icons/globe.svg?react";
import ListIcon from "#/icons/list-type-number.svg?react";
import { clearJupyter } from "#/state/jupyter-slice";
import { FilesProvider } from "#/context/files";
import { ChatInterface } from "../../components/features/chat/chat-interface";
import { ChatInterface } from "../components/features/chat/chat-interface";
import { WsClientProvider } from "#/context/ws-client-provider";
import { EventHandler } from "./event-handler";
import { EventHandler } from "../wrapper/event-handler";
import { useConversationConfig } from "#/hooks/query/use-conversation-config";
import { Container } from "#/components/layout/container";
import {
@@ -198,13 +198,13 @@ function AppContent() {
<Controls
setSecurityOpen={onSecurityModalOpen}
showSecurityLock={!!settings?.SECURITY_ANALYZER}
showSecurityLock={!!settings?.security_analyzer}
/>
{settings && (
<Security
isOpen={securityModalIsOpen}
onOpenChange={onSecurityModalOpenChange}
securityAnalyzer={settings.SECURITY_ANALYZER}
securityAnalyzer={settings.security_analyzer}
/>
)}
</div>

View File

@@ -5,8 +5,8 @@ import { setReplayJson } from "#/state/initial-query-slice";
import { useGitUser } from "#/hooks/query/use-git-user";
import { useGitHubAuthUrl } from "#/hooks/use-github-auth-url";
import { useConfig } from "#/hooks/query/use-config";
import { ReplaySuggestionBox } from "#/components/features/suggestions/replay-suggestion-box";
import { GitRepositoriesSuggestionBox } from "#/components/features/git/git-repositories-suggestion-box";
import { ReplaySuggestionBox } from "../../components/features/suggestions/replay-suggestion-box";
import { CodeNotInGitLink } from "#/components/features/git/code-not-in-github-link";
import { HeroHeading } from "#/components/shared/hero-heading";
import { TaskForm } from "#/components/shared/task-form";

View File

@@ -60,8 +60,8 @@ export default function MainApp() {
const navigate = useNavigate();
const { pathname } = useLocation();
const [searchParams] = useSearchParams();
const { data: settings, isFetched: settingsIsFetched } = useSettings();
const { providersAreSet } = useAuth();
const { data: settings } = useSettings();
const { error, isFetching } = useBalance();
const { migrateUserConsent } = useMigrateUserConsent();
const { t } = useTranslation();
@@ -81,17 +81,17 @@ export default function MainApp() {
const [consentFormIsOpen, setConsentFormIsOpen] = React.useState(false);
React.useEffect(() => {
if (settings?.LANGUAGE) {
i18n.changeLanguage(settings.LANGUAGE);
if (settingsIsFetched && settings?.language) {
i18n.changeLanguage(settings.language);
}
}, [settings?.LANGUAGE]);
}, [settingsIsFetched, settings?.language]);
React.useEffect(() => {
const consentFormModalIsOpen =
settings?.USER_CONSENTS_TO_ANALYTICS === null;
settingsIsFetched && settings?.user_consents_to_analytics === null;
setConsentFormIsOpen(consentFormModalIsOpen);
}, [settings]);
}, [settingsIsFetched, settings?.user_consents_to_analytics]);
React.useEffect(() => {
// Migrate user consent to the server if it was previously stored in localStorage
@@ -148,7 +148,7 @@ export default function MainApp() {
{config.data?.FEATURE_FLAGS.ENABLE_BILLING &&
config.data?.APP_MODE === "saas" &&
settings?.IS_NEW_USER && <SetupPaymentModal />}
settings?.is_new_user && <SetupPaymentModal />}
</div>
);
}

View File

@@ -1,28 +1,28 @@
import { Settings } from "#/types/settings";
import { ClientUserSettings } from "#/api/settings-service/settings-service.types";
export const LATEST_SETTINGS_VERSION = 5;
export const DEFAULT_SETTINGS: Settings = {
LLM_MODEL: "anthropic/claude-3-5-sonnet-20241022",
LLM_BASE_URL: "",
AGENT: "CodeActAgent",
LANGUAGE: "en",
LLM_API_KEY_SET: false,
CONFIRMATION_MODE: false,
SECURITY_ANALYZER: "",
REMOTE_RUNTIME_RESOURCE_FACTOR: 1,
PROVIDER_TOKENS_SET: { github: false, gitlab: false },
ENABLE_DEFAULT_CONDENSER: true,
ENABLE_SOUND_NOTIFICATIONS: false,
USER_CONSENTS_TO_ANALYTICS: false,
PROVIDER_TOKENS: {
github: "",
gitlab: "",
export const DEFAULT_SETTINGS: ClientUserSettings = {
llm_model: "anthropic/claude-3-5-sonnet-20241022",
llm_base_url: "",
agent: "CodeActAgent",
language: "en",
llm_api_key_set: null,
confirmation_mode: false,
security_analyzer: "",
remote_runtime_resource_factor: 1,
github_token_is_set: false,
enable_default_condenser: true,
enable_sound_notifications: false,
user_consents_to_analytics: false,
provider_tokens_set: {
github: false,
gitlab: false,
},
IS_NEW_USER: true,
is_new_user: true,
};
/**
* Get the default settings
*/
export const getDefaultSettings = (): Settings => DEFAULT_SETTINGS;
export const getDefaultSettings = () => DEFAULT_SETTINGS;

View File

@@ -2,14 +2,14 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import type { Message } from "#/message";
import { ActionSecurityRisk } from "#/state/security-analyzer-slice";
import {
OpenHandsObservation,
CommandObservation,
IPythonObservation,
RecallObservation,
} from "#/types/core/observations";
import { OpenHandsAction } from "#/types/core/actions";
import { OpenHandsEventType } from "#/types/core/base";
import {
CommandObservation,
IPythonObservation,
OpenHandsObservation,
RecallObservation,
} from "#/types/core/observations";
type SliceState = { messages: Message[] };
@@ -135,6 +135,7 @@ export const chatSlice = createSlice({
content: text,
imageUrls: [],
timestamp: new Date().toISOString(),
action,
};
state.messages.push(message);
@@ -224,6 +225,7 @@ export const chatSlice = createSlice({
return;
}
causeMessage.translationID = translationID;
causeMessage.observation = observation;
// Set success property based on observation type
if (observationID === "run") {
const commandObs = observation.payload as CommandObservation;
@@ -253,9 +255,7 @@ export const chatSlice = createSlice({
if (content.length > MAX_CONTENT_LENGTH) {
content = `${content.slice(0, MAX_CONTENT_LENGTH)}...`;
}
content = `${
causeMessage.content
}\n\nOutput:\n\`\`\`\n${content.trim() || "[Command finished execution with no output]"}\n\`\`\``;
content = `${causeMessage.content}\n\nOutput:\n\`\`\`\n${content.trim() || "[Command finished execution with no output]"}\n\`\`\``;
causeMessage.content = content; // Observation content includes the action
} else if (observationID === "read") {
causeMessage.content = `\`\`\`\n${observation.payload.content}\n\`\`\``; // Content is already truncated by the ACI

View File

@@ -1,38 +0,0 @@
import { Provider } from "#/types/settings";
interface GitHubErrorReponse {
message: string;
documentation_url: string;
status: number;
}
interface GitUser {
id: number;
login: string;
avatar_url: string;
company: string | null;
name: string | null;
email: string | null;
}
interface GitRepository {
id: number;
full_name: string;
git_provider: Provider;
stargazers_count?: number;
link_header?: string;
}
interface GitHubCommit {
html_url: string;
sha: string;
commit: {
author: {
date: string; // ISO 8601
};
};
}
interface GithubAppInstallation {
installations: { id: number }[];
}

24
frontend/src/types/git.ts Normal file
View File

@@ -0,0 +1,24 @@
import { GitProvider } from "#/api/settings-service/settings-service.types";
export interface GitHubErrorReponse {
message: string;
documentation_url: string;
status: number;
}
export interface GitUser {
id: number;
login: string;
avatar_url: string;
company: string | null;
name: string | null;
email: string | null;
}
export interface GitRepository {
id: number;
full_name: string;
git_provider: GitProvider;
stargazers_count?: number;
link_header?: string;
}

View File

@@ -1,10 +1,10 @@
import { UserSettings } from "#/api/settings-service/settings-service.types";
import { DEFAULT_SETTINGS } from "#/services/settings";
import { Settings } from "#/types/settings";
export const hasAdvancedSettingsSet = (settings: Partial<Settings>): boolean =>
!!settings.LLM_BASE_URL ||
settings.AGENT !== DEFAULT_SETTINGS.AGENT ||
settings.REMOTE_RUNTIME_RESOURCE_FACTOR !==
DEFAULT_SETTINGS.REMOTE_RUNTIME_RESOURCE_FACTOR ||
settings.CONFIRMATION_MODE ||
!!settings.SECURITY_ANALYZER;
export const hasAdvancedSettingsSet = (settings: UserSettings): boolean =>
!!settings.llm_base_url ||
settings.agent !== DEFAULT_SETTINGS.agent ||
settings.remote_runtime_resource_factor !==
DEFAULT_SETTINGS.remote_runtime_resource_factor ||
settings.confirmation_mode ||
!!settings.security_analyzer;

View File

@@ -1,4 +1,4 @@
import { Settings } from "#/types/settings";
import { UserSettings } from "#/api/settings-service/settings-service.types";
const extractBasicFormData = (formData: FormData) => {
const provider = formData.get("llm-provider-input")?.toString();
@@ -47,9 +47,7 @@ const extractAdvancedFormData = (formData: FormData) => {
};
};
export const extractSettings = (
formData: FormData,
): Partial<Settings> & { llm_api_key?: string | null } => {
export const extractSettings = (formData: FormData): Partial<UserSettings> => {
const { LLM_MODEL, LLM_API_KEY, AGENT, LANGUAGE } =
extractBasicFormData(formData);
@@ -74,15 +72,14 @@ export const extractSettings = (
}
return {
LLM_MODEL: CUSTOM_LLM_MODEL || LLM_MODEL,
LLM_API_KEY_SET: !!LLM_API_KEY,
AGENT,
LANGUAGE,
LLM_BASE_URL,
CONFIRMATION_MODE,
SECURITY_ANALYZER,
ENABLE_DEFAULT_CONDENSER,
PROVIDER_TOKENS: providerTokens,
llm_api_key: LLM_API_KEY,
llm_model: CUSTOM_LLM_MODEL || LLM_MODEL,
llm_api_key_set: LLM_API_KEY,
agent: AGENT,
language: LANGUAGE,
llm_base_url: LLM_BASE_URL,
confirmation_mode: CONFIRMATION_MODE,
security_analyzer: SECURITY_ANALYZER,
enable_default_condenser: ENABLE_DEFAULT_CONDENSER,
provider_tokens_set: providerTokens,
};
};

View File

@@ -1,6 +1,6 @@
import React from "react";
import { useHandleWSEvents } from "./hooks/use-handle-ws-events";
import { useHandleRuntimeActive } from "./hooks/use-handle-runtime-active";
import { useHandleWSEvents } from "../hooks/use-handle-ws-events";
import { useHandleRuntimeActive } from "../hooks/use-handle-runtime-active";
export function EventHandler({ children }: React.PropsWithChildren) {
useHandleWSEvents();

View File

@@ -3,7 +3,9 @@ import logging
import sys
from uuid import uuid4
from termcolor import colored
from prompt_toolkit import PromptSession, print_formatted_text
from prompt_toolkit.formatted_text import FormattedText
from prompt_toolkit.key_binding import KeyBindings
import openhands.agenthub # noqa F401 (we import this to get the agents registered)
from openhands.core.config import (
@@ -36,24 +38,66 @@ from openhands.events.observation import (
CmdOutputObservation,
FileEditObservation,
)
from openhands.io import read_input, read_task
from openhands.io import read_task
prompt_session = PromptSession()
def display_message(message: str):
print(colored('🤖 ' + message + '\n', 'yellow'))
print_formatted_text(
FormattedText(
[
('ansiyellow', '🤖 '),
('ansiyellow', message),
('', '\n'),
]
)
)
def display_command(command: str):
print(' ' + colored(command + '\n', 'green'))
print_formatted_text(
FormattedText(
[
('', ' '),
('ansigreen', command),
('', '\n'),
]
)
)
def display_confirmation(confirmation_state: ActionConfirmationStatus):
if confirmation_state == ActionConfirmationStatus.CONFIRMED:
print(colored('' + confirmation_state + '\n', 'green'))
print_formatted_text(
FormattedText(
[
('ansigreen', ''),
('ansigreen', str(confirmation_state)),
('', '\n'),
]
)
)
elif confirmation_state == ActionConfirmationStatus.REJECTED:
print(colored('' + confirmation_state + '\n', 'red'))
print_formatted_text(
FormattedText(
[
('ansired', ''),
('ansired', str(confirmation_state)),
('', '\n'),
]
)
)
else:
print(colored('' + confirmation_state + '\n', 'yellow'))
print_formatted_text(
FormattedText(
[
('ansiyellow', ''),
('ansiyellow', str(confirmation_state)),
('', '\n'),
]
)
)
def display_command_output(output: str):
@@ -62,12 +106,19 @@ def display_command_output(output: str):
if line.startswith('[Python Interpreter') or line.startswith('openhands@'):
# TODO: clean this up once we clean up terminal output
continue
print(colored(line, 'blue'))
print('\n')
print_formatted_text(FormattedText([('ansiblue', line)]))
print_formatted_text('')
def display_file_edit(event: FileEditAction | FileEditObservation):
print(colored(str(event), 'green'))
print_formatted_text(
FormattedText(
[
('ansigreen', str(event)),
('', '\n'),
]
)
)
def display_event(event: Event, config: AppConfig):
@@ -89,6 +140,41 @@ def display_event(event: Event, config: AppConfig):
display_confirmation(event.confirmation_state)
async def read_prompt_input(multiline=False):
try:
if multiline:
kb = KeyBindings()
@kb.add('c-d')
def _(event):
event.current_buffer.validate_and_handle()
message = await prompt_session.prompt_async(
'Enter your message and press Ctrl+D to finish:\n',
multiline=True,
key_bindings=kb,
)
else:
message = await prompt_session.prompt_async(
'>> ',
)
return message
except KeyboardInterrupt:
return 'exit'
except EOFError:
return 'exit'
async def read_confirmation_input():
try:
confirmation = await prompt_session.prompt_async(
'Confirm action (possible security risk)? (y/n) >> ',
)
return confirmation.lower() == 'y'
except (KeyboardInterrupt, EOFError):
return False
async def main(loop: asyncio.AbstractEventLoop):
"""Runs the agent in CLI mode."""
@@ -122,10 +208,7 @@ async def main(loop: asyncio.AbstractEventLoop):
event_stream = runtime.event_stream
async def prompt_for_next_task():
# Run input() in a thread pool to avoid blocking the event loop
next_message = await loop.run_in_executor(
None, read_input, config.cli_multiline_input
)
next_message = await read_prompt_input(config.cli_multiline_input)
if not next_message.strip():
await prompt_for_next_task()
if next_message == 'exit':
@@ -136,12 +219,6 @@ async def main(loop: asyncio.AbstractEventLoop):
action = MessageAction(content=next_message)
event_stream.add_event(action, EventSource.USER)
async def prompt_for_user_confirmation():
user_confirmation = await loop.run_in_executor(
None, lambda: input('Confirm action (possible security risk)? (y/n) >> ')
)
return user_confirmation.lower() == 'y'
async def on_event_async(event: Event):
display_event(event, config)
if isinstance(event, AgentStateChangedObservation):
@@ -151,7 +228,7 @@ async def main(loop: asyncio.AbstractEventLoop):
]:
await prompt_for_next_task()
if event.agent_state == AgentState.AWAITING_USER_CONFIRMATION:
user_confirmed = await prompt_for_user_confirmation()
user_confirmed = await read_confirmation_input()
if user_confirmed:
event_stream.add_event(
ChangeAgentStateAction(AgentState.USER_CONFIRMED),

View File

@@ -164,8 +164,8 @@ class LLMConfig(BaseModel):
if self.openrouter_app_name:
os.environ['OR_APP_NAME'] = self.openrouter_app_name
# Assign an API version for Azure models
# While it doesn't seem required, the format supported by the API without version seems old and will likely break.
# Azure issue: https://github.com/All-Hands-AI/OpenHands/issues/6777
# Set an API version by default for Azure models
# Required for newer models.
# Azure issue: https://github.com/All-Hands-AI/OpenHands/issues/7755
if self.model.startswith('azure') and self.api_version is None:
self.api_version = '2024-08-01-preview'
self.api_version = '2024-12-01-preview'

View File

@@ -100,7 +100,6 @@ class GitHubService(GitService):
email=response.get('email'),
)
async def _fetch_paginated_repos(
self, url: str, params: dict, max_repos: int, extract_key: str | None = None
) -> list[dict]:
@@ -204,7 +203,7 @@ class GitHubService(GitService):
}
response, _ = await self._fetch_data(url, params)
repos = response.get('items', [])
repo_items = response.get('items', [])
repos = [
Repository(
@@ -213,7 +212,7 @@ class GitHubService(GitService):
stargazers_count=repo.get('stargazers_count'),
git_provider=ProviderType.GITHUB,
)
for repo in repos
for repo in repo_items
]
return repos
@@ -238,7 +237,7 @@ class GitHubService(GitService):
f"GraphQL query error: {json.dumps(result['errors'])}"
)
return result
return dict(result)
except httpx.HTTPStatusError as e:
if e.response.status_code == 401:

View File

@@ -1,6 +1,5 @@
import os
from typing import Any
from urllib.parse import quote_plus
import httpx
from pydantic import SecretStr

View File

@@ -74,7 +74,7 @@ class SecretStore(BaseModel):
@field_serializer('provider_tokens')
def provider_tokens_serializer(
self, provider_tokens: PROVIDER_TOKEN_TYPE, info: SerializationInfo
):
) -> dict[str, dict[str, str | Any]]:
tokens = {}
expose_secrets = info.context and info.context.get('expose_secrets', False)
@@ -100,12 +100,12 @@ class SecretStore(BaseModel):
@classmethod
def convert_dict_to_mappingproxy(
cls, data: dict[str, dict[str, dict[str, str]]] | PROVIDER_TOKEN_TYPE
) -> dict[str, MappingProxyType]:
) -> dict[str, MappingProxyType[Any, Any]]:
"""Custom deserializer to convert dictionary into MappingProxyType"""
if not isinstance(data, dict):
raise ValueError('SecretStore must be initialized with a dictionary')
new_data = {}
new_data: dict[str, MappingProxyType[Any, Any]] = {}
if 'provider_tokens' in data:
tokens = data['provider_tokens']
@@ -210,7 +210,7 @@ class ProviderHandler:
per_page: int,
sort: str,
order: str,
):
) -> list[Repository]:
all_repos: list[Repository] = []
for provider in self.provider_tokens:
try:
@@ -228,7 +228,7 @@ class ProviderHandler:
self,
event_stream: EventStream,
env_vars: dict[ProviderType, SecretStr] | None = None,
):
) -> None:
"""
This ensures that the latest provider tokens are masked from the event stream
It is called when the provider tokens are first initialized in the runtime or when tokens are re-exported with the latest working ones

View File

@@ -349,6 +349,7 @@ def main() -> None:
model=my_args.llm_model or os.environ['LLM_MODEL'],
api_key=SecretStr(api_key) if api_key else None,
base_url=my_args.llm_base_url or os.environ.get('LLM_BASE_URL', None),
api_version=os.environ.get('LLM_API_VERSION', None),
)
repo_instruction = None

View File

@@ -656,12 +656,21 @@ def main() -> None:
raise ValueError('Token is invalid.')
api_key = my_args.llm_api_key or os.environ['LLM_API_KEY']
model = my_args.llm_model or os.environ['LLM_MODEL']
base_url = my_args.llm_base_url or os.environ.get('LLM_BASE_URL', None)
api_version = os.environ.get('LLM_API_VERSION', None)
# Create LLMConfig instance
llm_config = LLMConfig(
model=my_args.llm_model or os.environ['LLM_MODEL'],
model=model,
api_key=SecretStr(api_key) if api_key else None,
base_url=my_args.llm_base_url or os.environ.get('LLM_BASE_URL', None),
base_url=base_url,
)
# Only set api_version if it was explicitly provided, otherwise let LLMConfig handle it
if api_version is not None:
llm_config.api_version = api_version
repo_instruction = None
if my_args.repo_instruction_file:
with open(my_args.repo_instruction_file, 'r') as f:

View File

@@ -252,8 +252,9 @@ class DockerRuntime(ActionExecutionClient):
# Combine environment variables
environment = {
'port': str(self._container_port),
'PYTHONUNBUFFERED': 1,
'PYTHONUNBUFFERED': '1',
'VSCODE_PORT': str(self._vscode_port),
'PIP_BREAK_SYSTEM_PACKAGES': '1',
}
if self.config.debug or DEBUG:
environment['DEBUG'] = 'true'
@@ -293,6 +294,8 @@ class DockerRuntime(ActionExecutionClient):
self.container = self.docker_client.containers.run(
self.runtime_container_image,
command=command,
# Override the default 'bash' entrypoint because the command is a binary.
entrypoint=[],
network_mode=network_mode,
ports=port_mapping,
working_dir='/openhands/code/', # do not change this!

View File

@@ -17,16 +17,26 @@ class LogStreamer:
logFn: Callable,
):
self.log = logFn
self.log_generator = container.logs(stream=True, follow=True)
# Initialize all attributes before starting the thread on this instance
self.stdout_thread = None
self.log_generator = None
self._stop_event = threading.Event()
# Start the stdout streaming thread
self.stdout_thread = threading.Thread(target=self._stream_logs)
self.stdout_thread.daemon = True
self.stdout_thread.start()
try:
self.log_generator = container.logs(stream=True, follow=True)
# Start the stdout streaming thread
self.stdout_thread = threading.Thread(target=self._stream_logs)
self.stdout_thread.daemon = True
self.stdout_thread.start()
except Exception as e:
self.log('error', f'Failed to initialize log streaming: {e}')
def _stream_logs(self) -> None:
"""Stream logs from the Docker container to stdout."""
if not self.log_generator:
self.log('error', 'Log generator not initialized')
return
try:
for log_line in self.log_generator:
if self._stop_event.is_set():
@@ -38,7 +48,11 @@ class LogStreamer:
self.log('error', f'Error streaming docker logs to stdout: {e}')
def __del__(self) -> None:
if self.stdout_thread and self.stdout_thread.is_alive():
if (
hasattr(self, 'stdout_thread')
and self.stdout_thread
and self.stdout_thread.is_alive()
):
self.close(timeout=5)
def close(self, timeout: float = 5.0) -> None:
@@ -47,5 +61,5 @@ class LogStreamer:
if self.stdout_thread and self.stdout_thread.is_alive():
self.stdout_thread.join(timeout)
# Close the log generator to release the file descriptor
if hasattr(self.log_generator, 'close'):
if self.log_generator is not None:
self.log_generator.close()

View File

@@ -14,24 +14,34 @@ ENV POETRY_VIRTUALENVS_PATH=/openhands/poetry \
# Install base system dependencies
RUN apt-get update && \
apt-get upgrade -y && \
apt-get install -y --no-install-recommends \
wget curl sudo apt-utils git jq tmux \
wget curl ca-certificates sudo apt-utils git jq tmux build-essential \
{%- if 'ubuntu' in base_image and (base_image.endswith(':latest') or base_image.endswith(':24.04')) -%}
libgl1 \
{%- else %}
libgl1-mesa-glx \
{% endif -%}
libasound2-plugins libatomic1 && \
# Remove packages with CVEs and no updates yet, if present
(apt-get remove -y libaom3 || true) && \
(apt-get remove -y libjxl0.7 || true) && \
(apt-get remove -y libopenexr-3-1-30 || true) && \
{%- if 'ubuntu' in base_image -%}
curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && \
TZ=Etc/UTC DEBIAN_FRONTEND=noninteractive \
apt-get install -y --no-install-recommends nodejs python3.12 python-is-python3 python3-pip python3.12-venv && \
corepack enable yarn && \
{% endif -%}
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# Remove UID 1000 if it's called pn--this fixes the nikolaik image for ubuntu users
RUN if getent passwd 1000 | grep -q pn; then userdel pn; fi
{% if 'ubuntu' in base_image %}
RUN ln -s "$(dirname $(which node))/corepack" /usr/local/bin/corepack && \
npm install -g corepack && corepack enable yarn && \
curl -fsSL --compressed https://install.python-poetry.org | python - && \
curl -LsSf https://astral.sh/uv/install.sh | sh
{% endif %}
# Remove UID 1000 named pn or ubuntu, so the 'openhands' user can be created from ubuntu hosts
RUN (if getent passwd 1000 | grep -q pn; then userdel pn; fi) && \
(if getent passwd 1000 | grep -q ubuntu; then userdel ubuntu; fi)
# Create necessary directories
RUN mkdir -p /openhands && \
@@ -71,6 +81,8 @@ RUN if [ -z "${RELEASE_TAG}" ]; then \
cp ${OPENVSCODE_SERVER_ROOT}/bin/remote-cli/openvscode-server ${OPENVSCODE_SERVER_ROOT}/bin/remote-cli/code && \
rm -f ${RELEASE_TAG}-linux-${arch}.tar.gz
{% endmacro %}
{% macro install_vscode_extensions() %}
@@ -80,6 +92,9 @@ RUN mkdir -p ${OPENVSCODE_SERVER_ROOT}/extensions/openhands-hello-world && \
RUN mkdir -p ${OPENVSCODE_SERVER_ROOT}/extensions/openhands-memory-monitor && \
cp -r /openhands/code/openhands/runtime/utils/vscode-extensions/memory-monitor/* ${OPENVSCODE_SERVER_ROOT}/extensions/openhands-memory-monitor/
# Some extension dirs are removed because they trigger false positives in vulnerability scans.
RUN rm -rf ${OPENVSCODE_SERVER_ROOT}/extensions/{handlebars,pug,json,diff,grunt,ini,npm}
{% endmacro %}
{% macro install_dependencies() %}

View File

@@ -13,7 +13,7 @@ from openhands.events.stream import EventStream, EventStreamSubscriber
class SecurityAnalyzer:
"""Security analyzer that receives all events and analyzes agent actions for security risks."""
def __init__(self, event_stream: EventStream):
def __init__(self, event_stream: EventStream) -> None:
"""Initializes a new instance of the SecurityAnalyzer class.
Args:
@@ -36,6 +36,7 @@ class SecurityAnalyzer:
return
try:
# Set the security_risk attribute on the event
event.security_risk = await self.security_risk(event) # type: ignore [attr-defined]
await self.act(event)
except Exception as e:

View File

@@ -1,7 +1,7 @@
import ast
import re
import uuid
from typing import Any
from typing import Any, cast
import docker
from fastapi import HTTPException, Request
@@ -32,12 +32,12 @@ class InvariantAnalyzer(SecurityAnalyzer):
"""Security analyzer based on Invariant."""
trace: list[TraceElement]
input: list[dict]
input: list[dict[str, Any]]
container_name: str = 'openhands-invariant-server'
image_name: str = 'ghcr.io/invariantlabs-ai/server:openhands'
api_host: str = 'http://localhost'
timeout: int = 180
settings: dict = {}
settings: dict[str, Any] = {}
check_browsing_alignment: bool = False
guardrail_llm: LLM | None = None
@@ -47,7 +47,7 @@ class InvariantAnalyzer(SecurityAnalyzer):
event_stream: EventStream,
policy: str | None = None,
sid: str | None = None,
):
) -> None:
"""Initializes a new instance of the InvariantAnalzyer class."""
super().__init__(event_stream)
self.trace = []
@@ -108,14 +108,16 @@ class InvariantAnalyzer(SecurityAnalyzer):
policy = ''
self.monitor = self.client.Monitor.from_string(policy)
async def close(self):
async def close(self) -> None:
self.container.stop()
async def log_event(self, event: Event) -> None:
if isinstance(event, Observation):
element = parse_element(self.trace, event)
self.trace.extend(element)
self.input.extend([e.model_dump(exclude_none=True) for e in element]) # type: ignore [call-overload]
self.input.extend(
[cast(dict[str, Any], e.model_dump(exclude_none=True)) for e in element]
)
else:
logger.debug('Invariant skipping element: event')
@@ -126,7 +128,7 @@ class InvariantAnalyzer(SecurityAnalyzer):
'low': ActionSecurityRisk.LOW,
}
regex = r'(?<=risk=)\w+'
risks = []
risks: list[ActionSecurityRisk] = []
for result in results:
m = re.search(regex, result)
if m and m.group() in mapping:
@@ -148,7 +150,7 @@ class InvariantAnalyzer(SecurityAnalyzer):
await self.check_usertask()
await self.check_fillaction()
async def check_usertask(self):
async def check_usertask(self) -> None:
"""Looks at the most recent trace element. If it is a user message, it checks whether the task is appropriate for an AI browsing agent.
Ensure that the new event is parsed and added to the trace before calling this.
@@ -198,23 +200,24 @@ class InvariantAnalyzer(SecurityAnalyzer):
self.event_stream.add_event, new_event, event_source
)
def parse_browser_action(self, browser_action):
def parse_browser_action(
self, browser_action: str
) -> list[tuple[str | None, list[str]]]:
assert browser_action[-1] == ')'
tree = ast.parse(browser_action, mode='exec')
function_calls = []
function_calls: list[tuple[str | None, list[str]]] = []
for node in tree.body:
if isinstance(node, ast.Expr) and isinstance(node.value, ast.Call):
call_node = node.value # This contains the actual function call
# Extract function name
func_name: str | None = None
if isinstance(call_node.func, ast.Name):
func_name = call_node.func.id
elif isinstance(call_node.func, ast.Attribute):
func_name = (
f'{ast.unparse(call_node.func.value)}.{call_node.func.attr}'
)
else:
func_name = None
# Extract positional arguments
args = [ast.unparse(arg) for arg in call_node.args]
@@ -223,7 +226,7 @@ class InvariantAnalyzer(SecurityAnalyzer):
raise ValueError('The code does not represent a function call.')
return function_calls
async def check_fillaction(self):
async def check_fillaction(self) -> None:
"""Looks at the most recent trace element. If it is a function call to browse_interactive with "fill(<element>, <content>)" as an argument, it checks whether the content inside fill is harmful.
Ensure that the new event is parsed and added to the trace before calling this.
@@ -285,7 +288,7 @@ class InvariantAnalyzer(SecurityAnalyzer):
break
async def should_confirm(self, event: Event) -> bool:
risk = event.security_risk # type: ignore [attr-defined]
risk = event.security_risk if hasattr(event, 'security_risk') else None # type: ignore [attr-defined]
return (
risk is not None
and risk < self.settings.get('RISK_SEVERITY', ActionSecurityRisk.MEDIUM)
@@ -305,24 +308,21 @@ class InvariantAnalyzer(SecurityAnalyzer):
async def security_risk(self, event: Action) -> ActionSecurityRisk:
logger.debug('Calling security_risk on InvariantAnalyzer')
new_elements = parse_element(self.trace, event)
input = [e.model_dump(exclude_none=True) for e in new_elements] # type: ignore [call-overload]
input_data = [
cast(dict[str, Any], e.model_dump(exclude_none=True)) for e in new_elements
]
self.trace.extend(new_elements)
check_result = self.monitor.check(self.input, input)
self.input.extend(input)
check_result = self.monitor.check(self.input, input_data)
self.input.extend(input_data)
risk = ActionSecurityRisk.UNKNOWN
if isinstance(check_result, tuple):
result, err = check_result
if err:
logger.warning(f'Error checking policy: {err}')
return risk
else:
logger.warning(f'Error checking policy: {check_result}')
# Process check_result
result, err = check_result
if err:
logger.warning(f'Error checking policy: {err}')
return risk
risk = self.get_risk(result)
return risk
return self.get_risk(result)
### Handle API requests
async def handle_api_request(self, request: Request) -> Any:
@@ -343,23 +343,23 @@ class InvariantAnalyzer(SecurityAnalyzer):
return await self.update_settings(request)
raise HTTPException(status_code=405, detail='Method Not Allowed')
async def export_trace(self, request: Request) -> Any:
async def export_trace(self, request: Request) -> JSONResponse:
return JSONResponse(content=self.input)
async def get_policy(self, request: Request) -> Any:
async def get_policy(self, request: Request) -> JSONResponse:
return JSONResponse(content={'policy': self.monitor.policy})
async def update_policy(self, request: Request) -> Any:
async def update_policy(self, request: Request) -> JSONResponse:
data = await request.json()
policy = data.get('policy')
new_monitor = self.client.Monitor.from_string(policy)
self.monitor = new_monitor
return JSONResponse(content={'policy': policy})
async def get_settings(self, request: Request) -> Any:
async def get_settings(self, request: Request) -> JSONResponse:
return JSONResponse(content=self.settings)
async def update_settings(self, request: Request) -> Any:
async def update_settings(self, request: Request) -> JSONResponse:
settings = await request.json()
self.settings = settings
return JSONResponse(content=self.settings)

View File

@@ -1,5 +1,5 @@
import time
from typing import Any, Union
from typing import Any
import httpx
@@ -7,7 +7,7 @@ import httpx
class InvariantClient:
timeout: int = 120
def __init__(self, server_url: str, session_id: str | None = None):
def __init__(self, server_url: str, session_id: str | None = None) -> None:
self.server = server_url
self.session_id, err = self._create_session(session_id)
if err:
@@ -38,7 +38,7 @@ class InvariantClient:
return None, err
return None, ConnectionError('Connection timed out')
def close_session(self) -> Union[None, Exception]:
def close_session(self) -> Exception | None:
try:
response = httpx.delete(
f'{self.server}/session/?session_id={self.session_id}', timeout=60
@@ -52,6 +52,7 @@ class InvariantClient:
def __init__(self, invariant: 'InvariantClient') -> None:
self.server = invariant.server
self.session_id = invariant.session_id
self.policy_id: str | None = None
def _create_policy(self, rule: str) -> tuple[str | None, Exception | None]:
try:
@@ -83,7 +84,7 @@ class InvariantClient:
self.policy_id = policy_id
return self
def analyze(self, trace: list[dict]) -> Union[Any, Exception]:
def analyze(self, trace: list[dict[str, Any]]) -> tuple[Any, Exception | None]:
try:
response = httpx.post(
f'{self.server}/policy/{self.policy_id}/analyze?session_id={self.session_id}',
@@ -100,6 +101,7 @@ class InvariantClient:
self.server = invariant.server
self.session_id = invariant.session_id
self.policy = ''
self.monitor_id: str | None = None
def _create_monitor(self, rule: str) -> tuple[str | None, Exception | None]:
try:
@@ -122,8 +124,10 @@ class InvariantClient:
return self
def check(
self, past_events: list[dict], pending_events: list[dict]
) -> Union[Any, Exception]:
self,
past_events: list[dict[str, Any]],
pending_events: list[dict[str, Any]],
) -> tuple[Any, Exception | None]:
try:
response = httpx.post(
f'{self.server}/monitor/{self.monitor_id}/check?session_id={self.session_id}',

View File

@@ -11,14 +11,14 @@ class LLM:
class Event(BaseModel):
metadata: dict | None = Field(
metadata: dict[str, Any] | None = Field(
default_factory=lambda: dict(), description='Metadata associated with the event'
)
class Function(BaseModel):
name: str
arguments: dict
arguments: dict[str, Any]
class ToolCall(Event):

View File

@@ -1,5 +1,3 @@
from typing import Union
from pydantic import BaseModel, Field
from openhands.core.logger import openhands_logger as logger
@@ -18,7 +16,7 @@ from openhands.events.observation import (
from openhands.events.serialization.event import event_to_dict
from openhands.security.invariant.nodes import Function, Message, ToolCall, ToolOutput
TraceElement = Union[Message, ToolCall, ToolOutput, Function]
TraceElement = Message | ToolCall | ToolOutput | Function
def get_next_id(trace: list[TraceElement]) -> str:
@@ -40,7 +38,7 @@ def get_last_id(
def parse_action(trace: list[TraceElement], action: Action) -> list[TraceElement]:
next_id = get_next_id(trace)
inv_trace = [] # type: list[TraceElement]
inv_trace: list[TraceElement] = []
if type(action) == MessageAction:
if action.source == EventSource.USER:
inv_trace.append(Message(role='user', content=action.content))
@@ -82,8 +80,8 @@ def parse_element(
return parse_observation(trace, element)
def parse_trace(trace: list[tuple[Action, Observation]]):
inv_trace = [] # type: list[TraceElement]
def parse_trace(trace: list[tuple[Action, Observation]]) -> list[TraceElement]:
inv_trace: list[TraceElement] = []
for action, obs in trace:
inv_trace.extend(parse_action(inv_trace, action))
inv_trace.extend(parse_observation(inv_trace, obs))
@@ -93,11 +91,11 @@ def parse_trace(trace: list[tuple[Action, Observation]]):
class InvariantState(BaseModel):
trace: list[TraceElement] = Field(default_factory=list)
def add_action(self, action: Action):
def add_action(self, action: Action) -> None:
self.trace.extend(parse_action(self.trace, action))
def add_observation(self, obs: Observation):
def add_observation(self, obs: Observation) -> None:
self.trace.extend(parse_observation(self.trace, obs))
def concatenate(self, other: 'InvariantState'):
def concatenate(self, other: 'InvariantState') -> None:
self.trace.extend(other.trace)

View File

@@ -1,5 +1,8 @@
from typing import Type
from openhands.security.analyzer import SecurityAnalyzer
from openhands.security.invariant.analyzer import InvariantAnalyzer
SecurityAnalyzers = {
SecurityAnalyzers: dict[str, Type[SecurityAnalyzer]] = {
'invariant': InvariantAnalyzer,
}

View File

@@ -402,23 +402,40 @@ class StandaloneConversationManager(ConversationManager):
def _create_conversation_update_callback(
self, user_id: str | None, github_user_id: str | None, conversation_id: str
) -> Callable:
def callback(*args, **kwargs):
def callback(event, *args, **kwargs):
call_async_from_sync(
self._update_timestamp_for_conversation,
self._update_conversation_for_event,
GENERAL_TIMEOUT,
user_id,
github_user_id,
conversation_id,
event,
)
return callback
async def _update_timestamp_for_conversation(
self, user_id: str, github_user_id: str, conversation_id: str
async def _update_conversation_for_event(
self, user_id: str, github_user_id: str, conversation_id: str, event=None
):
conversation_store = await self._get_conversation_store(user_id, github_user_id)
conversation = await conversation_store.get_metadata(conversation_id)
conversation.last_updated_at = datetime.now(timezone.utc)
# Update cost/token metrics if event has llm_metrics
if event and hasattr(event, 'llm_metrics') and event.llm_metrics:
metrics = event.llm_metrics
# Update accumulated cost
if hasattr(metrics, 'accumulated_cost'):
conversation.accumulated_cost = metrics.accumulated_cost
# Update token usage
if hasattr(metrics, 'accumulated_token_usage'):
token_usage = metrics.accumulated_token_usage
conversation.prompt_tokens = token_usage.prompt_tokens
conversation.completion_tokens = token_usage.completion_tokens
conversation.total_tokens = token_usage.prompt_tokens + token_usage.completion_tokens
await conversation_store.save_metadata(conversation)

View File

@@ -1,11 +1,9 @@
import os
import tempfile
from fastapi import (
APIRouter,
HTTPException,
Request,
UploadFile,
status,
)
from fastapi.responses import FileResponse, JSONResponse
@@ -17,19 +15,14 @@ from openhands.core.exceptions import AgentRuntimeUnavailableError
from openhands.core.logger import openhands_logger as logger
from openhands.events.action import (
FileReadAction,
FileWriteAction,
)
from openhands.events.observation import (
ErrorObservation,
FileReadObservation,
FileWriteObservation,
)
from openhands.runtime.base import Runtime
from openhands.server.file_config import (
FILES_TO_IGNORE,
MAX_FILE_SIZE_MB,
is_extension_allowed,
sanitize_filename,
)
from openhands.utils.async_utils import call_sync_from_async
@@ -37,7 +30,7 @@ app = APIRouter(prefix='/api/conversations/{conversation_id}')
@app.get('/list-files')
async def list_files(request: Request, conversation_id: str, path: str | None = None):
async def list_files(request: Request, path: str | None = None):
"""List files in the specified path.
This function retrieves a list of files from the agent's runtime file store,
@@ -148,168 +141,8 @@ async def select_file(file: str, request: Request):
)
@app.post('/upload-files')
async def upload_file(request: Request, conversation_id: str, files: list[UploadFile]):
"""Upload a list of files to the workspace.
To upload a files:
```sh
curl -X POST -F "file=@<file_path1>" -F "file=@<file_path2>" http://localhost:3000/api/conversations/{conversation_id}/upload-files
```
Args:
request (Request): The incoming request object.
files (list[UploadFile]): A list of files to be uploaded.
Returns:
dict: A message indicating the success of the upload operation.
Raises:
HTTPException: If there's an error saving the files.
"""
try:
uploaded_files = []
skipped_files = []
for file in files:
safe_filename = sanitize_filename(file.filename)
file_contents = await file.read()
if (
MAX_FILE_SIZE_MB > 0
and len(file_contents) > MAX_FILE_SIZE_MB * 1024 * 1024
):
skipped_files.append(
{
'name': safe_filename,
'reason': f'Exceeds maximum size limit of {MAX_FILE_SIZE_MB}MB',
}
)
continue
if not is_extension_allowed(safe_filename):
skipped_files.append(
{'name': safe_filename, 'reason': 'File type not allowed'}
)
continue
# copy the file to the runtime
with tempfile.TemporaryDirectory() as tmp_dir:
tmp_file_path = os.path.join(tmp_dir, safe_filename)
with open(tmp_file_path, 'wb') as tmp_file:
tmp_file.write(file_contents)
tmp_file.flush()
runtime: Runtime = request.state.conversation.runtime
try:
await call_sync_from_async(
runtime.copy_to,
tmp_file_path,
runtime.config.workspace_mount_path_in_sandbox,
)
except AgentRuntimeUnavailableError as e:
logger.error(f'Error saving file {safe_filename}: {e}')
return JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content={'error': f'Error saving file: {e}'},
)
uploaded_files.append(safe_filename)
response_content = {
'message': 'File upload process completed',
'uploaded_files': uploaded_files,
'skipped_files': skipped_files,
}
if not uploaded_files and skipped_files:
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content={
**response_content,
'error': 'No files were uploaded successfully',
},
)
return JSONResponse(status_code=status.HTTP_200_OK, content=response_content)
except Exception as e:
logger.error(f'Error during file upload: {e}')
return JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content={
'error': f'Error during file upload: {str(e)}',
'uploaded_files': [],
'skipped_files': [],
},
)
@app.post('/save-file')
async def save_file(request: Request):
"""Save a file to the agent's runtime file store.
This endpoint allows saving a file when the agent is in a paused, finished,
or awaiting user input state. It checks the agent's state before proceeding
with the file save operation.
Args:
request (Request): The incoming FastAPI request object.
Returns:
JSONResponse: A JSON response indicating the success of the operation.
Raises:
HTTPException:
- 403 error if the agent is not in an allowed state for editing.
- 400 error if the file path or content is missing.
- 500 error if there's an unexpected error during the save operation.
"""
try:
# Extract file path and content from the request
data = await request.json()
file_path = data.get('filePath')
content = data.get('content')
# Validate the presence of required data
if not file_path or content is None:
raise HTTPException(status_code=400, detail='Missing filePath or content')
# Save the file to the agent's runtime file store
runtime: Runtime = request.state.conversation.runtime
file_path = os.path.join(
runtime.config.workspace_mount_path_in_sandbox, file_path
)
write_action = FileWriteAction(file_path, content)
try:
observation = await call_sync_from_async(runtime.run_action, write_action)
except AgentRuntimeUnavailableError as e:
logger.error(f'Error saving file: {e}')
return JSONResponse(
status_code=500,
content={'error': f'Error saving file: {e}'},
)
if isinstance(observation, FileWriteObservation):
return JSONResponse(
status_code=200, content={'message': 'File saved successfully'}
)
elif isinstance(observation, ErrorObservation):
return JSONResponse(
status_code=500,
content={'error': f'Failed to save file: {observation}'},
)
else:
return JSONResponse(
status_code=500,
content={'error': f'Unexpected observation: {observation}'},
)
except Exception as e:
# Log the error and return a 500 response
logger.error(f'Error saving file: {e}')
raise HTTPException(status_code=500, detail=f'Error saving file: {e}')
@app.get('/zip-directory')
def zip_current_workspace(request: Request, conversation_id: str):
def zip_current_workspace(request: Request):
try:
logger.debug('Zipping workspace')
runtime: Runtime = request.state.conversation.runtime

View File

@@ -82,7 +82,7 @@ async def unset_settings_tokens(request: Request) -> JSONResponse:
)
@app.post('/reset-settings', response_model=dict[str, str])
@app.post('/settings/reset', response_model=dict[str, str])
async def reset_settings(request: Request) -> JSONResponse:
"""
Resets user settings.

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