mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
55 Commits
login-work
...
gitlab-doc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bc1305fc0e | ||
|
|
f985a46cfd | ||
|
|
9ca96afe29 | ||
|
|
7f3f44432e | ||
|
|
35b6b8ae2f | ||
|
|
52110305b3 | ||
|
|
877644be8c | ||
|
|
3bc85eb7ac | ||
|
|
5fa01ed278 | ||
|
|
1f747232cf | ||
|
|
4b1ed30e97 | ||
|
|
998de564cd | ||
|
|
06ce12eff4 | ||
|
|
88fc26d9b0 | ||
|
|
99233ec153 | ||
|
|
ae9573a503 | ||
|
|
f2725eeb3d | ||
|
|
1b63633030 | ||
|
|
107789b5a8 | ||
|
|
04bdea5faf | ||
|
|
2bad4ea3d2 | ||
|
|
1c4c477b3f | ||
|
|
391ba1d988 | ||
|
|
70f469b0c1 | ||
|
|
8a5c6d3bef | ||
|
|
998e04e51b | ||
|
|
da7041b5e9 | ||
|
|
e4b7b31f48 | ||
|
|
587c53f115 | ||
|
|
4d76f31610 | ||
|
|
c25a96825c | ||
|
|
16137942eb | ||
|
|
cf07b151dc | ||
|
|
ea3787c2ba | ||
|
|
956d278138 | ||
|
|
fa186fc433 | ||
|
|
4ef9c72da1 | ||
|
|
c5245a622d | ||
|
|
9b1aaa53fe | ||
|
|
4deffa3907 | ||
|
|
a47c6f3ed1 | ||
|
|
90ece3f8e1 | ||
|
|
a948b0fef3 | ||
|
|
52848cd3db | ||
|
|
62f015370a | ||
|
|
7109b057b6 | ||
|
|
e694fc2d58 | ||
|
|
50baf3fd18 | ||
|
|
dc37f039bf | ||
|
|
91fff77ffe | ||
|
|
db01cd39d5 | ||
|
|
00865fbb8a | ||
|
|
356cd9ff9f | ||
|
|
78d82408b7 | ||
|
|
3c4ebc3d8a |
2
.github/workflows/openhands-resolver.yml
vendored
2
.github/workflows/openhands-resolver.yml
vendored
@@ -24,7 +24,7 @@ on:
|
||||
LLM_MODEL:
|
||||
required: false
|
||||
type: string
|
||||
default: "anthropic/claude-3-5-sonnet-20241022"
|
||||
default: "anthropic/claude-3-7-sonnet-20250219"
|
||||
LLM_API_VERSION:
|
||||
required: false
|
||||
type: string
|
||||
|
||||
13
Makefile
13
Makefile
@@ -39,6 +39,7 @@ ifeq ($(INSTALL_DOCKER),)
|
||||
@$(MAKE) -s check-docker
|
||||
endif
|
||||
@$(MAKE) -s check-poetry
|
||||
@$(MAKE) -s check-tmux
|
||||
@echo "$(GREEN)Dependencies checked successfully.$(RESET)"
|
||||
|
||||
check-system:
|
||||
@@ -101,6 +102,18 @@ check-docker:
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
check-tmux:
|
||||
@echo "$(YELLOW)Checking tmux installation...$(RESET)"
|
||||
@if command -v tmux > /dev/null; then \
|
||||
echo "$(BLUE)$(shell tmux -V) is already installed.$(RESET)"; \
|
||||
else \
|
||||
echo "$(YELLOW)╔════════════════════════════════════════════════════════════════════════════╗$(RESET)"; \
|
||||
echo "$(YELLOW)║ OPTIONAL: tmux is not installed. ║$(RESET)"; \
|
||||
echo "$(YELLOW)║ Some advanced terminal features may not work without tmux. ║$(RESET)"; \
|
||||
echo "$(YELLOW)║ You can install it if needed, but it's not required for development. ║$(RESET)"; \
|
||||
echo "$(YELLOW)╚════════════════════════════════════════════════════════════════════════════╝$(RESET)"; \
|
||||
fi
|
||||
|
||||
check-poetry:
|
||||
@echo "$(YELLOW)Checking Poetry installation...$(RESET)"
|
||||
@if command -v poetry > /dev/null; then \
|
||||
|
||||
@@ -61,8 +61,8 @@ RUN add-apt-repository ppa:deadsnakes/ppa \
|
||||
&& apt-get install -y python3.12 python3.12-venv python3.12-dev python3-pip \
|
||||
&& ln -s /usr/bin/python3.12 /usr/bin/python
|
||||
|
||||
# NodeJS >= 18.17.1
|
||||
RUN curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \
|
||||
# NodeJS >= 22.x
|
||||
RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
|
||||
&& apt-get install -y nodejs
|
||||
|
||||
# Poetry >= 1.8
|
||||
@@ -108,7 +108,7 @@ WORKDIR /app
|
||||
|
||||
# cache build dependencies
|
||||
RUN \
|
||||
--mount=type=bind,source=./,target=/app/ \
|
||||
--mount=type=bind,source=./,target=/app/,rw \
|
||||
<<EOF
|
||||
#!/bin/bash
|
||||
make -s clean
|
||||
|
||||
1
docs/.gitignore
vendored
1
docs/.gitignore
vendored
@@ -3,6 +3,7 @@
|
||||
|
||||
# Production
|
||||
/build
|
||||
/static/swagger-ui
|
||||
|
||||
# Generated files
|
||||
.docusaurus
|
||||
|
||||
@@ -36,6 +36,7 @@ const config: Config = {
|
||||
mermaid: true,
|
||||
},
|
||||
themes: ['@docusaurus/theme-mermaid'],
|
||||
plugins: [],
|
||||
presets: [
|
||||
[
|
||||
'classic',
|
||||
@@ -75,6 +76,11 @@ const config: Config = {
|
||||
position: 'left',
|
||||
label: 'User Guides',
|
||||
},
|
||||
{
|
||||
href: 'https://docs.all-hands.dev/swagger-ui/', // FIXME: this should be a relative path, but docusarus steals the click
|
||||
label: 'API',
|
||||
position: 'left',
|
||||
},
|
||||
{
|
||||
type: 'localeDropdown',
|
||||
position: 'left',
|
||||
|
||||
102
docs/generate-swagger-ui.js
Normal file
102
docs/generate-swagger-ui.js
Normal file
@@ -0,0 +1,102 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const swaggerUiDist = require('swagger-ui-dist');
|
||||
|
||||
/**
|
||||
* This script manually sets up Swagger UI for the Docusaurus documentation.
|
||||
*
|
||||
* Why we need this approach:
|
||||
* 1. Docusaurus doesn't have a built-in way to integrate Swagger UI
|
||||
* 2. We need to copy the necessary files from swagger-ui-dist to our static directory
|
||||
* 3. We need to create a custom index.html file that points to our OpenAPI spec
|
||||
* 4. This approach allows us to customize the Swagger UI to match our documentation style
|
||||
*/
|
||||
|
||||
// Get the absolute path to the swagger-ui-dist package
|
||||
const swaggerUiDistPath = swaggerUiDist.getAbsoluteFSPath();
|
||||
|
||||
// Create the target directory if it doesn't exist
|
||||
const targetDir = path.join(__dirname, 'static', 'swagger-ui');
|
||||
if (!fs.existsSync(targetDir)) {
|
||||
fs.mkdirSync(targetDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Copy all files from swagger-ui-dist to our target directory
|
||||
const files = fs.readdirSync(swaggerUiDistPath);
|
||||
files.forEach(file => {
|
||||
const sourcePath = path.join(swaggerUiDistPath, file);
|
||||
const targetPath = path.join(targetDir, file);
|
||||
|
||||
// Skip directories and non-essential files
|
||||
if (fs.statSync(sourcePath).isDirectory() ||
|
||||
file === 'package.json' ||
|
||||
file === 'README.md' ||
|
||||
file.endsWith('.map')) {
|
||||
return;
|
||||
}
|
||||
|
||||
fs.copyFileSync(sourcePath, targetPath);
|
||||
});
|
||||
|
||||
// Create a custom index.html file that points to our OpenAPI spec
|
||||
const indexHtml = `
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>OpenHands API Documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="./swagger-ui.css" />
|
||||
<link rel="icon" type="image/png" href="./favicon-32x32.png" sizes="32x32" />
|
||||
<link rel="icon" type="image/png" href="./favicon-16x16.png" sizes="16x16" />
|
||||
<style>
|
||||
html {
|
||||
box-sizing: border-box;
|
||||
overflow: -moz-scrollbars-vertical;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
*,
|
||||
*:before,
|
||||
*:after {
|
||||
box-sizing: inherit;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background: #fafafa;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="swagger-ui"></div>
|
||||
|
||||
<script src="./swagger-ui-bundle.js" charset="UTF-8"> </script>
|
||||
<script src="./swagger-ui-standalone-preset.js" charset="UTF-8"> </script>
|
||||
<script>
|
||||
window.onload = function() {
|
||||
// Begin Swagger UI call region
|
||||
const ui = SwaggerUIBundle({
|
||||
url: "/openapi.json",
|
||||
dom_id: '#swagger-ui',
|
||||
deepLinking: true,
|
||||
presets: [
|
||||
SwaggerUIBundle.presets.apis,
|
||||
SwaggerUIStandalonePreset
|
||||
],
|
||||
plugins: [
|
||||
SwaggerUIBundle.plugins.DownloadUrl
|
||||
],
|
||||
layout: "StandaloneLayout"
|
||||
});
|
||||
// End Swagger UI call region
|
||||
window.ui = ui;
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
fs.writeFileSync(path.join(targetDir, 'index.html'), indexHtml);
|
||||
|
||||
console.log('Swagger UI files generated successfully in static/swagger-ui/');
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
Microagentes públicos são diretrizes especializadas acionadas por palavras-chave para todos os usuários do OpenHands.
|
||||
Eles são definidos em arquivos markdown no diretório
|
||||
[`microagents/knowledge/`](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/knowledge).
|
||||
[`microagents/`](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/knowledge).
|
||||
|
||||
Microagentes públicos:
|
||||
- Monitoram comandos recebidos em busca de suas palavras-chave de acionamento.
|
||||
@@ -15,7 +15,7 @@ Microagentes públicos:
|
||||
## Microagentes Públicos Atuais
|
||||
|
||||
Para mais informações sobre microagentes específicos, consulte seus arquivos de documentação individuais no
|
||||
diretório [`microagents/knowledge/`](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/knowledge/).
|
||||
diretório [`microagents/`](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/).
|
||||
|
||||
### Agente GitHub
|
||||
**Arquivo**: `github.md`
|
||||
@@ -59,7 +59,7 @@ yes | npm install package-name
|
||||
## Contribuindo com um Microagente Público
|
||||
|
||||
Você pode criar seus próprios microagentes públicos adicionando novos arquivos markdown ao
|
||||
diretório [`microagents/knowledge/`](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/knowledge/).
|
||||
diretório [`microagents/`](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/).
|
||||
|
||||
### Melhores Práticas para Microagentes Públicos
|
||||
|
||||
@@ -81,7 +81,7 @@ Antes de criar um microagente público, considere:
|
||||
|
||||
#### 2. Crie o Arquivo
|
||||
|
||||
Crie um novo arquivo markdown em [`microagents/knowledge/`](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/knowledge/)
|
||||
Crie um novo arquivo markdown em [`microagents/`](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/)
|
||||
com um nome descritivo (por exemplo, `docker.md` para um agente focado em Docker).
|
||||
|
||||
Atualize o arquivo com o frontmatter necessário [de acordo com o formato exigido](./microagents-overview#microagent-format)
|
||||
|
||||
@@ -8,18 +8,22 @@ OpenHands Cloud can be accessed at https://app.all-hands.dev/.
|
||||
|
||||
## Getting Started
|
||||
|
||||
After visiting OpenHands Cloud, you will be asked to connect with your GitHub account:
|
||||
1. After reading and accepting the terms of service, click `Connect to GitHub`.
|
||||
After visiting OpenHands Cloud, you will be asked to connect with your GitHub or GitLab account:
|
||||
|
||||
1. After reading and accepting the terms of service, click `Log in with GitHub` or `Log in with GitLab`.
|
||||
2. Review the permissions requested by OpenHands and then click `Authorize OpenHands AI`.
|
||||
- OpenHands will require some permissions from your GitHub account. To read more about these permissions,
|
||||
you can click the `Learn more` link on the GitHub authorize page.
|
||||
- OpenHands will require some permissions from your GitHub or GitLab account. To read more about these permissions:
|
||||
- GitHub: You can click the `Learn more` link on the GitHub authorize page.
|
||||
- GitLab: You can expand each permission request on the GitLab authorize page.
|
||||
|
||||
## Repository Access
|
||||
|
||||
### Adding Repository Access
|
||||
### GitHub
|
||||
|
||||
#### Adding Repository Access
|
||||
|
||||
You can grant OpenHands specific repository access:
|
||||
1. Click the `Select a Git project` dropdown, select `Add more repositories...`.
|
||||
1. Click `Add GitHub repos` on the Home page.
|
||||
2. Select the organization, then choose the specific repositories to grant OpenHands access to.
|
||||
<details>
|
||||
<summary>Permission Details for Repository Access</summary>
|
||||
@@ -42,11 +46,15 @@ You can grant OpenHands specific repository access:
|
||||
|
||||
3. Click on `Install & Authorize`.
|
||||
|
||||
### Modifying Repository Access
|
||||
#### Modifying Repository Access
|
||||
|
||||
You can modify repository access at any time by:
|
||||
* Using the same `Select a Git project > Add more repositories` workflow, or
|
||||
* Visiting the Settings page and selecting `Configure GitHub Repositories` under the `GitHub Settings` section.
|
||||
You can modify GitHub repository access at any time by:
|
||||
* Using the same `Add GitHub repos` workflow, or
|
||||
* Visiting the Settings page and selecting `Configure GitHub Repositories` under the `Git Settings` section.
|
||||
|
||||
### GitLab
|
||||
|
||||
When using your GitLab account, OpenHands will automatically have access to your repositories.
|
||||
|
||||
## Conversation Persistence
|
||||
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
# Repository Customization
|
||||
|
||||
You can customize how OpenHands works with your repository by creating a
|
||||
You can customize how OpenHands interacts with your repository by creating a
|
||||
`.openhands` directory at the root level.
|
||||
|
||||
## Microagents
|
||||
You can use microagents to extend the OpenHands prompts with information
|
||||
about your project and how you want OpenHands to work. See
|
||||
[Repository Microagents](../prompting/microagents-repo) for more information.
|
||||
|
||||
Microagents allow you to extend OpenHands prompts with information specific to your project and define how OpenHands
|
||||
should function. See [Microagents Overview](../prompting/microagents-overview) for more information.
|
||||
|
||||
|
||||
## Setup Script
|
||||
You can add `.openhands/setup.sh`, which will be run every time OpenHands begins
|
||||
working with your repository. This is a good place to install dependencies, set
|
||||
environment variables, etc.
|
||||
You can add a `.openhands/setup.sh` file, which will run every time OpenHands begins working with your repository.
|
||||
This is an ideal location for installing dependencies, setting environment variables, and performing other setup tasks.
|
||||
|
||||
For example:
|
||||
```bash
|
||||
|
||||
1
docs/modules/usage/how-to/gitlab-runner.md
Normal file
1
docs/modules/usage/how-to/gitlab-runner.md
Normal file
@@ -0,0 +1 @@
|
||||
# Using GitLab CI Runners
|
||||
@@ -6,6 +6,9 @@
|
||||
- Displays the conversation between the user and OpenHands.
|
||||
- OpenHands explains its actions in this panel.
|
||||
|
||||
### Changes
|
||||
- Shows the file changes performed by OpenHands.
|
||||
|
||||
### Workspace
|
||||
- Browse project files and directories.
|
||||
- Use the `Open in VS Code` option to:
|
||||
@@ -20,7 +23,7 @@
|
||||
- Particularly handy when using OpenHands to perform data visualization tasks.
|
||||
|
||||
### App
|
||||
- Shows the web server when OpenHands runs an application.
|
||||
- Displays the web server when OpenHands runs an application.
|
||||
- Users can interact with the running application.
|
||||
|
||||
### Browser
|
||||
|
||||
49
docs/modules/usage/prompting/microagents-keyword.md
Normal file
49
docs/modules/usage/prompting/microagents-keyword.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# Keyword-Triggered Microagents
|
||||
|
||||
## Purpose
|
||||
|
||||
Keyword-triggered microagents provide OpenHands with specific instructions that are activated when certain keywords
|
||||
appear in the prompt. This is useful for tailoring behavior based on particular tools, languages, or frameworks.
|
||||
|
||||
## Microagent File
|
||||
|
||||
Create a keyword-triggered microagent (example: `.openhands/microagents/trigger-keyword.md`) to include instructions
|
||||
that activate only for prompts with specific keywords.
|
||||
|
||||
## Frontmatter Syntax
|
||||
|
||||
Frontmatter is required for keyword-triggered microagents. It must be placed at the top of the file,
|
||||
above the guidelines.
|
||||
|
||||
Enclose the frontmatter in triple dashes (---) and include the following fields:
|
||||
|
||||
| Field | Description | Required | Default |
|
||||
|------------|--------------------------------------------------|----------|------------------|
|
||||
| `name` | A unique identifier for the microagent. | Yes | 'default' |
|
||||
| `type` | Type of microagent. Must be set to `knowledge`. | Yes | 'repo' |
|
||||
| `triggers` | A list of keywords that activate the microagent. | Yes | None |
|
||||
| `agent` | The agent this microagent applies to. | No | 'CodeActAgent' |
|
||||
|
||||
|
||||
## Example
|
||||
|
||||
```
|
||||
---
|
||||
name: magic_word
|
||||
type: knowledge
|
||||
triggers:
|
||||
- yummyhappy
|
||||
- happyyummy
|
||||
agent: CodeActAgent
|
||||
---
|
||||
|
||||
The user has said the magic word. Respond with "That was delicious!"
|
||||
```
|
||||
|
||||
Keyword-triggered microagents:
|
||||
- Monitor incoming prompts for specified trigger words.
|
||||
- Activate when relevant triggers are detected.
|
||||
- Apply their specialized knowledge and capabilities.
|
||||
- Follow defined guidelines and restrictions.
|
||||
|
||||
[See examples of microagents triggered by keywords in the official OpenHands repository](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/knowledge)
|
||||
@@ -1,31 +1,40 @@
|
||||
# Microagents Overview
|
||||
|
||||
Microagents are specialized prompts that enhance OpenHands with domain-specific knowledge, repository-specific context
|
||||
and task-specific workflows. They help by providing expert guidance, automating common tasks, and ensuring
|
||||
consistent practices across projects.
|
||||
Microagents are specialized prompts that enhance OpenHands with domain-specific knowledge.
|
||||
They provide expert guidance, automate common tasks, and ensure consistent practices across projects.
|
||||
|
||||
## Microagent Categories
|
||||
## Microagent Types
|
||||
|
||||
Currently OpenHands supports two categories of microagents:
|
||||
Currently OpenHands supports the following types of microagents:
|
||||
|
||||
- [Repository-specific Microagents](./microagents-repo): Repository-specific context and guidelines for OpenHands.
|
||||
- [Public Microagents](./microagents-public): General guidelines triggered by keywords for all OpenHands users.
|
||||
- [General Repository Microagents](./microagents-repo): General guidelines for OpenHands about the repository.
|
||||
- [Keyword-Triggered Microagents](./microagents-keyword): Guidelines activated by specific keywords in prompts.
|
||||
|
||||
A microagent is classified as repository-specific or public depending on its location:
|
||||
To customize OpenHands' behavior, create a .openhands/microagents/ directory in the root of your repository and
|
||||
add `<microagent_name>.md` files inside.
|
||||
|
||||
- Repository-specific microagents are located in a repository's `.openhands/microagents/` directory
|
||||
- Public microagents are located in the official OpenHands repository inside the `/microagents` folder
|
||||
:::note
|
||||
Loaded microagents take up space in the context window.
|
||||
These microagents, alongside user messages, inform OpenHands about the task and the environment.
|
||||
:::
|
||||
|
||||
When OpenHands works with a repository, it:
|
||||
Example repository structure:
|
||||
|
||||
1. Loads **repository-specific** microagents from `.openhands/microagents/` if present in the repository.
|
||||
2. Loads **public knowledge** microagents triggered by keywords in conversations
|
||||
3. Loads **public tasks** microagents when explicitly requested by the user
|
||||
```
|
||||
some-repository/
|
||||
└── .openhands/
|
||||
└── microagents/
|
||||
└── repo.md # General repository guidelines
|
||||
└── trigger_this.md # Microagent triggered by specific keywords
|
||||
└── trigger_that.md # Microagent triggered by specific keywords
|
||||
```
|
||||
|
||||
You can check out the existing public microagents at the [official OpenHands repository](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/).
|
||||
## Microagents Frontmatter Requirements
|
||||
|
||||
## Microagent Format
|
||||
Each microagent file may include frontmatter that provides additional information. In some cases, this frontmatter
|
||||
is required:
|
||||
|
||||
All microagents use markdown files with YAML frontmatter that have special instructions to help OpenHands activate them.
|
||||
|
||||
Check out the [syntax documentation](./microagents-syntax) for a comprehensive guide on how to configure your microagents.
|
||||
| Microagent Type | Frontmatter Requirement |
|
||||
|----------------------------------|-------------------------------------------------------|
|
||||
| `General Repository Microagents` | Required only if more than one of this type exists. |
|
||||
| `Keyword-Triggered Microagents` | Required. |
|
||||
|
||||
@@ -1,35 +1,16 @@
|
||||
# Public Microagents
|
||||
# Global Microagents
|
||||
|
||||
## Overview
|
||||
|
||||
Public microagents provide specialized context and capabilities for all OpenHands users, regardless of their repository configuration. Unlike repository-specific microagents, public microagents are globally available across all repositories.
|
||||
Global microagents are [keyword-triggered microagents](./microagents-keyword) that apply to all OpenHands users.
|
||||
|
||||
Public microagents come in two types:
|
||||
## Contributing a Global Microagent
|
||||
|
||||
- **Knowledge microagents**: Automatically activated when keywords in conversations match their triggers
|
||||
- **Task microagents**: Explicitly invoked by users to guide through specific workflows
|
||||
|
||||
Both types follow the same syntax and structure as repository-specific microagents, using markdown files with YAML frontmatter that define their behavior and capabilities. They are located in the official OpenHands repository under:
|
||||
|
||||
- [`microagents/knowledge/`](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/knowledge) for knowledge microagents
|
||||
- [`microagents/tasks/`](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/tasks) for task microagents
|
||||
|
||||
Public microagents:
|
||||
|
||||
- Monitor incoming commands for their trigger words.
|
||||
- Activate when relevant triggers are detected.
|
||||
- Apply their specialized knowledge and capabilities.
|
||||
- Follow their specific guidelines and restrictions.
|
||||
|
||||
When loading public microagents, OpenHands scans the official repository's microagents directories recursively, processing all markdown files except README.md. The system categorizes each microagent based on its `type` field in the YAML frontmatter, regardless of its exact file location within the knowledge or tasks directories.
|
||||
|
||||
## Contributing a Public Microagent
|
||||
|
||||
You can create public microagents and share with the community by opening a pull request to the official repository.
|
||||
You can create global microagents and share with the community by opening a pull request to the official repository.
|
||||
|
||||
See the [CONTRIBUTING.md](https://github.com/All-Hands-AI/OpenHands/blob/main/CONTRIBUTING.md) for specific instructions on how to contribute to OpenHands.
|
||||
|
||||
### Public Microagents Best Practices
|
||||
### Global Microagents Best Practices
|
||||
|
||||
- **Clear Scope**: Keep the microagent focused on a specific domain or task.
|
||||
- **Explicit Instructions**: Provide clear, unambiguous guidelines.
|
||||
@@ -37,11 +18,11 @@ See the [CONTRIBUTING.md](https://github.com/All-Hands-AI/OpenHands/blob/main/CO
|
||||
- **Safety First**: Include necessary warnings and constraints.
|
||||
- **Integration Awareness**: Consider how the microagent interacts with other components.
|
||||
|
||||
### Steps to Contribute a Public Microagent
|
||||
### Steps to Contribute a Global Microagent
|
||||
|
||||
#### 1. Plan the Public Microagent
|
||||
#### 1. Plan the Global Microagent
|
||||
|
||||
Before creating a public microagent, consider:
|
||||
Before creating a global microagent, consider:
|
||||
|
||||
- What specific problem or use case will it address?
|
||||
- What unique capabilities or knowledge should it have?
|
||||
@@ -51,23 +32,19 @@ Before creating a public microagent, consider:
|
||||
#### 2. Create File
|
||||
|
||||
Create a new Markdown file with a descriptive name in the appropriate directory:
|
||||
[`microagents/`](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents)
|
||||
|
||||
- [`microagents/knowledge/`](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/knowledge) for knowledge microagents
|
||||
- [`microagents/tasks/`](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/tasks) for task microagents
|
||||
#### 3. Testing the Global Microagent
|
||||
|
||||
Ensure it follows the correct [syntax](./microagents-syntax.md) and [best practices](./microagents-syntax.md#markdown-content-best-practices).
|
||||
|
||||
#### 3. Testing the Public Microagent
|
||||
|
||||
- Test the agent with various prompts
|
||||
- Verify trigger words activate the agent correctly
|
||||
- Ensure instructions are clear and comprehensive
|
||||
- Check for potential conflicts and overlaps with existing agents
|
||||
- Test the agent with various prompts.
|
||||
- Verify trigger words activate the agent correctly.
|
||||
- Ensure instructions are clear and comprehensive.
|
||||
- Check for potential conflicts and overlaps with existing agents.
|
||||
|
||||
#### 4. Submission Process
|
||||
|
||||
Submit a pull request with:
|
||||
|
||||
- The new microagent file
|
||||
- Updated documentation if needed
|
||||
- Description of the agent's purpose and capabilities
|
||||
- The new microagent file.
|
||||
- Updated documentation if needed.
|
||||
- Description of the agent's purpose and capabilities.
|
||||
|
||||
@@ -1,117 +1,38 @@
|
||||
# Repository-specific Microagents
|
||||
# General Repository Microagents
|
||||
|
||||
## Overview
|
||||
## Purpose
|
||||
|
||||
OpenHands can be customized to work more effectively with specific repositories by providing repository-specific context and guidelines.
|
||||
General guidelines for OpenHands to work more effectively with the repository.
|
||||
|
||||
This section explains how to optimize OpenHands for your project.
|
||||
## Microagent File
|
||||
|
||||
## Creating Repository Microagents
|
||||
Create a general repository microagent (example: `.openhands/microagents/repo.md`) to include
|
||||
project-specific instructions, team practices, coding standards, and architectural guidelines that are relevant for
|
||||
**all** prompts in that repository.
|
||||
|
||||
You can customize OpenHands' behavior for your repository by creating a `.openhands/microagents/` directory in your repository's root.
|
||||
## Frontmatter Syntax
|
||||
|
||||
You can enhance OpenHands' performance by adding custom microagents to your repository:
|
||||
The frontmatter for this type of microagent is optional, unless you plan to include more than one general
|
||||
repository microagent.
|
||||
|
||||
1. For overall repository-specific instructions, create a `.openhands/microagents/repo.md` file
|
||||
2. For reusable domain knowledge triggered by keywords, add multiple `.md` files to `.openhands/microagents/knowledge/`
|
||||
3. For common workflows and tasks, create multiple `.md` files to `.openhands/microagents/tasks/`
|
||||
Frontmatter should be enclosed in triple dashes (---) and may include the following fields:
|
||||
|
||||
Check out the [best practices](./microagents-syntax.md#markdown-content-best-practices) for formatting the content of your custom microagent.
|
||||
| Field | Description | Required | Default |
|
||||
|-----------|-----------------------------------------|--------------------------------------------------------------------|----------------|
|
||||
| `name` | A unique identifier for the microagent | Required only if using more than one general repository microagent | 'default' |
|
||||
| `agent` | The agent this microagent applies to | No | 'CodeActAgent' |
|
||||
|
||||
Keep in mind that loaded microagents take up space in the context window. It's crucial to strike a balance between the additional context provided by microagents and the instructions provided in the user's inputs.
|
||||
|
||||
Note that you can use OpenHands to create new microagents. The public microagent [`add_agent`](https://github.com/All-Hands-AI/OpenHands/blob/main/microagents/knowledge/add_agent.md) is loaded to all OpenHands instance and can support you on this.
|
||||
|
||||
## Types of Microagents
|
||||
|
||||
OpenHands supports three primary types of microagents, each with specific purposes and features to enhance agent performance:
|
||||
|
||||
- [repository](#repository-microagents)
|
||||
- [knowledge](#knowledge-microagents)
|
||||
- [tasks](#tasks-microagents)
|
||||
|
||||
The standard directory structure within a repository is:
|
||||
|
||||
- One main `repo.md` file containing repository-specific instructions
|
||||
- Additional `Knowledge` agents in `.openhands/microagents/knowledge/` directory
|
||||
- Additional `Task` agents in `.openhands/microagents/tasks/` directory
|
||||
|
||||
When processing the `.openhands/microagents/` directory, OpenHands will recursively scan all subfolders and process any `.md` files (except `README.md`) it finds. The system determines the microagent type based on the `type` field in the YAML frontmatter, not by the file's location. However, for organizational clarity, it's recommended to follow the standard directory structure.
|
||||
|
||||
### Repository Microagents
|
||||
|
||||
The `Repository` microagent is loaded specifically from `.openhands/microagents/repo.md` and serves as the main
|
||||
repository-specific instruction file. This single file is automatically loaded whenever OpenHands works with that repository
|
||||
without requiring any keyword matching or explicit call from the user.
|
||||
|
||||
OpenHands does not support multiple `repo.md` files in different locations or multiple microagents with type `repo`.
|
||||
|
||||
If you need to organize different types of repository information, the recommended approach is to use a single `repo.md` file with well-structured sections rather than trying to create multiple microagents with the type `repo`.
|
||||
|
||||
The best practice is to include project-specific instructions, team practices, coding standards, and architectural guidelines that are relevant for **all** prompts in that repository.
|
||||
|
||||
Example structure:
|
||||
## Example
|
||||
|
||||
```
|
||||
your-repository/
|
||||
└── .openhands/
|
||||
└── microagents/
|
||||
└── repo.md # Repository-specific instructions
|
||||
---
|
||||
name: repo
|
||||
---
|
||||
|
||||
This project is a TODO application that allows users to track TODO items.
|
||||
|
||||
To set it up, you can run `npm run build`.
|
||||
Always make sure the tests are passing before committing changes. You can run the tests by running `npm run test`.
|
||||
```
|
||||
|
||||
[See the example in the official OpenHands repository](https://github.com/All-Hands-AI/OpenHands/blob/main/.openhands/microagents/repo.md?plain=1)
|
||||
|
||||
### Knowledge Microagents
|
||||
|
||||
Knowledge microagents provide specialized domain expertise:
|
||||
|
||||
- Recommended to be located in `.openhands/microagents/knowledge/`
|
||||
- Triggered by specific keywords in conversations
|
||||
- Contain expertise on tools, languages, frameworks, and common practices
|
||||
|
||||
Use knowledge microagents to trigger additional context relevant to specific technologies, tools, or workflows. For example, mentioning "git" in your conversation will automatically trigger git-related expertise to help with Git operations.
|
||||
|
||||
Examples structure:
|
||||
|
||||
```
|
||||
your-repository/
|
||||
└── .openhands/
|
||||
└── microagents/
|
||||
└── knowledge/
|
||||
└── git.md
|
||||
└── docker.md
|
||||
└── python.md
|
||||
└── ...
|
||||
└── repo.md
|
||||
```
|
||||
|
||||
You can find several real examples of `Knowledge` microagents in the [offical OpenHands repository](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/knowledge)
|
||||
|
||||
### Tasks Microagents
|
||||
|
||||
Task microagents guide users through interactive workflows:
|
||||
|
||||
- Recommended to be located in `.openhands/microagents/tasks/`
|
||||
- Provide step-by-step processes for common development tasks
|
||||
- Accept inputs and adapt to different scenarios
|
||||
- Ensure consistent outcomes for complex operations
|
||||
|
||||
Task microagents are a convenient way to store multi-step processes you perform regularly. For instance, you can create a `update_pr_description.md` microagent to automatically generate better pull request descriptions based on code changes.
|
||||
|
||||
Examples structure:
|
||||
|
||||
```
|
||||
your-repository/
|
||||
└── .openhands/
|
||||
└── microagents/
|
||||
└── tasks/
|
||||
└── update_pr_description.md
|
||||
└── address_pr_comments.md
|
||||
└── get_test_to_pass.md
|
||||
└── ...
|
||||
└── knowledge/
|
||||
└── ...
|
||||
└── repo.md
|
||||
```
|
||||
|
||||
You can find several real examples of `Tasks` microagents in the [offical OpenHands repository](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/tasks)
|
||||
[See more examples of general repository microagents here.](https://github.com/All-Hands-AI/OpenHands/tree/main/.openhands/microagents)
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
# Microagents Syntax
|
||||
|
||||
Microagents are defined using markdown files with YAML frontmatter that specify their behavior, triggers, and capabilities.
|
||||
|
||||
Find below a comprehensive description of the frontmatter syntax and other details about how to use each type of microagent available at OpenHands.
|
||||
|
||||
## Frontmatter Schema
|
||||
|
||||
Every microagent requires a YAML frontmatter section at the beginning of the file, enclosed by triple dashes (`---`). The fields are:
|
||||
|
||||
| Field | Description | Required | Used By |
|
||||
| ---------- | -------------------------------------------------- | ------------------------ | ---------------- |
|
||||
| `name` | Unique identifier for the microagent | Yes | All types |
|
||||
| `type` | Type of microagent: `repo`, `knowledge`, or `task` | Yes | All types |
|
||||
| `version` | Version number (Semantic versioning recommended) | Yes | All types |
|
||||
| `agent` | The agent type (typically `CodeActAgent`) | Yes | All types |
|
||||
| `author` | Creator of the microagent | No | All types |
|
||||
| `triggers` | List of keywords that activate the microagent | Yes for knowledge agents | Knowledge agents |
|
||||
| `inputs` | Defines required user inputs for task execution | Yes for task agents | Task agents |
|
||||
|
||||
## Core Fields
|
||||
|
||||
### `agent`
|
||||
|
||||
**Purpose**: Specifies which agent implementation processes the microagent (typically `CodeActAgent`).
|
||||
|
||||
- Defines a single agent responsible for processing the microagent
|
||||
- Must be available in the OpenHands system (see the [agent hub](https://github.com/All-Hands-AI/OpenHands/tree/main/openhands/agenthub))
|
||||
- If the specified agent is not active, the microagent will not be used
|
||||
|
||||
### `triggers`
|
||||
|
||||
**Purpose**: Defines keywords that activate the `knowledge` microagent.
|
||||
|
||||
**Example**:
|
||||
|
||||
```yaml
|
||||
triggers:
|
||||
- kubernetes
|
||||
- k8s
|
||||
- docker
|
||||
- security
|
||||
- containers cluster
|
||||
```
|
||||
|
||||
**Key points**:
|
||||
|
||||
- Can include both single words and multi-word phrases
|
||||
- Case-insensitive matching is typically used
|
||||
- More specific triggers (like "docker compose") prevent false activations
|
||||
- Multiple triggers increase the chance of activation in relevant contexts
|
||||
- Unique triggers like "flarglebargle" can be used for testing or special functionality
|
||||
- Triggers should be carefully chosen to avoid unwanted activations or conflicts with other microagents
|
||||
- Common terms used in many conversations may cause the microagent to be activated too frequently
|
||||
|
||||
When using multiple triggers, the microagent will be activated if any of the trigger words or phrases appear in the
|
||||
conversation.
|
||||
|
||||
### `inputs`
|
||||
|
||||
**Purpose**: Defines parameters required from the user when a `task` microagent is activated.
|
||||
|
||||
**Schema**:
|
||||
|
||||
```yaml
|
||||
inputs:
|
||||
- name: INPUT_NAME # Used with {{ INPUT_NAME }}
|
||||
description: 'Description of what this input is for'
|
||||
required: true # Optional, defaults to true
|
||||
```
|
||||
|
||||
**Key points**:
|
||||
|
||||
- The `name` and `description` properties are required for each input
|
||||
- The `required` property is optional and defaults to `true`
|
||||
- Input values are referenced in the microagent body using double curly braces (e.g., `{{ INPUT_NAME }}`)
|
||||
- All inputs defined will be collected from the user before the task microagent executes
|
||||
|
||||
**Variable Usage**: Reference input values using double curly braces `{{ INPUT_NAME }}`.
|
||||
|
||||
## Example Formats
|
||||
|
||||
### Repository Microagent
|
||||
|
||||
Repository microagents provide context and guidelines for a specific repository.
|
||||
|
||||
- Located at: `.openhands/microagents/repo.md`
|
||||
- Automatically loaded when working with the repository
|
||||
- Only one per repository
|
||||
|
||||
The `Repository` microagent is loaded specifically from `.openhands/microagents/repo.md` and serves as the main
|
||||
repository-specific instruction file. This single file is automatically loaded whenever OpenHands works with that repository
|
||||
without requiring any keyword matching or explicit call from the user.
|
||||
|
||||
[See the example in the official OpenHands repository](https://github.com/All-Hands-AI/OpenHands/blob/main/.openhands/microagents/repo.md?plain=1)
|
||||
|
||||
### Knowledge Microagent
|
||||
|
||||
Provides specialized domain expertise triggered by keywords.
|
||||
|
||||
You can find several real examples of `Knowledge` microagents in the [offical OpenHands repository](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/knowledge)
|
||||
|
||||
### Task Microagent
|
||||
|
||||
When explicitly asked by the user, will guide through interactive workflows with specific inputs.
|
||||
|
||||
You can find several real examples of `Tasks` microagents in the [offical OpenHands repository](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/tasks)
|
||||
|
||||
## Markdown Content Best Practices
|
||||
|
||||
After the frontmatter, compose the microagent body using Markdown syntax. Examples of elements you can include are:
|
||||
|
||||
- Clear, concise instructions outlining the microagent's purpose and responsibilities
|
||||
- Specific guidelines and constraints the microagent should adhere to
|
||||
- Relevant code snippets and practical examples to illustrate key points
|
||||
- Step-by-step procedures for task agents, guiding users through workflows
|
||||
|
||||
**Design Tips**:
|
||||
|
||||
- Keep microagents focused with a clear purpose
|
||||
- Provide specific guidelines rather than general advice
|
||||
- Use distinctive triggers for knowledge agents
|
||||
- Keep content concise to minimize context window usage
|
||||
- Break large microagents into smaller, focused ones
|
||||
|
||||
Aim for clarity, brevity, and practicality in your writing. Use formatting like bullet points, code blocks, and emphasis to enhance readability and comprehension.
|
||||
|
||||
Remember that balancing microagents details with user input space is important for maintaining effective interactions.
|
||||
433
docs/package-lock.json
generated
433
docs/package-lock.json
generated
@@ -24,6 +24,8 @@
|
||||
"@docusaurus/module-type-aliases": "^3.5.1",
|
||||
"@docusaurus/tsconfig": "^3.7.0",
|
||||
"@docusaurus/types": "^3.5.1",
|
||||
"swagger-cli": "^4.0.4",
|
||||
"swagger-ui-dist": "^5.21.0",
|
||||
"typescript": "~5.8.3"
|
||||
},
|
||||
"engines": {
|
||||
@@ -273,6 +275,273 @@
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@apidevtools/openapi-schemas": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz",
|
||||
"integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@apidevtools/swagger-cli": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@apidevtools/swagger-cli/-/swagger-cli-4.0.4.tgz",
|
||||
"integrity": "sha512-hdDT3B6GLVovCsRZYDi3+wMcB1HfetTU20l2DC8zD3iFRNMC6QNAZG5fo/6PYeHWBEv7ri4MvnlKodhNB0nt7g==",
|
||||
"deprecated": "This package has been abandoned. Please switch to using the actively maintained @redocly/cli",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@apidevtools/swagger-parser": "^10.0.1",
|
||||
"chalk": "^4.1.0",
|
||||
"js-yaml": "^3.14.0",
|
||||
"yargs": "^15.4.1"
|
||||
},
|
||||
"bin": {
|
||||
"swagger-cli": "bin/swagger-cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@apidevtools/swagger-cli/node_modules/argparse": {
|
||||
"version": "1.0.10",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
|
||||
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"sprintf-js": "~1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@apidevtools/swagger-cli/node_modules/camelcase": {
|
||||
"version": "5.3.1",
|
||||
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
|
||||
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@apidevtools/swagger-cli/node_modules/cliui": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
|
||||
"integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"string-width": "^4.2.0",
|
||||
"strip-ansi": "^6.0.0",
|
||||
"wrap-ansi": "^6.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@apidevtools/swagger-cli/node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@apidevtools/swagger-cli/node_modules/find-up": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
|
||||
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"locate-path": "^5.0.0",
|
||||
"path-exists": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@apidevtools/swagger-cli/node_modules/js-yaml": {
|
||||
"version": "3.14.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
|
||||
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"argparse": "^1.0.7",
|
||||
"esprima": "^4.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"js-yaml": "bin/js-yaml.js"
|
||||
}
|
||||
},
|
||||
"node_modules/@apidevtools/swagger-cli/node_modules/locate-path": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
|
||||
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-locate": "^4.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@apidevtools/swagger-cli/node_modules/p-limit": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
|
||||
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-try": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/@apidevtools/swagger-cli/node_modules/p-locate": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
|
||||
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"p-limit": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@apidevtools/swagger-cli/node_modules/path-exists": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@apidevtools/swagger-cli/node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@apidevtools/swagger-cli/node_modules/wrap-ansi": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
|
||||
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@apidevtools/swagger-cli/node_modules/y18n": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
|
||||
"integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/@apidevtools/swagger-cli/node_modules/yargs": {
|
||||
"version": "15.4.1",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
|
||||
"integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cliui": "^6.0.0",
|
||||
"decamelize": "^1.2.0",
|
||||
"find-up": "^4.1.0",
|
||||
"get-caller-file": "^2.0.1",
|
||||
"require-directory": "^2.1.1",
|
||||
"require-main-filename": "^2.0.0",
|
||||
"set-blocking": "^2.0.0",
|
||||
"string-width": "^4.2.0",
|
||||
"which-module": "^2.0.0",
|
||||
"y18n": "^4.0.0",
|
||||
"yargs-parser": "^18.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@apidevtools/swagger-cli/node_modules/yargs-parser": {
|
||||
"version": "18.1.3",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
|
||||
"integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"camelcase": "^5.0.0",
|
||||
"decamelize": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@apidevtools/swagger-methods": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz",
|
||||
"integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@apidevtools/swagger-parser": {
|
||||
"version": "10.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.1.1.tgz",
|
||||
"integrity": "sha512-u/kozRnsPO/x8QtKYJOqoGtC4kH6yg1lfYkB9Au0WhYB0FNLpyFusttQtvhlwjtG3rOwiRz4D8DnnXa8iEpIKA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@apidevtools/json-schema-ref-parser": "11.7.2",
|
||||
"@apidevtools/openapi-schemas": "^2.1.0",
|
||||
"@apidevtools/swagger-methods": "^3.0.2",
|
||||
"@jsdevtools/ono": "^7.1.3",
|
||||
"ajv": "^8.17.1",
|
||||
"ajv-draft-04": "^1.0.0",
|
||||
"call-me-maybe": "^1.0.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"openapi-types": ">=7"
|
||||
}
|
||||
},
|
||||
"node_modules/@apidevtools/swagger-parser/node_modules/@apidevtools/json-schema-ref-parser": {
|
||||
"version": "11.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-11.7.2.tgz",
|
||||
"integrity": "sha512-4gY54eEGEstClvEkGnwVkTkrx0sqwemEFG5OSRRn3tD91XH0+Q8XIkYIfo7IwEWPpJZwILb9GUXeShtplRc/eA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jsdevtools/ono": "^7.1.3",
|
||||
"@types/json-schema": "^7.0.15",
|
||||
"js-yaml": "^4.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/philsturgeon"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
"version": "7.26.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz",
|
||||
@@ -3835,6 +4104,13 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@jsdevtools/ono": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz",
|
||||
"integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@leichtgewicht/ip-codec": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz",
|
||||
@@ -3970,6 +4246,14 @@
|
||||
"integrity": "sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@scarf/scarf": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz",
|
||||
"integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@sideway/address": {
|
||||
"version": "4.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz",
|
||||
@@ -4967,6 +5251,21 @@
|
||||
"url": "https://github.com/sponsors/epoberezkin"
|
||||
}
|
||||
},
|
||||
"node_modules/ajv-draft-04": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz",
|
||||
"integrity": "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"ajv": "^8.5.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"ajv": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/ajv-formats": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz",
|
||||
@@ -5549,6 +5848,13 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/call-me-maybe": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz",
|
||||
"integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/callsites": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
|
||||
@@ -7192,6 +7498,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/decamelize": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
|
||||
"integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/decode-named-character-reference": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz",
|
||||
@@ -8600,6 +8916,16 @@
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/get-caller-file": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "6.* || 8.* || >= 10.*"
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.2.7",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz",
|
||||
@@ -13110,15 +13436,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.7",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
|
||||
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
|
||||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
},
|
||||
@@ -13427,6 +13754,14 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/openapi-types": {
|
||||
"version": "12.1.3",
|
||||
"resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz",
|
||||
"integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/opener": {
|
||||
"version": "1.5.2",
|
||||
"resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz",
|
||||
@@ -13809,9 +14144,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.4.38",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz",
|
||||
"integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==",
|
||||
"version": "8.4.49",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz",
|
||||
"integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -13826,10 +14161,11 @@
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.7",
|
||||
"picocolors": "^1.0.0",
|
||||
"source-map-js": "^1.2.0"
|
||||
"picocolors": "^1.1.1",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || >=14"
|
||||
@@ -15367,6 +15703,15 @@
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/pupa": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/pupa/-/pupa-3.1.0.tgz",
|
||||
@@ -16205,6 +16550,16 @@
|
||||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/require-from-string": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
||||
@@ -16222,6 +16577,13 @@
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/require-main-filename": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
|
||||
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/requires-port": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
|
||||
@@ -16702,6 +17064,13 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/set-blocking": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
|
||||
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/set-function-length": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
|
||||
@@ -16980,9 +17349,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
|
||||
"integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==",
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -17347,6 +17717,32 @@
|
||||
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz",
|
||||
"integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA=="
|
||||
},
|
||||
"node_modules/swagger-cli": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/swagger-cli/-/swagger-cli-4.0.4.tgz",
|
||||
"integrity": "sha512-Cp8YYuLny3RJFQ4CvOBTaqmOOgYsem52dPx1xM5S4EUWFblIh2Q8atppMZvXKUr1e9xH5RwipYpmdUzdPcxWcA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@apidevtools/swagger-cli": "4.0.4"
|
||||
},
|
||||
"bin": {
|
||||
"swagger-cli": "swagger-cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/swagger-ui-dist": {
|
||||
"version": "5.21.0",
|
||||
"resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.21.0.tgz",
|
||||
"integrity": "sha512-E0K3AB6HvQd8yQNSMR7eE5bk+323AUxjtCz/4ZNKiahOlPhPJxqn3UPIGs00cyY/dhrTDJ61L7C/a8u6zhGrZg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@scarf/scarf": "=1.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tapable": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
|
||||
@@ -17949,14 +18345,6 @@
|
||||
"punycode": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/uri-js/node_modules/punycode": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/url-loader": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/url-loader/-/url-loader-4.1.1.tgz",
|
||||
@@ -18610,6 +18998,13 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/which-module": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
|
||||
"integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/widest-line": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/widest-line/-/widest-line-4.0.1.tgz",
|
||||
|
||||
@@ -4,16 +4,18 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"docusaurus": "docusaurus",
|
||||
"start": "docusaurus start",
|
||||
"build": "docusaurus build",
|
||||
"start": "node generate-swagger-ui.js && docusaurus start",
|
||||
"build": "node generate-swagger-ui.js && docusaurus build",
|
||||
"swizzle": "docusaurus swizzle",
|
||||
"deploy": "docusaurus deploy",
|
||||
"clear": "docusaurus clear",
|
||||
"serve": "docusaurus serve",
|
||||
"write-translations": "docusaurus write-translations",
|
||||
"write-heading-ids": "docusaurus write-heading-ids",
|
||||
"typecheck": "tsc"
|
||||
"typecheck": "tsc",
|
||||
"generate-swagger-ui": "node generate-swagger-ui.js"
|
||||
},
|
||||
"// Note": "The OpenAPI spec is stored in docs/static/openapi.json so it's accessible at /openapi.json in the deployed site",
|
||||
"dependencies": {
|
||||
"@docusaurus/core": "^3.7.0",
|
||||
"@docusaurus/plugin-content-pages": "^3.7.0",
|
||||
@@ -31,6 +33,8 @@
|
||||
"@docusaurus/module-type-aliases": "^3.5.1",
|
||||
"@docusaurus/tsconfig": "^3.7.0",
|
||||
"@docusaurus/types": "^3.5.1",
|
||||
"swagger-cli": "^4.0.4",
|
||||
"swagger-ui-dist": "^5.21.0",
|
||||
"typescript": "~5.8.3"
|
||||
},
|
||||
"browserslist": {
|
||||
@@ -47,5 +51,6 @@
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0"
|
||||
}
|
||||
},
|
||||
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
||||
}
|
||||
|
||||
@@ -66,18 +66,18 @@ const sidebars: SidebarsConfig = {
|
||||
},
|
||||
{
|
||||
type: 'doc',
|
||||
label: 'Repository-specific',
|
||||
label: 'General Repository Microagents',
|
||||
id: 'usage/prompting/microagents-repo',
|
||||
},
|
||||
{
|
||||
type: 'doc',
|
||||
label: 'Public',
|
||||
id: 'usage/prompting/microagents-public',
|
||||
label: 'Keyword-Triggered Microagents',
|
||||
id: 'usage/prompting/microagents-keyword',
|
||||
},
|
||||
{
|
||||
type: 'doc',
|
||||
label: 'Syntax',
|
||||
id: 'usage/prompting/microagents-syntax',
|
||||
label: 'Global Microagents',
|
||||
id: 'usage/prompting/microagents-public',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
15
docs/static/README.md
vendored
Normal file
15
docs/static/README.md
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
# Static Files for OpenHands Documentation
|
||||
|
||||
This directory contains static files that are copied directly to the build output of the Docusaurus documentation.
|
||||
|
||||
## OpenAPI Specification
|
||||
|
||||
The `openapi.json` file in this directory is the OpenAPI specification for the OpenHands API. It is copied to the build output and is accessible at `/openapi.json` in the deployed site.
|
||||
|
||||
This file is used by the Swagger UI interface, which is accessible at `/swagger-ui/` in the deployed site.
|
||||
|
||||
## Why is the OpenAPI spec in the static directory?
|
||||
|
||||
The OpenAPI specification is placed in the static directory so that it's accessible at a predictable URL in the deployed site. This allows the Swagger UI to reference it directly.
|
||||
|
||||
We only need one copy of the OpenAPI spec file, which is this one in the static directory.
|
||||
BIN
docs/static/img/oh-features.png
vendored
BIN
docs/static/img/oh-features.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 121 KiB After Width: | Height: | Size: 120 KiB |
2085
docs/static/openapi.json
vendored
Normal file
2085
docs/static/openapi.json
vendored
Normal file
File diff suppressed because it is too large
Load Diff
628
docs/yarn.lock
628
docs/yarn.lock
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,10 @@
|
||||
# Run frontend checks
|
||||
echo "Running frontend checks..."
|
||||
cd frontend
|
||||
npm run check-unlocalized-strings
|
||||
npx lint-staged
|
||||
|
||||
# Run backend pre-commit
|
||||
echo "Running backend pre-commit..."
|
||||
cd ..
|
||||
pre-commit run --files openhands/**/* evaluation/**/* tests/**/* --show-diff-on-failure --config ./dev_config/python/.pre-commit-config.yaml
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { it, describe, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { it, describe, expect, vi, beforeAll, afterAll } from "vitest";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { AuthModal } from "#/components/features/waitlist/auth-modal";
|
||||
import * as CaptureConsent from "#/utils/handle-capture-consent";
|
||||
import * as AuthHook from "#/context/auth-context";
|
||||
|
||||
// Mock the useAuthUrl hook
|
||||
vi.mock("#/hooks/use-auth-url", () => ({
|
||||
useAuthUrl: () => "https://gitlab.com/oauth/authorize"
|
||||
}));
|
||||
|
||||
describe("AuthModal", () => {
|
||||
beforeEach(() => {
|
||||
beforeAll(() => {
|
||||
vi.stubGlobal("location", { href: "" });
|
||||
vi.spyOn(AuthHook, "useAuth").mockReturnValue({
|
||||
providersAreSet: false,
|
||||
@@ -20,29 +16,50 @@ describe("AuthModal", () => {
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
afterAll(() => {
|
||||
vi.unstubAllGlobals();
|
||||
vi.resetAllMocks();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("should render the GitHub and GitLab buttons", () => {
|
||||
render(<AuthModal githubAuthUrl="mock-url" appMode="saas" />);
|
||||
|
||||
it("should render a tos checkbox that is unchecked by default", () => {
|
||||
render(<AuthModal githubAuthUrl={null} appMode="saas" />);
|
||||
const checkbox = screen.getByRole("checkbox");
|
||||
|
||||
expect(checkbox).not.toBeChecked();
|
||||
});
|
||||
|
||||
it("should only enable the identity provider buttons if the tos checkbox is checked", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<AuthModal githubAuthUrl={null} appMode="saas" />);
|
||||
|
||||
const checkbox = screen.getByRole("checkbox");
|
||||
const githubButton = screen.getByRole("button", { name: "GITHUB$CONNECT_TO_GITHUB" });
|
||||
const gitlabButton = screen.getByRole("button", { name: "GITLAB$CONNECT_TO_GITLAB" });
|
||||
|
||||
expect(githubButton).toBeInTheDocument();
|
||||
expect(gitlabButton).toBeInTheDocument();
|
||||
expect(githubButton).toBeDisabled();
|
||||
expect(gitlabButton).toBeDisabled();
|
||||
|
||||
await user.click(checkbox);
|
||||
|
||||
expect(githubButton).not.toBeDisabled();
|
||||
expect(gitlabButton).not.toBeDisabled();
|
||||
});
|
||||
|
||||
it("should redirect to GitHub auth URL when GitHub button is clicked", async () => {
|
||||
it("should set user analytics consent to true when the user checks the tos checkbox", async () => {
|
||||
const handleCaptureConsentSpy = vi.spyOn(
|
||||
CaptureConsent,
|
||||
"handleCaptureConsent",
|
||||
);
|
||||
|
||||
const user = userEvent.setup();
|
||||
const mockUrl = "https://github.com/login/oauth/authorize";
|
||||
render(<AuthModal githubAuthUrl={mockUrl} appMode="saas" />);
|
||||
render(<AuthModal githubAuthUrl="mock-url" appMode="saas" />);
|
||||
|
||||
const githubButton = screen.getByRole("button", { name: "GITHUB$CONNECT_TO_GITHUB" });
|
||||
await user.click(githubButton);
|
||||
const checkbox = screen.getByRole("checkbox");
|
||||
await user.click(checkbox);
|
||||
|
||||
expect(window.location.href).toBe(mockUrl);
|
||||
const button = screen.getByRole("button", { name: "GITHUB$CONNECT_TO_GITHUB" });
|
||||
await user.click(button);
|
||||
|
||||
expect(handleCaptureConsentSpy).toHaveBeenCalledWith(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -46,10 +46,12 @@ describe("HomeHeader", () => {
|
||||
await userEvent.click(launchButton);
|
||||
|
||||
expect(createConversationSpy).toHaveBeenCalledExactlyOnceWith(
|
||||
"gui",
|
||||
undefined,
|
||||
undefined,
|
||||
[],
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
|
||||
// expect to be redirected to /conversations/:conversationId
|
||||
|
||||
@@ -154,8 +154,8 @@ describe("RepoConnector", () => {
|
||||
expect(createConversationSpy).not.toHaveBeenCalled();
|
||||
|
||||
// select a repository from the dropdown
|
||||
const dropdown = await waitFor(() =>
|
||||
within(repoConnector).getByTestId("repo-dropdown")
|
||||
const dropdown = await waitFor(() =>
|
||||
within(repoConnector).getByTestId("repo-dropdown"),
|
||||
);
|
||||
await userEvent.click(dropdown);
|
||||
|
||||
@@ -164,6 +164,7 @@ describe("RepoConnector", () => {
|
||||
await userEvent.click(launchButton);
|
||||
|
||||
expect(createConversationSpy).toHaveBeenCalledExactlyOnceWith(
|
||||
"gui",
|
||||
{
|
||||
full_name: "rbren/polaris",
|
||||
git_provider: "github",
|
||||
@@ -173,6 +174,7 @@ describe("RepoConnector", () => {
|
||||
undefined,
|
||||
[],
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -11,18 +11,13 @@ import { AuthProvider } from "#/context/auth-context";
|
||||
import { TaskCard } from "#/components/features/home/tasks/task-card";
|
||||
import * as GitService from "#/api/git";
|
||||
import { GitRepository } from "#/types/git";
|
||||
import {
|
||||
getFailingChecksPrompt,
|
||||
getMergeConflictPrompt,
|
||||
getOpenIssuePrompt,
|
||||
getUnresolvedCommentsPrompt,
|
||||
} from "#/components/features/home/tasks/get-prompt-for-query";
|
||||
|
||||
const MOCK_TASK_1: SuggestedTask = {
|
||||
issue_number: 123,
|
||||
repo: "repo1",
|
||||
title: "Task 1",
|
||||
task_type: "MERGE_CONFLICTS",
|
||||
git_provider: "github",
|
||||
};
|
||||
|
||||
const MOCK_TASK_2: SuggestedTask = {
|
||||
@@ -30,6 +25,7 @@ const MOCK_TASK_2: SuggestedTask = {
|
||||
repo: "repo2",
|
||||
title: "Task 2",
|
||||
task_type: "FAILING_CHECKS",
|
||||
git_provider: "github",
|
||||
};
|
||||
|
||||
const MOCK_TASK_3: SuggestedTask = {
|
||||
@@ -37,6 +33,7 @@ const MOCK_TASK_3: SuggestedTask = {
|
||||
repo: "repo3",
|
||||
title: "Task 3",
|
||||
task_type: "UNRESOLVED_COMMENTS",
|
||||
git_provider: "gitlab",
|
||||
};
|
||||
|
||||
const MOCK_TASK_4: SuggestedTask = {
|
||||
@@ -44,6 +41,7 @@ const MOCK_TASK_4: SuggestedTask = {
|
||||
repo: "repo4",
|
||||
title: "Task 4",
|
||||
task_type: "OPEN_ISSUE",
|
||||
git_provider: "gitlab",
|
||||
};
|
||||
|
||||
const MOCK_RESPOSITORIES: GitRepository[] = [
|
||||
@@ -97,7 +95,7 @@ describe("TaskCard", () => {
|
||||
expect(createConversationSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe("creating conversation prompts", () => {
|
||||
describe("creating suggested task conversation", () => {
|
||||
beforeEach(() => {
|
||||
const retrieveUserGitRepositoriesSpy = vi.spyOn(
|
||||
GitService,
|
||||
@@ -109,7 +107,7 @@ describe("TaskCard", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should call create conversation with the merge conflict prompt", async () => {
|
||||
it("should call create conversation with suggest task trigger and selected suggested task", async () => {
|
||||
const createConversationSpy = vi.spyOn(OpenHands, "createConversation");
|
||||
|
||||
renderTaskCard(MOCK_TASK_1);
|
||||
@@ -118,58 +116,12 @@ describe("TaskCard", () => {
|
||||
await userEvent.click(launchButton);
|
||||
|
||||
expect(createConversationSpy).toHaveBeenCalledWith(
|
||||
"suggested_task",
|
||||
MOCK_RESPOSITORIES[0],
|
||||
getMergeConflictPrompt(MOCK_TASK_1.issue_number, MOCK_TASK_1.repo),
|
||||
[],
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
it("should call create conversation with the failing checks prompt", async () => {
|
||||
const createConversationSpy = vi.spyOn(OpenHands, "createConversation");
|
||||
|
||||
renderTaskCard(MOCK_TASK_2);
|
||||
|
||||
const launchButton = screen.getByTestId("task-launch-button");
|
||||
await userEvent.click(launchButton);
|
||||
|
||||
expect(createConversationSpy).toHaveBeenCalledWith(
|
||||
MOCK_RESPOSITORIES[1],
|
||||
getFailingChecksPrompt(MOCK_TASK_2.issue_number, MOCK_TASK_2.repo),
|
||||
[],
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
it("should call create conversation with the unresolved comments prompt", async () => {
|
||||
const createConversationSpy = vi.spyOn(OpenHands, "createConversation");
|
||||
|
||||
renderTaskCard(MOCK_TASK_3);
|
||||
|
||||
const launchButton = screen.getByTestId("task-launch-button");
|
||||
await userEvent.click(launchButton);
|
||||
|
||||
expect(createConversationSpy).toHaveBeenCalledWith(
|
||||
MOCK_RESPOSITORIES[2],
|
||||
getUnresolvedCommentsPrompt(MOCK_TASK_3.issue_number, MOCK_TASK_3.repo),
|
||||
[],
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
it("should call create conversation with the open issue prompt", async () => {
|
||||
const createConversationSpy = vi.spyOn(OpenHands, "createConversation");
|
||||
|
||||
renderTaskCard(MOCK_TASK_4);
|
||||
|
||||
const launchButton = screen.getByTestId("task-launch-button");
|
||||
await userEvent.click(launchButton);
|
||||
|
||||
expect(createConversationSpy).toHaveBeenCalledWith(
|
||||
MOCK_RESPOSITORIES[3],
|
||||
getOpenIssuePrompt(MOCK_TASK_4.issue_number, MOCK_TASK_4.repo),
|
||||
undefined,
|
||||
[],
|
||||
undefined,
|
||||
MOCK_TASK_1,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import { screen } from "@testing-library/react";
|
||||
import { renderWithProviders } from "test-utils";
|
||||
import { describe, afterEach, vi, it, expect } from "vitest";
|
||||
import { ExplorerTree } from "#/components/features/file-explorer/explorer-tree";
|
||||
|
||||
const FILES = ["file-1-1.ts", "folder-1-2"];
|
||||
|
||||
describe.skip("ExplorerTree", () => {
|
||||
afterEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
it("should render the explorer", () => {
|
||||
renderWithProviders(<ExplorerTree files={FILES} defaultOpen />);
|
||||
|
||||
expect(screen.getByText("file-1-1.ts")).toBeInTheDocument();
|
||||
expect(screen.getByText("folder-1-2")).toBeInTheDocument();
|
||||
// TODO: make sure children render
|
||||
});
|
||||
|
||||
it("should render the explorer given the defaultExpanded prop", () => {
|
||||
renderWithProviders(<ExplorerTree files={FILES} />);
|
||||
|
||||
expect(screen.queryByText("file-1-1.ts")).toBeInTheDocument();
|
||||
expect(screen.queryByText("folder-1-2")).toBeInTheDocument();
|
||||
// TODO: make sure children don't render
|
||||
});
|
||||
});
|
||||
@@ -1,64 +0,0 @@
|
||||
import { screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { renderWithProviders } from "test-utils";
|
||||
import { describe, it, expect, vi, afterEach } from "vitest";
|
||||
import { AgentState } from "#/types/agent-state";
|
||||
import { FileExplorer } from "#/components/features/file-explorer/file-explorer";
|
||||
import { FileService } from "#/api/file-service/file-service.api";
|
||||
|
||||
const getFilesSpy = vi.spyOn(FileService, "getFiles");
|
||||
|
||||
vi.mock("../../services/fileService", async () => ({
|
||||
uploadFiles: vi.fn(),
|
||||
}));
|
||||
|
||||
const renderFileExplorerWithRunningAgentState = () =>
|
||||
renderWithProviders(<FileExplorer isOpen onToggle={() => {}} />, {
|
||||
preloadedState: {
|
||||
agent: {
|
||||
curAgentState: AgentState.RUNNING,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
describe.skip("FileExplorer", () => {
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should get the workspace directory", async () => {
|
||||
renderFileExplorerWithRunningAgentState();
|
||||
|
||||
expect(await screen.findByText("folder1")).toBeInTheDocument();
|
||||
expect(await screen.findByText("file1.ts")).toBeInTheDocument();
|
||||
expect(getFilesSpy).toHaveBeenCalledTimes(1); // once for root
|
||||
});
|
||||
|
||||
it("should refetch the workspace when clicking the refresh button", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderFileExplorerWithRunningAgentState();
|
||||
|
||||
expect(await screen.findByText("folder1")).toBeInTheDocument();
|
||||
expect(await screen.findByText("file1.ts")).toBeInTheDocument();
|
||||
expect(getFilesSpy).toHaveBeenCalledTimes(1); // once for root
|
||||
|
||||
const refreshButton = screen.getByTestId("refresh");
|
||||
await user.click(refreshButton);
|
||||
|
||||
expect(getFilesSpy).toHaveBeenCalledTimes(2); // once for root, once for refresh button
|
||||
});
|
||||
|
||||
it("should toggle the explorer visibility when clicking the toggle button", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderFileExplorerWithRunningAgentState();
|
||||
|
||||
const folder1 = await screen.findByText("folder1");
|
||||
expect(folder1).toBeInTheDocument();
|
||||
|
||||
const toggleButton = screen.getByTestId("toggle");
|
||||
await user.click(toggleButton);
|
||||
|
||||
expect(folder1).toBeInTheDocument();
|
||||
expect(folder1).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
@@ -1,110 +0,0 @@
|
||||
import { screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { renderWithProviders } from "test-utils";
|
||||
import { vi, describe, afterEach, it, expect } from "vitest";
|
||||
import TreeNode from "#/components/features/file-explorer/tree-node";
|
||||
import { FileService } from "#/api/file-service/file-service.api";
|
||||
|
||||
const getFileSpy = vi.spyOn(FileService, "getFile");
|
||||
const getFilesSpy = vi.spyOn(FileService, "getFiles");
|
||||
|
||||
vi.mock("../../services/fileService", async () => ({
|
||||
uploadFile: vi.fn(),
|
||||
}));
|
||||
|
||||
describe.skip("TreeNode", () => {
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should render a file if property has no children", () => {
|
||||
renderWithProviders(<TreeNode path="/file.ts" defaultOpen />);
|
||||
expect(screen.getByText("file.ts")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render a folder if it's in a subdir", async () => {
|
||||
renderWithProviders(<TreeNode path="/folder1/" defaultOpen />);
|
||||
expect(getFilesSpy).toHaveBeenCalledWith("/folder1/");
|
||||
|
||||
expect(await screen.findByText("folder1")).toBeInTheDocument();
|
||||
expect(await screen.findByText("file2.ts")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should close a folder when clicking on it", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<TreeNode path="/folder1/" defaultOpen />);
|
||||
|
||||
const folder1 = await screen.findByText("folder1");
|
||||
const file2 = await screen.findByText("file2.ts");
|
||||
|
||||
expect(folder1).toBeInTheDocument();
|
||||
expect(file2).toBeInTheDocument();
|
||||
|
||||
await user.click(folder1);
|
||||
|
||||
expect(folder1).toBeInTheDocument();
|
||||
expect(screen.queryByText("file2.ts")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should open a folder when clicking on it", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<TreeNode path="/folder1/" />);
|
||||
|
||||
const folder1 = await screen.findByText("folder1");
|
||||
|
||||
expect(folder1).toBeInTheDocument();
|
||||
expect(screen.queryByText("file2.ts")).not.toBeInTheDocument();
|
||||
|
||||
await user.click(folder1);
|
||||
expect(getFilesSpy).toHaveBeenCalledWith("/folder1/");
|
||||
|
||||
expect(folder1).toBeInTheDocument();
|
||||
expect(await screen.findByText("file2.ts")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should call `OpenHands.getFile` and return the full path of a file when clicking on a file", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<TreeNode path="/folder1/file2.ts" defaultOpen />);
|
||||
|
||||
const file2 = screen.getByText("file2.ts");
|
||||
await user.click(file2);
|
||||
|
||||
expect(getFileSpy).toHaveBeenCalledWith("/folder1/file2.ts");
|
||||
});
|
||||
|
||||
it("should render the full explorer given the defaultOpen prop", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderWithProviders(<TreeNode path="/" defaultOpen />);
|
||||
|
||||
expect(getFilesSpy).toHaveBeenCalledWith("/");
|
||||
|
||||
const file1 = await screen.findByText("file1.ts");
|
||||
const folder1 = await screen.findByText("folder1");
|
||||
|
||||
expect(file1).toBeInTheDocument();
|
||||
expect(folder1).toBeInTheDocument();
|
||||
expect(screen.queryByText("file2.ts")).not.toBeInTheDocument();
|
||||
|
||||
await user.click(folder1);
|
||||
expect(getFilesSpy).toHaveBeenCalledWith("folder1/");
|
||||
|
||||
expect(file1).toBeInTheDocument();
|
||||
expect(folder1).toBeInTheDocument();
|
||||
expect(await screen.findByText("file2.ts")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render all children as collapsed when defaultOpen is false", async () => {
|
||||
renderWithProviders(<TreeNode path="/folder1/" defaultOpen={false} />);
|
||||
|
||||
const folder1 = await screen.findByText("folder1");
|
||||
|
||||
expect(folder1).toBeInTheDocument();
|
||||
expect(screen.queryByText("file2.ts")).not.toBeInTheDocument();
|
||||
|
||||
await userEvent.click(folder1);
|
||||
expect(getFilesSpy).toHaveBeenCalledWith("/folder1/");
|
||||
|
||||
expect(folder1).toBeInTheDocument();
|
||||
expect(await screen.findByText("file2.ts")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,108 +0,0 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { it, describe, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import AcceptTOS from "#/routes/accept-tos";
|
||||
import * as CaptureConsent from "#/utils/handle-capture-consent";
|
||||
import { openHands } from "#/api/open-hands-axios";
|
||||
|
||||
// Mock the react-router hooks
|
||||
vi.mock("react-router", () => ({
|
||||
useNavigate: () => vi.fn(),
|
||||
useSearchParams: () => [
|
||||
{
|
||||
get: (param: string) => {
|
||||
if (param === "redirect_url") {
|
||||
return "/dashboard";
|
||||
}
|
||||
return null;
|
||||
},
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
// Mock the axios instance
|
||||
vi.mock("#/api/open-hands-axios", () => ({
|
||||
openHands: {
|
||||
post: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe("AcceptTOS", () => {
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal("location", { href: "" });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
it("should render a TOS checkbox that is unchecked by default", () => {
|
||||
render(<AcceptTOS />);
|
||||
|
||||
const checkbox = screen.getByRole("checkbox");
|
||||
const continueButton = screen.getByRole("button", { name: "TOS$CONTINUE" });
|
||||
|
||||
expect(checkbox).not.toBeChecked();
|
||||
expect(continueButton).toBeDisabled();
|
||||
});
|
||||
|
||||
it("should enable the continue button when the TOS checkbox is checked", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<AcceptTOS />);
|
||||
|
||||
const checkbox = screen.getByRole("checkbox");
|
||||
const continueButton = screen.getByRole("button", { name: "TOS$CONTINUE" });
|
||||
|
||||
expect(continueButton).toBeDisabled();
|
||||
|
||||
await user.click(checkbox);
|
||||
|
||||
expect(continueButton).not.toBeDisabled();
|
||||
});
|
||||
|
||||
it("should set user analytics consent to true when the user accepts TOS", async () => {
|
||||
const handleCaptureConsentSpy = vi.spyOn(
|
||||
CaptureConsent,
|
||||
"handleCaptureConsent",
|
||||
);
|
||||
|
||||
// Mock the API response
|
||||
vi.mocked(openHands.post).mockResolvedValue({
|
||||
data: { redirect_url: "/dashboard" },
|
||||
});
|
||||
|
||||
const user = userEvent.setup();
|
||||
render(<AcceptTOS />);
|
||||
|
||||
const checkbox = screen.getByRole("checkbox");
|
||||
await user.click(checkbox);
|
||||
|
||||
const continueButton = screen.getByRole("button", { name: "TOS$CONTINUE" });
|
||||
await user.click(continueButton);
|
||||
|
||||
expect(handleCaptureConsentSpy).toHaveBeenCalledWith(true);
|
||||
expect(openHands.post).toHaveBeenCalledWith("/api/accept_tos", {
|
||||
redirect_url: "/dashboard",
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle external redirect URLs", async () => {
|
||||
// Mock the API response with an external URL
|
||||
const externalUrl = "https://example.com/callback";
|
||||
vi.mocked(openHands.post).mockResolvedValue({
|
||||
data: { redirect_url: externalUrl },
|
||||
});
|
||||
|
||||
const user = userEvent.setup();
|
||||
render(<AcceptTOS />);
|
||||
|
||||
const checkbox = screen.getByRole("checkbox");
|
||||
await user.click(checkbox);
|
||||
|
||||
const continueButton = screen.getByRole("button", { name: "TOS$CONTINUE" });
|
||||
await user.click(continueButton);
|
||||
|
||||
expect(window.location.href).toBe(externalUrl);
|
||||
});
|
||||
});
|
||||
291
frontend/__tests__/routes/app-settings.test.tsx
Normal file
291
frontend/__tests__/routes/app-settings.test.tsx
Normal file
@@ -0,0 +1,291 @@
|
||||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import AppSettingsScreen from "#/routes/app-settings";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import { MOCK_DEFAULT_USER_SETTINGS } from "#/mocks/handlers";
|
||||
import { AuthProvider } from "#/context/auth-context";
|
||||
import { AvailableLanguages } from "#/i18n";
|
||||
import * as CaptureConsent from "#/utils/handle-capture-consent";
|
||||
import * as ToastHandlers from "#/utils/custom-toast-handlers";
|
||||
|
||||
const renderAppSettingsScreen = () =>
|
||||
render(<AppSettingsScreen />, {
|
||||
wrapper: ({ children }) => (
|
||||
<QueryClientProvider client={new QueryClient()}>
|
||||
<AuthProvider>{children}</AuthProvider>
|
||||
</QueryClientProvider>
|
||||
),
|
||||
});
|
||||
|
||||
describe("Content", () => {
|
||||
it("should render the screen", () => {
|
||||
renderAppSettingsScreen();
|
||||
screen.getByTestId("app-settings-screen");
|
||||
});
|
||||
|
||||
it("should render the correct default values", async () => {
|
||||
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
language: "no",
|
||||
user_consents_to_analytics: true,
|
||||
enable_sound_notifications: true,
|
||||
});
|
||||
|
||||
renderAppSettingsScreen();
|
||||
|
||||
await waitFor(() => {
|
||||
const language = screen.getByTestId("language-input");
|
||||
const analytics = screen.getByTestId("enable-analytics-switch");
|
||||
const sound = screen.getByTestId("enable-sound-notifications-switch");
|
||||
|
||||
expect(language).toHaveValue("Norsk");
|
||||
expect(analytics).toBeChecked();
|
||||
expect(sound).toBeChecked();
|
||||
});
|
||||
});
|
||||
|
||||
it("should render the language options", async () => {
|
||||
renderAppSettingsScreen();
|
||||
|
||||
const language = await screen.findByTestId("language-input");
|
||||
await userEvent.click(language);
|
||||
|
||||
AvailableLanguages.forEach((lang) => {
|
||||
const option = screen.getByText(lang.label);
|
||||
expect(option).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Form submission", () => {
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should submit the form with the correct values", async () => {
|
||||
const saveSettingsSpy = vi.spyOn(OpenHands, "saveSettings");
|
||||
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
|
||||
getSettingsSpy.mockResolvedValue(MOCK_DEFAULT_USER_SETTINGS);
|
||||
|
||||
renderAppSettingsScreen();
|
||||
|
||||
const language = await screen.findByTestId("language-input");
|
||||
const analytics = await screen.findByTestId("enable-analytics-switch");
|
||||
const sound = await screen.findByTestId(
|
||||
"enable-sound-notifications-switch",
|
||||
);
|
||||
|
||||
expect(language).toHaveValue("English");
|
||||
expect(analytics).not.toBeChecked();
|
||||
expect(sound).not.toBeChecked();
|
||||
|
||||
// change language
|
||||
await userEvent.click(language);
|
||||
const norsk = screen.getByText("Norsk");
|
||||
await userEvent.click(norsk);
|
||||
expect(language).toHaveValue("Norsk");
|
||||
|
||||
// toggle options
|
||||
await userEvent.click(analytics);
|
||||
expect(analytics).toBeChecked();
|
||||
await userEvent.click(sound);
|
||||
expect(sound).toBeChecked();
|
||||
|
||||
// submit the form
|
||||
const submit = await screen.findByTestId("submit-button");
|
||||
await userEvent.click(submit);
|
||||
expect(saveSettingsSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
language: "no",
|
||||
user_consents_to_analytics: true,
|
||||
enable_sound_notifications: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("should only enable the submit button when there are changes", async () => {
|
||||
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
|
||||
getSettingsSpy.mockResolvedValue(MOCK_DEFAULT_USER_SETTINGS);
|
||||
|
||||
renderAppSettingsScreen();
|
||||
|
||||
const submit = await screen.findByTestId("submit-button");
|
||||
expect(submit).toBeDisabled();
|
||||
|
||||
// Language check
|
||||
const language = await screen.findByTestId("language-input");
|
||||
await userEvent.click(language);
|
||||
const norsk = screen.getByText("Norsk");
|
||||
await userEvent.click(norsk);
|
||||
expect(submit).not.toBeDisabled();
|
||||
|
||||
await userEvent.click(language);
|
||||
const english = screen.getByText("English");
|
||||
await userEvent.click(english);
|
||||
expect(submit).toBeDisabled();
|
||||
|
||||
// Analytics check
|
||||
const analytics = await screen.findByTestId("enable-analytics-switch");
|
||||
await userEvent.click(analytics);
|
||||
expect(submit).not.toBeDisabled();
|
||||
|
||||
await userEvent.click(analytics);
|
||||
expect(submit).toBeDisabled();
|
||||
|
||||
// Sound check
|
||||
const sound = await screen.findByTestId(
|
||||
"enable-sound-notifications-switch",
|
||||
);
|
||||
await userEvent.click(sound);
|
||||
expect(submit).not.toBeDisabled();
|
||||
|
||||
await userEvent.click(sound);
|
||||
expect(submit).toBeDisabled();
|
||||
});
|
||||
|
||||
it("should call handleCaptureConsents with true when the analytics switch is toggled", async () => {
|
||||
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
|
||||
getSettingsSpy.mockResolvedValue(MOCK_DEFAULT_USER_SETTINGS);
|
||||
|
||||
const handleCaptureConsentsSpy = vi.spyOn(
|
||||
CaptureConsent,
|
||||
"handleCaptureConsent",
|
||||
);
|
||||
|
||||
renderAppSettingsScreen();
|
||||
|
||||
const analytics = await screen.findByTestId("enable-analytics-switch");
|
||||
const submit = await screen.findByTestId("submit-button");
|
||||
|
||||
await userEvent.click(analytics);
|
||||
await userEvent.click(submit);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(handleCaptureConsentsSpy).toHaveBeenCalledWith(true),
|
||||
);
|
||||
});
|
||||
|
||||
it("should call handleCaptureConsents with false when the analytics switch is toggled", async () => {
|
||||
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
user_consents_to_analytics: true,
|
||||
});
|
||||
|
||||
const handleCaptureConsentsSpy = vi.spyOn(
|
||||
CaptureConsent,
|
||||
"handleCaptureConsent",
|
||||
);
|
||||
|
||||
renderAppSettingsScreen();
|
||||
|
||||
const analytics = await screen.findByTestId("enable-analytics-switch");
|
||||
const submit = await screen.findByTestId("submit-button");
|
||||
|
||||
await userEvent.click(analytics);
|
||||
await userEvent.click(submit);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(handleCaptureConsentsSpy).toHaveBeenCalledWith(false),
|
||||
);
|
||||
});
|
||||
|
||||
// flaky test
|
||||
it.skip("should disable the button when submitting changes", async () => {
|
||||
renderAppSettingsScreen();
|
||||
|
||||
const submit = await screen.findByTestId("submit-button");
|
||||
expect(submit).toBeDisabled();
|
||||
|
||||
const sound = await screen.findByTestId(
|
||||
"enable-sound-notifications-switch",
|
||||
);
|
||||
await userEvent.click(sound);
|
||||
expect(submit).not.toBeDisabled();
|
||||
|
||||
// submit the form
|
||||
await userEvent.click(submit);
|
||||
|
||||
expect(submit).toHaveTextContent("Saving...");
|
||||
expect(submit).toBeDisabled();
|
||||
|
||||
await waitFor(() => expect(submit).toHaveTextContent("Save"));
|
||||
});
|
||||
|
||||
it("should disable the button after submitting changes", async () => {
|
||||
const saveSettingsSpy = vi.spyOn(OpenHands, "saveSettings");
|
||||
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
|
||||
getSettingsSpy.mockResolvedValue(MOCK_DEFAULT_USER_SETTINGS);
|
||||
|
||||
renderAppSettingsScreen();
|
||||
|
||||
const submit = await screen.findByTestId("submit-button");
|
||||
expect(submit).toBeDisabled();
|
||||
|
||||
const sound = await screen.findByTestId(
|
||||
"enable-sound-notifications-switch",
|
||||
);
|
||||
await userEvent.click(sound);
|
||||
expect(submit).not.toBeDisabled();
|
||||
|
||||
// submit the form
|
||||
await userEvent.click(submit);
|
||||
expect(saveSettingsSpy).toHaveBeenCalled();
|
||||
|
||||
await waitFor(() => expect(submit).toBeDisabled());
|
||||
});
|
||||
});
|
||||
|
||||
describe("Status toasts", () => {
|
||||
it("should call displaySuccessToast when the settings are saved", async () => {
|
||||
const saveSettingsSpy = vi.spyOn(OpenHands, "saveSettings");
|
||||
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
|
||||
getSettingsSpy.mockResolvedValue(MOCK_DEFAULT_USER_SETTINGS);
|
||||
|
||||
const displaySuccessToastSpy = vi.spyOn(
|
||||
ToastHandlers,
|
||||
"displaySuccessToast",
|
||||
);
|
||||
|
||||
renderAppSettingsScreen();
|
||||
|
||||
// Toggle setting to change
|
||||
const sound = await screen.findByTestId(
|
||||
"enable-sound-notifications-switch",
|
||||
);
|
||||
await userEvent.click(sound);
|
||||
|
||||
const submit = await screen.findByTestId("submit-button");
|
||||
await userEvent.click(submit);
|
||||
|
||||
expect(saveSettingsSpy).toHaveBeenCalled();
|
||||
await waitFor(() => expect(displaySuccessToastSpy).toHaveBeenCalled());
|
||||
});
|
||||
|
||||
it("should call displayErrorToast when the settings fail to save", async () => {
|
||||
const saveSettingsSpy = vi.spyOn(OpenHands, "saveSettings");
|
||||
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
|
||||
getSettingsSpy.mockResolvedValue(MOCK_DEFAULT_USER_SETTINGS);
|
||||
|
||||
const displayErrorToastSpy = vi.spyOn(ToastHandlers, "displayErrorToast");
|
||||
|
||||
saveSettingsSpy.mockRejectedValue(new Error("Failed to save settings"));
|
||||
|
||||
renderAppSettingsScreen();
|
||||
|
||||
// Toggle setting to change
|
||||
const sound = await screen.findByTestId(
|
||||
"enable-sound-notifications-switch",
|
||||
);
|
||||
await userEvent.click(sound);
|
||||
|
||||
const submit = await screen.findByTestId("submit-button");
|
||||
await userEvent.click(submit);
|
||||
|
||||
expect(saveSettingsSpy).toHaveBeenCalled();
|
||||
expect(displayErrorToastSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
461
frontend/__tests__/routes/git-settings.test.tsx
Normal file
461
frontend/__tests__/routes/git-settings.test.tsx
Normal file
@@ -0,0 +1,461 @@
|
||||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import { createRoutesStub } from "react-router";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import GitSettingsScreen from "#/routes/git-settings";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import { MOCK_DEFAULT_USER_SETTINGS } from "#/mocks/handlers";
|
||||
import { AuthProvider } from "#/context/auth-context";
|
||||
import { GetConfigResponse } from "#/api/open-hands.types";
|
||||
import * as ToastHandlers from "#/utils/custom-toast-handlers";
|
||||
|
||||
const VALID_OSS_CONFIG: GetConfigResponse = {
|
||||
APP_MODE: "oss",
|
||||
GITHUB_CLIENT_ID: "123",
|
||||
POSTHOG_CLIENT_KEY: "456",
|
||||
FEATURE_FLAGS: {
|
||||
ENABLE_BILLING: false,
|
||||
HIDE_LLM_SETTINGS: false,
|
||||
},
|
||||
};
|
||||
|
||||
const VALID_SAAS_CONFIG: GetConfigResponse = {
|
||||
APP_MODE: "saas",
|
||||
GITHUB_CLIENT_ID: "123",
|
||||
POSTHOG_CLIENT_KEY: "456",
|
||||
FEATURE_FLAGS: {
|
||||
ENABLE_BILLING: false,
|
||||
HIDE_LLM_SETTINGS: false,
|
||||
},
|
||||
};
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
const GitSettingsRouterStub = createRoutesStub([
|
||||
{
|
||||
Component: GitSettingsScreen,
|
||||
path: "/settings/github",
|
||||
},
|
||||
]);
|
||||
|
||||
const renderGitSettingsScreen = () => {
|
||||
const { rerender, ...rest } = render(
|
||||
<GitSettingsRouterStub initialEntries={["/settings/github"]} />,
|
||||
{
|
||||
wrapper: ({ children }) => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AuthProvider>{children}</AuthProvider>
|
||||
</QueryClientProvider>
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
const rerenderGitSettingsScreen = () =>
|
||||
rerender(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AuthProvider>
|
||||
<GitSettingsRouterStub initialEntries={["/settings/github"]} />
|
||||
</AuthProvider>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
|
||||
return {
|
||||
...rest,
|
||||
rerender: rerenderGitSettingsScreen,
|
||||
};
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
// Since we don't recreate the query client on every test, we need to
|
||||
// reset the query client before each test to avoid state leaks
|
||||
// between tests.
|
||||
queryClient.invalidateQueries();
|
||||
});
|
||||
|
||||
describe("Content", () => {
|
||||
it("should render", async () => {
|
||||
renderGitSettingsScreen();
|
||||
await screen.findByTestId("git-settings-screen");
|
||||
});
|
||||
|
||||
it("should render the inputs if OSS mode", async () => {
|
||||
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
|
||||
getConfigSpy.mockResolvedValue(VALID_OSS_CONFIG);
|
||||
|
||||
const { rerender } = renderGitSettingsScreen();
|
||||
|
||||
await screen.findByTestId("github-token-input");
|
||||
await screen.findByTestId("github-token-help-anchor");
|
||||
|
||||
await screen.findByTestId("gitlab-token-input");
|
||||
await screen.findByTestId("gitlab-token-help-anchor");
|
||||
|
||||
getConfigSpy.mockResolvedValue(VALID_SAAS_CONFIG);
|
||||
queryClient.invalidateQueries();
|
||||
rerender();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.queryByTestId("github-token-input"),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByTestId("github-token-help-anchor"),
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
expect(
|
||||
screen.queryByTestId("gitlab-token-input"),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByTestId("gitlab-token-help-anchor"),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should set '<hidden>' placeholder and indicator if the GitHub token is set", async () => {
|
||||
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
|
||||
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
|
||||
|
||||
getConfigSpy.mockResolvedValue(VALID_OSS_CONFIG);
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
provider_tokens_set: {
|
||||
github: false,
|
||||
gitlab: false,
|
||||
},
|
||||
});
|
||||
|
||||
const { rerender } = renderGitSettingsScreen();
|
||||
|
||||
await waitFor(() => {
|
||||
const githubInput = screen.getByTestId("github-token-input");
|
||||
expect(githubInput).toHaveProperty("placeholder", "");
|
||||
expect(
|
||||
screen.queryByTestId("gh-set-token-indicator"),
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
const gitlabInput = screen.getByTestId("gitlab-token-input");
|
||||
expect(gitlabInput).toHaveProperty("placeholder", "");
|
||||
expect(
|
||||
screen.queryByTestId("gl-set-token-indicator"),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
provider_tokens_set: {
|
||||
github: true,
|
||||
gitlab: true,
|
||||
},
|
||||
});
|
||||
queryClient.invalidateQueries();
|
||||
|
||||
rerender();
|
||||
|
||||
await waitFor(() => {
|
||||
const githubInput = screen.getByTestId("github-token-input");
|
||||
expect(githubInput).toHaveProperty("placeholder", "<hidden>");
|
||||
expect(
|
||||
screen.queryByTestId("gh-set-token-indicator"),
|
||||
).toBeInTheDocument();
|
||||
|
||||
const gitlabInput = screen.getByTestId("gitlab-token-input");
|
||||
expect(gitlabInput).toHaveProperty("placeholder", "<hidden>");
|
||||
expect(
|
||||
screen.queryByTestId("gl-set-token-indicator"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
provider_tokens_set: {
|
||||
github: false,
|
||||
gitlab: true,
|
||||
},
|
||||
});
|
||||
queryClient.invalidateQueries();
|
||||
|
||||
rerender();
|
||||
|
||||
await waitFor(() => {
|
||||
const githubInput = screen.getByTestId("github-token-input");
|
||||
expect(githubInput).toHaveProperty("placeholder", "");
|
||||
expect(
|
||||
screen.queryByTestId("gh-set-token-indicator"),
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
const gitlabInput = screen.getByTestId("gitlab-token-input");
|
||||
expect(gitlabInput).toHaveProperty("placeholder", "<hidden>");
|
||||
expect(
|
||||
screen.queryByTestId("gl-set-token-indicator"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("should render the 'Configure GitHub Repositories' button if SaaS mode and app slug exists", async () => {
|
||||
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
|
||||
getConfigSpy.mockResolvedValue(VALID_OSS_CONFIG);
|
||||
|
||||
const { rerender } = renderGitSettingsScreen();
|
||||
|
||||
let button = screen.queryByTestId("configure-github-repositories-button");
|
||||
expect(button).not.toBeInTheDocument();
|
||||
|
||||
expect(screen.getByTestId("submit-button")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("disconnect-tokens-button")).toBeInTheDocument();
|
||||
|
||||
getConfigSpy.mockResolvedValue(VALID_SAAS_CONFIG);
|
||||
queryClient.invalidateQueries();
|
||||
rerender();
|
||||
|
||||
await waitFor(() => {
|
||||
// wait until queries are resolved
|
||||
expect(queryClient.isFetching()).toBe(0);
|
||||
button = screen.queryByTestId("configure-github-repositories-button");
|
||||
expect(button).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
getConfigSpy.mockResolvedValue({
|
||||
...VALID_SAAS_CONFIG,
|
||||
APP_SLUG: "test-slug",
|
||||
});
|
||||
queryClient.invalidateQueries();
|
||||
rerender();
|
||||
|
||||
await waitFor(() => {
|
||||
button = screen.getByTestId("configure-github-repositories-button");
|
||||
expect(button).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("submit-button")).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByTestId("disconnect-tokens-button"),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Form submission", () => {
|
||||
it("should save the GitHub token", async () => {
|
||||
const saveSettingsSpy = vi.spyOn(OpenHands, "saveSettings");
|
||||
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
|
||||
getConfigSpy.mockResolvedValue(VALID_OSS_CONFIG);
|
||||
|
||||
renderGitSettingsScreen();
|
||||
|
||||
const githubInput = await screen.findByTestId("github-token-input");
|
||||
const submit = await screen.findByTestId("submit-button");
|
||||
|
||||
await userEvent.type(githubInput, "test-token");
|
||||
await userEvent.click(submit);
|
||||
|
||||
expect(saveSettingsSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
provider_tokens: {
|
||||
github: "test-token",
|
||||
gitlab: "",
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const gitlabInput = await screen.findByTestId("gitlab-token-input");
|
||||
await userEvent.type(gitlabInput, "test-token");
|
||||
await userEvent.click(submit);
|
||||
|
||||
expect(saveSettingsSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
provider_tokens: {
|
||||
github: "",
|
||||
gitlab: "test-token",
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("should disable the button if there is no input", async () => {
|
||||
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
|
||||
getConfigSpy.mockResolvedValue(VALID_OSS_CONFIG);
|
||||
|
||||
renderGitSettingsScreen();
|
||||
|
||||
const submit = await screen.findByTestId("submit-button");
|
||||
expect(submit).toBeDisabled();
|
||||
|
||||
const githubInput = await screen.findByTestId("github-token-input");
|
||||
await userEvent.type(githubInput, "test-token");
|
||||
|
||||
expect(submit).not.toBeDisabled();
|
||||
|
||||
await userEvent.clear(githubInput);
|
||||
expect(submit).toBeDisabled();
|
||||
|
||||
const gitlabInput = await screen.findByTestId("gitlab-token-input");
|
||||
await userEvent.type(gitlabInput, "test-token");
|
||||
|
||||
expect(submit).not.toBeDisabled();
|
||||
|
||||
await userEvent.clear(gitlabInput);
|
||||
expect(submit).toBeDisabled();
|
||||
});
|
||||
|
||||
it("should enable a disconnect tokens button if there is at least one token set", async () => {
|
||||
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
|
||||
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
|
||||
|
||||
getConfigSpy.mockResolvedValue(VALID_OSS_CONFIG);
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
provider_tokens_set: {
|
||||
github: true,
|
||||
gitlab: false,
|
||||
},
|
||||
});
|
||||
|
||||
renderGitSettingsScreen();
|
||||
await screen.findByTestId("git-settings-screen");
|
||||
|
||||
let disconnectButton = await screen.findByTestId(
|
||||
"disconnect-tokens-button",
|
||||
);
|
||||
await waitFor(() => expect(disconnectButton).not.toBeDisabled());
|
||||
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
provider_tokens_set: {
|
||||
github: false,
|
||||
gitlab: false,
|
||||
},
|
||||
});
|
||||
queryClient.invalidateQueries();
|
||||
|
||||
disconnectButton = await screen.findByTestId("disconnect-tokens-button");
|
||||
await waitFor(() => expect(disconnectButton).toBeDisabled());
|
||||
});
|
||||
|
||||
it("should call logout when pressing the disconnect tokens button", async () => {
|
||||
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
|
||||
const logoutSpy = vi.spyOn(OpenHands, "logout");
|
||||
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
|
||||
|
||||
getConfigSpy.mockResolvedValue(VALID_OSS_CONFIG);
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
provider_tokens_set: {
|
||||
github: true,
|
||||
gitlab: false,
|
||||
},
|
||||
});
|
||||
|
||||
renderGitSettingsScreen();
|
||||
|
||||
const disconnectButton = await screen.findByTestId(
|
||||
"disconnect-tokens-button",
|
||||
);
|
||||
await waitFor(() => expect(disconnectButton).not.toBeDisabled());
|
||||
await userEvent.click(disconnectButton);
|
||||
|
||||
expect(logoutSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
// flaky test
|
||||
it.skip("should disable the button when submitting changes", async () => {
|
||||
const saveSettingsSpy = vi.spyOn(OpenHands, "saveSettings");
|
||||
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
|
||||
getConfigSpy.mockResolvedValue(VALID_OSS_CONFIG);
|
||||
|
||||
renderGitSettingsScreen();
|
||||
|
||||
const submit = await screen.findByTestId("submit-button");
|
||||
expect(submit).toBeDisabled();
|
||||
|
||||
const githubInput = await screen.findByTestId("github-token-input");
|
||||
await userEvent.type(githubInput, "test-token");
|
||||
expect(submit).not.toBeDisabled();
|
||||
|
||||
// submit the form
|
||||
await userEvent.click(submit);
|
||||
expect(saveSettingsSpy).toHaveBeenCalled();
|
||||
|
||||
expect(submit).toHaveTextContent("Saving...");
|
||||
expect(submit).toBeDisabled();
|
||||
|
||||
await waitFor(() => expect(submit).toHaveTextContent("Save"));
|
||||
});
|
||||
|
||||
it("should disable the button after submitting changes", async () => {
|
||||
const saveSettingsSpy = vi.spyOn(OpenHands, "saveSettings");
|
||||
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
|
||||
getConfigSpy.mockResolvedValue(VALID_OSS_CONFIG);
|
||||
|
||||
renderGitSettingsScreen();
|
||||
await screen.findByTestId("git-settings-screen");
|
||||
|
||||
const submit = await screen.findByTestId("submit-button");
|
||||
expect(submit).toBeDisabled();
|
||||
|
||||
const githubInput = await screen.findByTestId("github-token-input");
|
||||
await userEvent.type(githubInput, "test-token");
|
||||
expect(submit).not.toBeDisabled();
|
||||
|
||||
// submit the form
|
||||
await userEvent.click(submit);
|
||||
expect(saveSettingsSpy).toHaveBeenCalled();
|
||||
expect(submit).toBeDisabled();
|
||||
|
||||
const gitlabInput = await screen.findByTestId("gitlab-token-input");
|
||||
await userEvent.type(gitlabInput, "test-token");
|
||||
expect(gitlabInput).toHaveValue("test-token");
|
||||
expect(submit).not.toBeDisabled();
|
||||
|
||||
// submit the form
|
||||
await userEvent.click(submit);
|
||||
expect(saveSettingsSpy).toHaveBeenCalled();
|
||||
|
||||
await waitFor(() => expect(submit).toBeDisabled());
|
||||
});
|
||||
});
|
||||
|
||||
describe("Status toasts", () => {
|
||||
it("should call displaySuccessToast when the settings are saved", async () => {
|
||||
const saveSettingsSpy = vi.spyOn(OpenHands, "saveSettings");
|
||||
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
|
||||
getSettingsSpy.mockResolvedValue(MOCK_DEFAULT_USER_SETTINGS);
|
||||
|
||||
const displaySuccessToastSpy = vi.spyOn(
|
||||
ToastHandlers,
|
||||
"displaySuccessToast",
|
||||
);
|
||||
|
||||
renderGitSettingsScreen();
|
||||
|
||||
// Toggle setting to change
|
||||
const githubInput = await screen.findByTestId("github-token-input");
|
||||
await userEvent.type(githubInput, "test-token");
|
||||
|
||||
const submit = await screen.findByTestId("submit-button");
|
||||
await userEvent.click(submit);
|
||||
|
||||
expect(saveSettingsSpy).toHaveBeenCalled();
|
||||
await waitFor(() => expect(displaySuccessToastSpy).toHaveBeenCalled());
|
||||
});
|
||||
|
||||
it("should call displayErrorToast when the settings fail to save", async () => {
|
||||
const saveSettingsSpy = vi.spyOn(OpenHands, "saveSettings");
|
||||
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
|
||||
getSettingsSpy.mockResolvedValue(MOCK_DEFAULT_USER_SETTINGS);
|
||||
|
||||
const displayErrorToastSpy = vi.spyOn(ToastHandlers, "displayErrorToast");
|
||||
|
||||
saveSettingsSpy.mockRejectedValue(new Error("Failed to save settings"));
|
||||
|
||||
renderGitSettingsScreen();
|
||||
|
||||
// Toggle setting to change
|
||||
const gitlabInput = await screen.findByTestId("gitlab-token-input");
|
||||
await userEvent.type(gitlabInput, "test-token");
|
||||
|
||||
const submit = await screen.findByTestId("submit-button");
|
||||
await userEvent.click(submit);
|
||||
|
||||
expect(saveSettingsSpy).toHaveBeenCalled();
|
||||
expect(displayErrorToastSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -91,6 +91,13 @@ describe("HomeScreen", () => {
|
||||
screen.getByTestId("task-suggestions");
|
||||
});
|
||||
|
||||
it("should have responsive layout for mobile and desktop screens", async () => {
|
||||
renderHomeScreen();
|
||||
|
||||
const mainContainer = screen.getByTestId("home-screen").querySelector("main");
|
||||
expect(mainContainer).toHaveClass("flex", "flex-col", "md:flex-row");
|
||||
});
|
||||
|
||||
it("should filter the suggested tasks based on the selected repository", async () => {
|
||||
const retrieveUserGitRepositoriesSpy = vi.spyOn(
|
||||
GitService,
|
||||
|
||||
674
frontend/__tests__/routes/llm-settings.test.tsx
Normal file
674
frontend/__tests__/routes/llm-settings.test.tsx
Normal file
@@ -0,0 +1,674 @@
|
||||
import { render, screen, waitFor, within } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { QueryClientProvider, QueryClient } from "@tanstack/react-query";
|
||||
import LlmSettingsScreen from "#/routes/llm-settings";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import {
|
||||
MOCK_DEFAULT_USER_SETTINGS,
|
||||
resetTestHandlersMockSettings,
|
||||
} from "#/mocks/handlers";
|
||||
import { AuthProvider } from "#/context/auth-context";
|
||||
import * as AdvancedSettingsUtlls from "#/utils/has-advanced-settings-set";
|
||||
import * as ToastHandlers from "#/utils/custom-toast-handlers";
|
||||
|
||||
const renderLlmSettingsScreen = () =>
|
||||
render(<LlmSettingsScreen />, {
|
||||
wrapper: ({ children }) => (
|
||||
<QueryClientProvider client={new QueryClient()}>
|
||||
<AuthProvider>{children}</AuthProvider>
|
||||
</QueryClientProvider>
|
||||
),
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
resetTestHandlersMockSettings();
|
||||
});
|
||||
|
||||
describe("Content", () => {
|
||||
describe("Basic form", () => {
|
||||
it("should render the basic form by default", async () => {
|
||||
renderLlmSettingsScreen();
|
||||
await screen.findByTestId("llm-settings-screen");
|
||||
|
||||
const basicFom = screen.getByTestId("llm-settings-form-basic");
|
||||
within(basicFom).getByTestId("llm-provider-input");
|
||||
within(basicFom).getByTestId("llm-model-input");
|
||||
within(basicFom).getByTestId("llm-api-key-input");
|
||||
within(basicFom).getByTestId("llm-api-key-help-anchor");
|
||||
});
|
||||
|
||||
it("should render the default values if non exist", async () => {
|
||||
renderLlmSettingsScreen();
|
||||
await screen.findByTestId("llm-settings-screen");
|
||||
|
||||
const provider = screen.getByTestId("llm-provider-input");
|
||||
const model = screen.getByTestId("llm-model-input");
|
||||
const apiKey = screen.getByTestId("llm-api-key-input");
|
||||
|
||||
await waitFor(() => {
|
||||
expect(provider).toHaveValue("Anthropic");
|
||||
expect(model).toHaveValue("claude-3-5-sonnet-20241022");
|
||||
|
||||
expect(apiKey).toHaveValue("");
|
||||
expect(apiKey).toHaveProperty("placeholder", "");
|
||||
});
|
||||
});
|
||||
|
||||
it("should render the existing settings values", async () => {
|
||||
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
llm_model: "openai/gpt-4o",
|
||||
llm_api_key_set: true,
|
||||
});
|
||||
|
||||
renderLlmSettingsScreen();
|
||||
await screen.findByTestId("llm-settings-screen");
|
||||
|
||||
const provider = screen.getByTestId("llm-provider-input");
|
||||
const model = screen.getByTestId("llm-model-input");
|
||||
const apiKey = screen.getByTestId("llm-api-key-input");
|
||||
|
||||
await waitFor(() => {
|
||||
expect(provider).toHaveValue("OpenAI");
|
||||
expect(model).toHaveValue("gpt-4o");
|
||||
|
||||
expect(apiKey).toHaveValue("");
|
||||
expect(apiKey).toHaveProperty("placeholder", "<hidden>");
|
||||
expect(screen.getByTestId("set-indicator")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Advanced form", () => {
|
||||
it("should render the advanced form if the switch is toggled", async () => {
|
||||
renderLlmSettingsScreen();
|
||||
await screen.findByTestId("llm-settings-screen");
|
||||
|
||||
const advancedSwitch = screen.getByTestId("advanced-settings-switch");
|
||||
const basicForm = screen.getByTestId("llm-settings-form-basic");
|
||||
|
||||
expect(
|
||||
screen.queryByTestId("llm-settings-form-advanced"),
|
||||
).not.toBeInTheDocument();
|
||||
expect(basicForm).toBeInTheDocument();
|
||||
|
||||
await userEvent.click(advancedSwitch);
|
||||
|
||||
expect(
|
||||
screen.queryByTestId("llm-settings-form-advanced"),
|
||||
).toBeInTheDocument();
|
||||
expect(basicForm).not.toBeInTheDocument();
|
||||
|
||||
const advancedForm = screen.getByTestId("llm-settings-form-advanced");
|
||||
within(advancedForm).getByTestId("llm-custom-model-input");
|
||||
within(advancedForm).getByTestId("base-url-input");
|
||||
within(advancedForm).getByTestId("llm-api-key-input");
|
||||
within(advancedForm).getByTestId("llm-api-key-help-anchor");
|
||||
within(advancedForm).getByTestId("agent-input");
|
||||
within(advancedForm).getByTestId("enable-confirmation-mode-switch");
|
||||
within(advancedForm).getByTestId("enable-memory-condenser-switch");
|
||||
|
||||
await userEvent.click(advancedSwitch);
|
||||
expect(
|
||||
screen.queryByTestId("llm-settings-form-advanced"),
|
||||
).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId("llm-settings-form-basic")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render the default advanced settings", async () => {
|
||||
renderLlmSettingsScreen();
|
||||
await screen.findByTestId("llm-settings-screen");
|
||||
|
||||
const advancedSwitch = screen.getByTestId("advanced-settings-switch");
|
||||
expect(advancedSwitch).not.toBeChecked();
|
||||
|
||||
await userEvent.click(advancedSwitch);
|
||||
|
||||
const model = screen.getByTestId("llm-custom-model-input");
|
||||
const baseUrl = screen.getByTestId("base-url-input");
|
||||
const apiKey = screen.getByTestId("llm-api-key-input");
|
||||
const agent = screen.getByTestId("agent-input");
|
||||
const confirmation = screen.getByTestId(
|
||||
"enable-confirmation-mode-switch",
|
||||
);
|
||||
const condensor = screen.getByTestId("enable-memory-condenser-switch");
|
||||
|
||||
expect(model).toHaveValue("anthropic/claude-3-5-sonnet-20241022");
|
||||
expect(baseUrl).toHaveValue("");
|
||||
expect(apiKey).toHaveValue("");
|
||||
expect(apiKey).toHaveProperty("placeholder", "");
|
||||
expect(agent).toHaveValue("CodeActAgent");
|
||||
expect(confirmation).not.toBeChecked();
|
||||
expect(condensor).toBeChecked();
|
||||
|
||||
// check that security analyzer is present
|
||||
expect(
|
||||
screen.queryByTestId("security-analyzer-input"),
|
||||
).not.toBeInTheDocument();
|
||||
await userEvent.click(confirmation);
|
||||
screen.getByTestId("security-analyzer-input");
|
||||
});
|
||||
|
||||
it("should render the advanced form if existings settings are advanced", async () => {
|
||||
const hasAdvancedSettingsSetSpy = vi.spyOn(
|
||||
AdvancedSettingsUtlls,
|
||||
"hasAdvancedSettingsSet",
|
||||
);
|
||||
hasAdvancedSettingsSetSpy.mockReturnValue(true);
|
||||
|
||||
renderLlmSettingsScreen();
|
||||
|
||||
await waitFor(() => {
|
||||
const advancedSwitch = screen.getByTestId("advanced-settings-switch");
|
||||
expect(advancedSwitch).toBeChecked();
|
||||
screen.getByTestId("llm-settings-form-advanced");
|
||||
});
|
||||
});
|
||||
|
||||
it("should render existing advanced settings correctly", async () => {
|
||||
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
llm_model: "openai/gpt-4o",
|
||||
llm_base_url: "https://api.openai.com/v1/chat/completions",
|
||||
llm_api_key_set: true,
|
||||
agent: "CoActAgent",
|
||||
confirmation_mode: true,
|
||||
enable_default_condenser: false,
|
||||
security_analyzer: "mock-invariant",
|
||||
});
|
||||
|
||||
renderLlmSettingsScreen();
|
||||
await screen.findByTestId("llm-settings-screen");
|
||||
|
||||
const model = screen.getByTestId("llm-custom-model-input");
|
||||
const baseUrl = screen.getByTestId("base-url-input");
|
||||
const apiKey = screen.getByTestId("llm-api-key-input");
|
||||
const agent = screen.getByTestId("agent-input");
|
||||
const confirmation = screen.getByTestId(
|
||||
"enable-confirmation-mode-switch",
|
||||
);
|
||||
const condensor = screen.getByTestId("enable-memory-condenser-switch");
|
||||
const securityAnalyzer = screen.getByTestId("security-analyzer-input");
|
||||
|
||||
await waitFor(() => {
|
||||
expect(model).toHaveValue("openai/gpt-4o");
|
||||
expect(baseUrl).toHaveValue(
|
||||
"https://api.openai.com/v1/chat/completions",
|
||||
);
|
||||
expect(apiKey).toHaveValue("");
|
||||
expect(apiKey).toHaveProperty("placeholder", "<hidden>");
|
||||
expect(agent).toHaveValue("CoActAgent");
|
||||
expect(confirmation).toBeChecked();
|
||||
expect(condensor).not.toBeChecked();
|
||||
expect(securityAnalyzer).toHaveValue("mock-invariant");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it.todo("should render an indicator if the llm api key is set");
|
||||
});
|
||||
|
||||
describe("Form submission", () => {
|
||||
it("should submit the basic form with the correct values", async () => {
|
||||
const saveSettingsSpy = vi.spyOn(OpenHands, "saveSettings");
|
||||
|
||||
renderLlmSettingsScreen();
|
||||
await screen.findByTestId("llm-settings-screen");
|
||||
|
||||
const provider = screen.getByTestId("llm-provider-input");
|
||||
const model = screen.getByTestId("llm-model-input");
|
||||
const apiKey = screen.getByTestId("llm-api-key-input");
|
||||
|
||||
// select provider
|
||||
await userEvent.click(provider);
|
||||
const providerOption = screen.getByText("OpenAI");
|
||||
await userEvent.click(providerOption);
|
||||
expect(provider).toHaveValue("OpenAI");
|
||||
|
||||
// enter api key
|
||||
await userEvent.type(apiKey, "test-api-key");
|
||||
|
||||
// select model
|
||||
await userEvent.click(model);
|
||||
const modelOption = screen.getByText("gpt-4o");
|
||||
await userEvent.click(modelOption);
|
||||
expect(model).toHaveValue("gpt-4o");
|
||||
|
||||
const submitButton = screen.getByTestId("submit-button");
|
||||
await userEvent.click(submitButton);
|
||||
|
||||
expect(saveSettingsSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
llm_model: "openai/gpt-4o",
|
||||
llm_api_key: "test-api-key",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("should submit the advanced form with the correct values", async () => {
|
||||
const saveSettingsSpy = vi.spyOn(OpenHands, "saveSettings");
|
||||
|
||||
renderLlmSettingsScreen();
|
||||
await screen.findByTestId("llm-settings-screen");
|
||||
|
||||
const advancedSwitch = screen.getByTestId("advanced-settings-switch");
|
||||
await userEvent.click(advancedSwitch);
|
||||
|
||||
const model = screen.getByTestId("llm-custom-model-input");
|
||||
const baseUrl = screen.getByTestId("base-url-input");
|
||||
const apiKey = screen.getByTestId("llm-api-key-input");
|
||||
const agent = screen.getByTestId("agent-input");
|
||||
const confirmation = screen.getByTestId("enable-confirmation-mode-switch");
|
||||
const condensor = screen.getByTestId("enable-memory-condenser-switch");
|
||||
|
||||
// enter custom model
|
||||
await userEvent.clear(model);
|
||||
await userEvent.type(model, "openai/gpt-4o");
|
||||
expect(model).toHaveValue("openai/gpt-4o");
|
||||
|
||||
// enter base url
|
||||
await userEvent.type(baseUrl, "https://api.openai.com/v1/chat/completions");
|
||||
expect(baseUrl).toHaveValue("https://api.openai.com/v1/chat/completions");
|
||||
|
||||
// enter api key
|
||||
await userEvent.type(apiKey, "test-api-key");
|
||||
|
||||
// toggle confirmation mode
|
||||
await userEvent.click(confirmation);
|
||||
expect(confirmation).toBeChecked();
|
||||
|
||||
// toggle memory condensor
|
||||
await userEvent.click(condensor);
|
||||
expect(condensor).not.toBeChecked();
|
||||
|
||||
// select agent
|
||||
await userEvent.click(agent);
|
||||
const agentOption = screen.getByText("CoActAgent");
|
||||
await userEvent.click(agentOption);
|
||||
expect(agent).toHaveValue("CoActAgent");
|
||||
|
||||
// select security analyzer
|
||||
const securityAnalyzer = screen.getByTestId("security-analyzer-input");
|
||||
await userEvent.click(securityAnalyzer);
|
||||
const securityAnalyzerOption = screen.getByText("mock-invariant");
|
||||
await userEvent.click(securityAnalyzerOption);
|
||||
|
||||
const submitButton = screen.getByTestId("submit-button");
|
||||
await userEvent.click(submitButton);
|
||||
|
||||
expect(saveSettingsSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
llm_model: "openai/gpt-4o",
|
||||
llm_base_url: "https://api.openai.com/v1/chat/completions",
|
||||
agent: "CoActAgent",
|
||||
confirmation_mode: true,
|
||||
enable_default_condenser: false,
|
||||
security_analyzer: "mock-invariant",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("should disable the button if there are no changes in the basic form", async () => {
|
||||
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
llm_model: "openai/gpt-4o",
|
||||
llm_api_key_set: true,
|
||||
});
|
||||
|
||||
renderLlmSettingsScreen();
|
||||
await screen.findByTestId("llm-settings-screen");
|
||||
screen.getByTestId("llm-settings-form-basic");
|
||||
|
||||
const submitButton = screen.getByTestId("submit-button");
|
||||
expect(submitButton).toBeDisabled();
|
||||
|
||||
const model = screen.getByTestId("llm-model-input");
|
||||
const apiKey = screen.getByTestId("llm-api-key-input");
|
||||
|
||||
// select model
|
||||
await userEvent.click(model);
|
||||
const modelOption = screen.getByText("gpt-4o-mini");
|
||||
await userEvent.click(modelOption);
|
||||
expect(model).toHaveValue("gpt-4o-mini");
|
||||
expect(submitButton).not.toBeDisabled();
|
||||
|
||||
// reset model
|
||||
await userEvent.click(model);
|
||||
const modelOption2 = screen.getByText("gpt-4o");
|
||||
await userEvent.click(modelOption2);
|
||||
expect(model).toHaveValue("gpt-4o");
|
||||
expect(submitButton).toBeDisabled();
|
||||
|
||||
// set api key
|
||||
await userEvent.type(apiKey, "test-api-key");
|
||||
expect(apiKey).toHaveValue("test-api-key");
|
||||
expect(submitButton).not.toBeDisabled();
|
||||
|
||||
// reset api key
|
||||
await userEvent.clear(apiKey);
|
||||
expect(apiKey).toHaveValue("");
|
||||
expect(submitButton).toBeDisabled();
|
||||
});
|
||||
|
||||
it("should disable the button if there are no changes in the advanced form", async () => {
|
||||
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
llm_model: "openai/gpt-4o",
|
||||
llm_base_url: "https://api.openai.com/v1/chat/completions",
|
||||
llm_api_key_set: true,
|
||||
confirmation_mode: true,
|
||||
});
|
||||
|
||||
renderLlmSettingsScreen();
|
||||
await screen.findByTestId("llm-settings-screen");
|
||||
screen.getByTestId("llm-settings-form-advanced");
|
||||
|
||||
const submitButton = screen.getByTestId("submit-button");
|
||||
expect(submitButton).toBeDisabled();
|
||||
|
||||
const model = screen.getByTestId("llm-custom-model-input");
|
||||
const baseUrl = screen.getByTestId("base-url-input");
|
||||
const apiKey = screen.getByTestId("llm-api-key-input");
|
||||
const agent = screen.getByTestId("agent-input");
|
||||
const confirmation = screen.getByTestId("enable-confirmation-mode-switch");
|
||||
const condensor = screen.getByTestId("enable-memory-condenser-switch");
|
||||
|
||||
// enter custom model
|
||||
await userEvent.type(model, "-mini");
|
||||
expect(model).toHaveValue("openai/gpt-4o-mini");
|
||||
expect(submitButton).not.toBeDisabled();
|
||||
|
||||
// reset model
|
||||
await userEvent.clear(model);
|
||||
expect(model).toHaveValue("");
|
||||
expect(submitButton).toBeDisabled();
|
||||
|
||||
await userEvent.type(model, "openai/gpt-4o");
|
||||
expect(model).toHaveValue("openai/gpt-4o");
|
||||
expect(submitButton).toBeDisabled();
|
||||
|
||||
// enter base url
|
||||
await userEvent.type(baseUrl, "/extra");
|
||||
expect(baseUrl).toHaveValue(
|
||||
"https://api.openai.com/v1/chat/completions/extra",
|
||||
);
|
||||
expect(submitButton).not.toBeDisabled();
|
||||
|
||||
await userEvent.clear(baseUrl);
|
||||
expect(baseUrl).toHaveValue("");
|
||||
expect(submitButton).not.toBeDisabled();
|
||||
|
||||
await userEvent.type(baseUrl, "https://api.openai.com/v1/chat/completions");
|
||||
expect(baseUrl).toHaveValue("https://api.openai.com/v1/chat/completions");
|
||||
expect(submitButton).toBeDisabled();
|
||||
|
||||
// set api key
|
||||
await userEvent.type(apiKey, "test-api-key");
|
||||
expect(apiKey).toHaveValue("test-api-key");
|
||||
expect(submitButton).not.toBeDisabled();
|
||||
|
||||
// reset api key
|
||||
await userEvent.clear(apiKey);
|
||||
expect(apiKey).toHaveValue("");
|
||||
expect(submitButton).toBeDisabled();
|
||||
|
||||
// set agent
|
||||
await userEvent.clear(agent);
|
||||
await userEvent.type(agent, "test-agent");
|
||||
expect(agent).toHaveValue("test-agent");
|
||||
expect(submitButton).not.toBeDisabled();
|
||||
|
||||
// reset agent
|
||||
await userEvent.clear(agent);
|
||||
expect(agent).toHaveValue("");
|
||||
expect(submitButton).toBeDisabled();
|
||||
|
||||
await userEvent.type(agent, "CodeActAgent");
|
||||
expect(agent).toHaveValue("CodeActAgent");
|
||||
expect(submitButton).toBeDisabled();
|
||||
|
||||
// toggle confirmation mode
|
||||
await userEvent.click(confirmation);
|
||||
expect(confirmation).not.toBeChecked();
|
||||
expect(submitButton).not.toBeDisabled();
|
||||
await userEvent.click(confirmation);
|
||||
expect(confirmation).toBeChecked();
|
||||
expect(submitButton).toBeDisabled();
|
||||
|
||||
// toggle memory condensor
|
||||
await userEvent.click(condensor);
|
||||
expect(condensor).not.toBeChecked();
|
||||
expect(submitButton).not.toBeDisabled();
|
||||
await userEvent.click(condensor);
|
||||
expect(condensor).toBeChecked();
|
||||
expect(submitButton).toBeDisabled();
|
||||
|
||||
// select security analyzer
|
||||
const securityAnalyzer = screen.getByTestId("security-analyzer-input");
|
||||
await userEvent.click(securityAnalyzer);
|
||||
const securityAnalyzerOption = screen.getByText("mock-invariant");
|
||||
await userEvent.click(securityAnalyzerOption);
|
||||
expect(securityAnalyzer).toHaveValue("mock-invariant");
|
||||
|
||||
expect(submitButton).not.toBeDisabled();
|
||||
|
||||
await userEvent.clear(securityAnalyzer);
|
||||
expect(securityAnalyzer).toHaveValue("");
|
||||
expect(submitButton).toBeDisabled();
|
||||
});
|
||||
|
||||
it("should reset button state when switching between forms", async () => {
|
||||
renderLlmSettingsScreen();
|
||||
await screen.findByTestId("llm-settings-screen");
|
||||
|
||||
const advancedSwitch = screen.getByTestId("advanced-settings-switch");
|
||||
const submitButton = screen.getByTestId("submit-button");
|
||||
|
||||
expect(submitButton).toBeDisabled();
|
||||
|
||||
// dirty the basic form
|
||||
const apiKey = screen.getByTestId("llm-api-key-input");
|
||||
await userEvent.type(apiKey, "test-api-key");
|
||||
expect(submitButton).not.toBeDisabled();
|
||||
|
||||
await userEvent.click(advancedSwitch);
|
||||
expect(submitButton).toBeDisabled();
|
||||
|
||||
// dirty the advanced form
|
||||
const model = screen.getByTestId("llm-custom-model-input");
|
||||
await userEvent.type(model, "openai/gpt-4o");
|
||||
expect(submitButton).not.toBeDisabled();
|
||||
|
||||
await userEvent.click(advancedSwitch);
|
||||
expect(submitButton).toBeDisabled();
|
||||
});
|
||||
|
||||
// flaky test
|
||||
it.skip("should disable the button when submitting changes", async () => {
|
||||
const saveSettingsSpy = vi.spyOn(OpenHands, "saveSettings");
|
||||
|
||||
renderLlmSettingsScreen();
|
||||
await screen.findByTestId("llm-settings-screen");
|
||||
|
||||
const apiKey = screen.getByTestId("llm-api-key-input");
|
||||
await userEvent.type(apiKey, "test-api-key");
|
||||
|
||||
const submitButton = screen.getByTestId("submit-button");
|
||||
await userEvent.click(submitButton);
|
||||
|
||||
expect(saveSettingsSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
llm_api_key: "test-api-key",
|
||||
}),
|
||||
);
|
||||
|
||||
expect(submitButton).toHaveTextContent("Saving...");
|
||||
expect(submitButton).toBeDisabled();
|
||||
|
||||
await waitFor(() => {
|
||||
expect(submitButton).toHaveTextContent("Save");
|
||||
expect(submitButton).toBeDisabled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Status toasts", () => {
|
||||
describe("Basic form", () => {
|
||||
it("should call displaySuccessToast when the settings are saved", async () => {
|
||||
const saveSettingsSpy = vi.spyOn(OpenHands, "saveSettings");
|
||||
|
||||
const displaySuccessToastSpy = vi.spyOn(
|
||||
ToastHandlers,
|
||||
"displaySuccessToast",
|
||||
);
|
||||
|
||||
renderLlmSettingsScreen();
|
||||
|
||||
// Toggle setting to change
|
||||
const apiKeyInput = await screen.findByTestId("llm-api-key-input");
|
||||
await userEvent.type(apiKeyInput, "test-api-key");
|
||||
|
||||
const submit = await screen.findByTestId("submit-button");
|
||||
await userEvent.click(submit);
|
||||
|
||||
expect(saveSettingsSpy).toHaveBeenCalled();
|
||||
await waitFor(() => expect(displaySuccessToastSpy).toHaveBeenCalled());
|
||||
});
|
||||
|
||||
it("should call displayErrorToast when the settings fail to save", async () => {
|
||||
const saveSettingsSpy = vi.spyOn(OpenHands, "saveSettings");
|
||||
|
||||
const displayErrorToastSpy = vi.spyOn(ToastHandlers, "displayErrorToast");
|
||||
|
||||
saveSettingsSpy.mockRejectedValue(new Error("Failed to save settings"));
|
||||
|
||||
renderLlmSettingsScreen();
|
||||
|
||||
// Toggle setting to change
|
||||
const apiKeyInput = await screen.findByTestId("llm-api-key-input");
|
||||
await userEvent.type(apiKeyInput, "test-api-key");
|
||||
|
||||
const submit = await screen.findByTestId("submit-button");
|
||||
await userEvent.click(submit);
|
||||
|
||||
expect(saveSettingsSpy).toHaveBeenCalled();
|
||||
expect(displayErrorToastSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Advanced form", () => {
|
||||
it("should call displaySuccessToast when the settings are saved", async () => {
|
||||
const saveSettingsSpy = vi.spyOn(OpenHands, "saveSettings");
|
||||
|
||||
const displaySuccessToastSpy = vi.spyOn(
|
||||
ToastHandlers,
|
||||
"displaySuccessToast",
|
||||
);
|
||||
|
||||
renderLlmSettingsScreen();
|
||||
await screen.findByTestId("llm-settings-screen");
|
||||
|
||||
const advancedSwitch = screen.getByTestId("advanced-settings-switch");
|
||||
await userEvent.click(advancedSwitch);
|
||||
await screen.findByTestId("llm-settings-form-advanced");
|
||||
|
||||
// Toggle setting to change
|
||||
const apiKeyInput = await screen.findByTestId("llm-api-key-input");
|
||||
await userEvent.type(apiKeyInput, "test-api-key");
|
||||
|
||||
const submit = await screen.findByTestId("submit-button");
|
||||
await userEvent.click(submit);
|
||||
|
||||
expect(saveSettingsSpy).toHaveBeenCalled();
|
||||
await waitFor(() => expect(displaySuccessToastSpy).toHaveBeenCalled());
|
||||
});
|
||||
|
||||
it("should call displayErrorToast when the settings fail to save", async () => {
|
||||
const saveSettingsSpy = vi.spyOn(OpenHands, "saveSettings");
|
||||
|
||||
const displayErrorToastSpy = vi.spyOn(ToastHandlers, "displayErrorToast");
|
||||
|
||||
saveSettingsSpy.mockRejectedValue(new Error("Failed to save settings"));
|
||||
|
||||
renderLlmSettingsScreen();
|
||||
await screen.findByTestId("llm-settings-screen");
|
||||
|
||||
const advancedSwitch = screen.getByTestId("advanced-settings-switch");
|
||||
await userEvent.click(advancedSwitch);
|
||||
await screen.findByTestId("llm-settings-form-advanced");
|
||||
|
||||
// Toggle setting to change
|
||||
const apiKeyInput = await screen.findByTestId("llm-api-key-input");
|
||||
await userEvent.type(apiKeyInput, "test-api-key");
|
||||
|
||||
const submit = await screen.findByTestId("submit-button");
|
||||
await userEvent.click(submit);
|
||||
|
||||
expect(saveSettingsSpy).toHaveBeenCalled();
|
||||
expect(displayErrorToastSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("SaaS mode", () => {
|
||||
it("should not render the runtime settings input in oss mode", async () => {
|
||||
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
|
||||
// @ts-expect-error - only return mode
|
||||
getConfigSpy.mockResolvedValue({
|
||||
APP_MODE: "oss",
|
||||
});
|
||||
|
||||
renderLlmSettingsScreen();
|
||||
await screen.findByTestId("llm-settings-screen");
|
||||
|
||||
const advancedSwitch = screen.getByTestId("advanced-settings-switch");
|
||||
await userEvent.click(advancedSwitch);
|
||||
await screen.findByTestId("llm-settings-form-advanced");
|
||||
|
||||
const runtimeSettingsInput = screen.queryByTestId("runtime-settings-input");
|
||||
expect(runtimeSettingsInput).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render the runtime settings input in saas mode", async () => {
|
||||
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
|
||||
// @ts-expect-error - only return mode
|
||||
getConfigSpy.mockResolvedValue({
|
||||
APP_MODE: "saas",
|
||||
});
|
||||
|
||||
renderLlmSettingsScreen();
|
||||
await screen.findByTestId("llm-settings-screen");
|
||||
|
||||
const advancedSwitch = screen.getByTestId("advanced-settings-switch");
|
||||
await userEvent.click(advancedSwitch);
|
||||
await screen.findByTestId("llm-settings-form-advanced");
|
||||
|
||||
const runtimeSettingsInput = screen.queryByTestId("runtime-settings-input");
|
||||
expect(runtimeSettingsInput).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should always render the runtime settings input as disabled", async () => {
|
||||
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
|
||||
// @ts-expect-error - only return mode
|
||||
getConfigSpy.mockResolvedValue({
|
||||
APP_MODE: "saas",
|
||||
});
|
||||
|
||||
renderLlmSettingsScreen();
|
||||
await screen.findByTestId("llm-settings-screen");
|
||||
|
||||
const advancedSwitch = screen.getByTestId("advanced-settings-switch");
|
||||
await userEvent.click(advancedSwitch);
|
||||
await screen.findByTestId("llm-settings-form-advanced");
|
||||
|
||||
const runtimeSettingsInput = screen.queryByTestId("runtime-settings-input");
|
||||
expect(runtimeSettingsInput).toBeInTheDocument();
|
||||
expect(runtimeSettingsInput).toBeDisabled();
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import { screen, waitFor, within } from "@testing-library/react";
|
||||
import { screen, within } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { createRoutesStub } from "react-router";
|
||||
@@ -7,6 +7,30 @@ import OpenHands from "#/api/open-hands";
|
||||
import SettingsScreen from "#/routes/settings";
|
||||
import { PaymentForm } from "#/components/features/payment/payment-form";
|
||||
|
||||
// Mock the i18next hook
|
||||
vi.mock("react-i18next", async () => {
|
||||
const actual = await vi.importActual<typeof import("react-i18next")>("react-i18next");
|
||||
return {
|
||||
...actual,
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => {
|
||||
const translations: Record<string, string> = {
|
||||
"SETTINGS$NAV_GIT": "Git",
|
||||
"SETTINGS$NAV_APPLICATION": "Application",
|
||||
"SETTINGS$NAV_CREDITS": "Credits",
|
||||
"SETTINGS$NAV_API_KEYS": "API Keys",
|
||||
"SETTINGS$NAV_LLM": "LLM",
|
||||
"SETTINGS$TITLE": "Settings"
|
||||
};
|
||||
return translations[key] || key;
|
||||
},
|
||||
i18n: {
|
||||
changeLanguage: vi.fn(),
|
||||
},
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
describe("Settings Billing", () => {
|
||||
const getConfigSpy = vi.spyOn(OpenHands, "getConfig");
|
||||
|
||||
@@ -19,18 +43,22 @@ describe("Settings Billing", () => {
|
||||
Component: () => <PaymentForm />,
|
||||
path: "/settings/billing",
|
||||
},
|
||||
{
|
||||
Component: () => <div data-testid="git-settings-screen" />,
|
||||
path: "/settings/git",
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
const renderSettingsScreen = () =>
|
||||
renderWithProviders(<RoutesStub initialEntries={["/settings"]} />);
|
||||
renderWithProviders(<RoutesStub initialEntries={["/settings/billing"]} />);
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should not render the navbar if OSS mode", async () => {
|
||||
it("should not render the credits tab if OSS mode", async () => {
|
||||
getConfigSpy.mockResolvedValue({
|
||||
APP_MODE: "oss",
|
||||
GITHUB_CLIENT_ID: "123",
|
||||
@@ -43,15 +71,12 @@ describe("Settings Billing", () => {
|
||||
|
||||
renderSettingsScreen();
|
||||
|
||||
// Wait for the settings screen to be rendered
|
||||
await screen.findByTestId("settings-screen");
|
||||
|
||||
// Then check that the navbar is not present
|
||||
const navbar = screen.queryByTestId("settings-navbar");
|
||||
expect(navbar).not.toBeInTheDocument();
|
||||
const navbar = await screen.findByTestId("settings-navbar");
|
||||
const credits = within(navbar).queryByText("Credits");
|
||||
expect(credits).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render the navbar if SaaS mode", async () => {
|
||||
it("should render the credits tab if SaaS mode and billing is enabled", async () => {
|
||||
getConfigSpy.mockResolvedValue({
|
||||
APP_MODE: "saas",
|
||||
GITHUB_CLIENT_ID: "123",
|
||||
@@ -64,11 +89,8 @@ describe("Settings Billing", () => {
|
||||
|
||||
renderSettingsScreen();
|
||||
|
||||
await waitFor(() => {
|
||||
const navbar = screen.getByTestId("settings-navbar");
|
||||
within(navbar).getByText("Account");
|
||||
within(navbar).getByText("Credits");
|
||||
});
|
||||
const navbar = await screen.findByTestId("settings-navbar");
|
||||
within(navbar).getByText("Credits");
|
||||
});
|
||||
|
||||
it("should render the billing settings if clicking the credits item", async () => {
|
||||
@@ -90,6 +112,6 @@ describe("Settings Billing", () => {
|
||||
await user.click(credits);
|
||||
|
||||
const billingSection = await screen.findByTestId("billing-settings");
|
||||
within(billingSection).getByText("PAYMENT$MANAGE_CREDITS");
|
||||
expect(billingSection).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -11,30 +11,35 @@ const rawTasks: SuggestedTask[] = [
|
||||
repo: "repo1",
|
||||
title: "Task 1",
|
||||
task_type: "MERGE_CONFLICTS",
|
||||
git_provider: "github",
|
||||
},
|
||||
{
|
||||
issue_number: 2,
|
||||
repo: "repo1",
|
||||
title: "Task 2",
|
||||
task_type: "FAILING_CHECKS",
|
||||
git_provider: "github",
|
||||
},
|
||||
{
|
||||
issue_number: 3,
|
||||
repo: "repo2",
|
||||
title: "Task 3",
|
||||
task_type: "UNRESOLVED_COMMENTS",
|
||||
git_provider: "github",
|
||||
},
|
||||
{
|
||||
issue_number: 4,
|
||||
repo: "repo2",
|
||||
title: "Task 4",
|
||||
task_type: "OPEN_ISSUE",
|
||||
git_provider: "github",
|
||||
},
|
||||
{
|
||||
issue_number: 5,
|
||||
repo: "repo3",
|
||||
title: "Task 5",
|
||||
task_type: "FAILING_CHECKS",
|
||||
git_provider: "github",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -47,12 +52,14 @@ const groupedTasks: SuggestedTaskGroup[] = [
|
||||
repo: "repo1",
|
||||
title: "Task 1",
|
||||
task_type: "MERGE_CONFLICTS",
|
||||
git_provider: "github",
|
||||
},
|
||||
{
|
||||
issue_number: 2,
|
||||
repo: "repo1",
|
||||
title: "Task 2",
|
||||
task_type: "FAILING_CHECKS",
|
||||
git_provider: "github",
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -64,12 +71,14 @@ const groupedTasks: SuggestedTaskGroup[] = [
|
||||
repo: "repo2",
|
||||
title: "Task 3",
|
||||
task_type: "UNRESOLVED_COMMENTS",
|
||||
git_provider: "github",
|
||||
},
|
||||
{
|
||||
issue_number: 4,
|
||||
repo: "repo2",
|
||||
title: "Task 4",
|
||||
task_type: "OPEN_ISSUE",
|
||||
git_provider: "github",
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -81,6 +90,7 @@ const groupedTasks: SuggestedTaskGroup[] = [
|
||||
repo: "repo3",
|
||||
title: "Task 5",
|
||||
task_type: "FAILING_CHECKS",
|
||||
git_provider: "github",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -7,6 +7,10 @@ describe("hasAdvancedSettingsSet", () => {
|
||||
expect(hasAdvancedSettingsSet(DEFAULT_SETTINGS)).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false if an empty object", () => {
|
||||
expect(hasAdvancedSettingsSet({})).toBe(false);
|
||||
});
|
||||
|
||||
describe("should be true if", () => {
|
||||
test("LLM_BASE_URL is set", () => {
|
||||
expect(
|
||||
@@ -26,15 +30,6 @@ describe("hasAdvancedSettingsSet", () => {
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test("REMOTE_RUNTIME_RESOURCE_FACTOR is not default value", () => {
|
||||
expect(
|
||||
hasAdvancedSettingsSet({
|
||||
...DEFAULT_SETTINGS,
|
||||
REMOTE_RUNTIME_RESOURCE_FACTOR: 999,
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test("CONFIRMATION_MODE is true", () => {
|
||||
expect(
|
||||
hasAdvancedSettingsSet({
|
||||
|
||||
822
frontend/package-lock.json
generated
822
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -10,45 +10,44 @@
|
||||
"@heroui/react": "2.7.6",
|
||||
"@microlink/react-json-view": "^1.26.1",
|
||||
"@monaco-editor/react": "^4.7.0-rc.0",
|
||||
"@react-router/node": "^7.5.1",
|
||||
"@react-router/serve": "^7.5.1",
|
||||
"@react-router/node": "^7.5.2",
|
||||
"@react-router/serve": "^7.5.2",
|
||||
"@react-types/shared": "^3.29.0",
|
||||
"@reduxjs/toolkit": "^2.7.0",
|
||||
"@stripe/react-stripe-js": "^3.6.0",
|
||||
"@stripe/stripe-js": "^7.1.0",
|
||||
"@tanstack/react-query": "^5.74.4",
|
||||
"@stripe/stripe-js": "^7.2.0",
|
||||
"@tanstack/react-query": "^5.74.7",
|
||||
"@vitejs/plugin-react": "^4.4.0",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/xterm": "^5.4.0",
|
||||
"axios": "^1.8.4",
|
||||
"axios": "^1.9.0",
|
||||
"clsx": "^2.1.1",
|
||||
"eslint-config-airbnb-typescript": "^18.0.0",
|
||||
"framer-motion": "^12.7.4",
|
||||
"i18next": "^25.0.0",
|
||||
"framer-motion": "^12.9.2",
|
||||
"i18next": "^25.0.1",
|
||||
"i18next-browser-languagedetector": "^8.0.5",
|
||||
"i18next-http-backend": "^3.0.2",
|
||||
"isbot": "^5.1.25",
|
||||
"isbot": "^5.1.27",
|
||||
"jose": "^6.0.10",
|
||||
"lucide-react": "^0.501.0",
|
||||
"lucide-react": "^0.503.0",
|
||||
"monaco-editor": "^0.52.2",
|
||||
"posthog-js": "^1.236.2",
|
||||
"posthog-js": "^1.236.7",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-highlight": "^0.15.0",
|
||||
"react-hot-toast": "^2.5.1",
|
||||
"react-i18next": "^15.4.1",
|
||||
"react-i18next": "^15.5.1",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-redux": "^9.2.0",
|
||||
"react-router": "^7.5.1",
|
||||
"react-router": "^7.5.2",
|
||||
"react-syntax-highlighter": "^15.6.1",
|
||||
"react-textarea-autosize": "^8.5.9",
|
||||
"react-toastify": "^11.0.5",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"sirv-cli": "^3.0.1",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"tailwind-merge": "^3.2.0",
|
||||
"vite": "^6.3.2",
|
||||
"vite": "^6.3.3",
|
||||
"web-vitals": "^3.5.2",
|
||||
"ws": "^8.18.1"
|
||||
},
|
||||
@@ -83,14 +82,14 @@
|
||||
"@babel/types": "^7.27.0",
|
||||
"@mswjs/socket.io-binding": "^0.1.1",
|
||||
"@playwright/test": "^1.52.0",
|
||||
"@react-router/dev": "^7.5.1",
|
||||
"@react-router/dev": "^7.5.2",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@tanstack/eslint-plugin-query": "^5.73.3",
|
||||
"@tanstack/eslint-plugin-query": "^5.74.7",
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
"@testing-library/jest-dom": "^6.6.1",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/node": "^22.14.1",
|
||||
"@types/node": "^22.15.3",
|
||||
"@types/react": "^19.1.2",
|
||||
"@types/react-dom": "^19.1.1",
|
||||
"@types/react-highlight": "^0.12.8",
|
||||
@@ -98,7 +97,7 @@
|
||||
"@types/ws": "^8.18.1",
|
||||
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
||||
"@typescript-eslint/parser": "^7.18.0",
|
||||
"@vitest/coverage-v8": "^3.1.1",
|
||||
"@vitest/coverage-v8": "^3.1.2",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.57.0",
|
||||
|
||||
@@ -39,6 +39,7 @@ const IGNORE_PATHS = [
|
||||
"entry.client.tsx", // Client entry point
|
||||
"utils/scan-unlocalized-strings.ts", // Original scanner
|
||||
"utils/scan-unlocalized-strings-ast.ts", // This file itself
|
||||
"frontend/src/components/features/home/tasks/get-prompt-for-query.ts", // Only contains agent prompts
|
||||
];
|
||||
|
||||
// Extensions to scan
|
||||
@@ -106,6 +107,10 @@ function isRawTranslationKey(str) {
|
||||
const EXCLUDED_TECHNICAL_STRINGS = [
|
||||
"openid email profile", // OAuth scope string - not user-facing
|
||||
"OPEN_ISSUE", // Task type identifier, not a UI string
|
||||
"Merge Request", // Git provider specific terminology
|
||||
"GitLab API", // Git provider specific terminology
|
||||
"Pull Request", // Git provider specific terminology
|
||||
"GitHub API", // Git provider specific terminology
|
||||
];
|
||||
|
||||
function isExcludedTechnicalString(str) {
|
||||
|
||||
@@ -10,10 +10,12 @@ import {
|
||||
GetTrajectoryResponse,
|
||||
GitChangeDiff,
|
||||
GitChange,
|
||||
ConversationTrigger,
|
||||
} from "./open-hands.types";
|
||||
import { openHands } from "./open-hands-axios";
|
||||
import { ApiSettings, PostApiSettings } from "#/types/settings";
|
||||
import { GitUser, GitRepository } from "#/types/git";
|
||||
import { SuggestedTask } from "#/components/features/home/tasks/task.types";
|
||||
|
||||
class OpenHands {
|
||||
/**
|
||||
@@ -149,17 +151,21 @@ class OpenHands {
|
||||
}
|
||||
|
||||
static async createConversation(
|
||||
conversation_trigger: ConversationTrigger = "gui",
|
||||
selectedRepository?: GitRepository,
|
||||
initialUserMsg?: string,
|
||||
imageUrls?: string[],
|
||||
replayJson?: string,
|
||||
suggested_task?: SuggestedTask,
|
||||
): Promise<Conversation> {
|
||||
const body = {
|
||||
conversation_trigger,
|
||||
selected_repository: selectedRepository,
|
||||
selected_branch: undefined,
|
||||
initial_user_msg: initialUserMsg,
|
||||
image_urls: imageUrls,
|
||||
replay_json: replayJson,
|
||||
suggested_task,
|
||||
};
|
||||
|
||||
const { data } = await openHands.post<Conversation>(
|
||||
@@ -199,14 +205,6 @@ class OpenHands {
|
||||
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",
|
||||
|
||||
@@ -70,7 +70,7 @@ export interface AuthenticateResponse {
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export type ConversationTrigger = "resolver" | "gui";
|
||||
export type ConversationTrigger = "resolver" | "gui" | "suggested_task";
|
||||
|
||||
export interface Conversation {
|
||||
conversation_id: string;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DiffEditor } from "@monaco-editor/react";
|
||||
import { DiffEditor, Monaco } from "@monaco-editor/react";
|
||||
import React from "react";
|
||||
import { editor as editor_t } from "monaco-editor";
|
||||
import { LuFileDiff, LuFileMinus, LuFilePlus } from "react-icons/lu";
|
||||
@@ -88,6 +88,29 @@ export function FileDiffViewer({ path, type }: FileDiffViewerProps) {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const beforeMount = (monaco: Monaco) => {
|
||||
monaco.editor.defineTheme("custom-diff-theme", {
|
||||
base: "vs-dark",
|
||||
inherit: true,
|
||||
rules: [
|
||||
{ token: "comment", foreground: "6a9955" },
|
||||
{ token: "keyword", foreground: "569cd6" },
|
||||
{ token: "string", foreground: "ce9178" },
|
||||
{ token: "number", foreground: "b5cea8" },
|
||||
],
|
||||
colors: {
|
||||
"diffEditor.insertedTextBackground": "#014b01AA", // Stronger green background
|
||||
"diffEditor.removedTextBackground": "#750000AA", // Stronger red background
|
||||
"diffEditor.insertedLineBackground": "#003f00AA", // Dark green for added lines
|
||||
"diffEditor.removedLineBackground": "#5a0000AA", // Dark red for removed lines
|
||||
"diffEditor.border": "#444444", // Border between diff editors
|
||||
|
||||
"editorUnnecessaryCode.border": "#00000000", // No border for unnecessary code
|
||||
"editorUnnecessaryCode.opacity": "#00000077", // Slightly faded
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleEditorDidMount = (editor: editor_t.IStandaloneDiffEditor) => {
|
||||
diffEditorRef.current = editor;
|
||||
updateEditorHeight();
|
||||
@@ -145,8 +168,9 @@ export function FileDiffViewer({ path, type }: FileDiffViewerProps) {
|
||||
language={getLanguageFromPath(filePath)}
|
||||
original={isAdded ? "" : diff.original}
|
||||
modified={isDeleted ? "" : diff.modified}
|
||||
theme="vs-dark"
|
||||
theme="custom-diff-theme"
|
||||
onMount={handleEditorDidMount}
|
||||
beforeMount={beforeMount}
|
||||
options={{
|
||||
renderValidationDecorations: "off",
|
||||
readOnly: true,
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import TreeNode from "./tree-node";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
|
||||
interface ExplorerTreeProps {
|
||||
files: string[] | null;
|
||||
defaultOpen?: boolean;
|
||||
}
|
||||
|
||||
export function ExplorerTree({
|
||||
files,
|
||||
defaultOpen = false,
|
||||
}: ExplorerTreeProps) {
|
||||
const { t } = useTranslation();
|
||||
if (!files?.length) {
|
||||
const message = !files
|
||||
? I18nKey.EXPLORER$LOADING_WORKSPACE_MESSAGE
|
||||
: I18nKey.EXPLORER$EMPTY_WORKSPACE_MESSAGE;
|
||||
return <div className="text-sm text-gray-400 pt-4">{t(message)}</div>;
|
||||
}
|
||||
return (
|
||||
<div className="w-full h-full pt-[4px]">
|
||||
{files.map((file) => (
|
||||
<TreeNode key={file} path={file} defaultOpen={defaultOpen} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import { RefreshIconButton } from "#/components/shared/buttons/refresh-icon-button";
|
||||
import { ToggleWorkspaceIconButton } from "#/components/shared/buttons/toggle-workspace-icon-button";
|
||||
import { cn } from "#/utils/utils";
|
||||
|
||||
interface ExplorerActionsProps {
|
||||
onRefresh: () => void;
|
||||
toggleHidden: () => void;
|
||||
isHidden: boolean;
|
||||
}
|
||||
|
||||
export function ExplorerActions({
|
||||
toggleHidden,
|
||||
onRefresh,
|
||||
isHidden,
|
||||
}: ExplorerActionsProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-[24px] items-center gap-1",
|
||||
isHidden ? "right-3" : "right-2",
|
||||
)}
|
||||
>
|
||||
{!isHidden && <RefreshIconButton onClick={onRefresh} />}
|
||||
|
||||
<ToggleWorkspaceIconButton isHidden={isHidden} onClick={toggleHidden} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { cn } from "#/utils/utils";
|
||||
import { ExplorerActions } from "./file-explorer-actions";
|
||||
|
||||
interface FileExplorerHeaderProps {
|
||||
isOpen: boolean;
|
||||
onToggle: () => void;
|
||||
onRefreshWorkspace: () => void;
|
||||
}
|
||||
|
||||
export function FileExplorerHeader({
|
||||
isOpen,
|
||||
onToggle,
|
||||
onRefreshWorkspace,
|
||||
}: FileExplorerHeaderProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"sticky top-0 bg-base-secondary",
|
||||
"flex items-center",
|
||||
!isOpen ? "justify-center" : "justify-between",
|
||||
)}
|
||||
>
|
||||
{isOpen && (
|
||||
<div className="text-neutral-300 font-bold text-sm">
|
||||
{t(I18nKey.EXPLORER$LABEL_WORKSPACE)}
|
||||
</div>
|
||||
)}
|
||||
<ExplorerActions
|
||||
isHidden={!isOpen}
|
||||
toggleHidden={onToggle}
|
||||
onRefresh={onRefreshWorkspace}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
import React from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { RUNTIME_INACTIVE_STATES } from "#/types/agent-state";
|
||||
import { ExplorerTree } from "#/components/features/file-explorer/explorer-tree";
|
||||
import toast from "#/utils/toast";
|
||||
import { RootState } from "#/store";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { useListFiles } from "#/hooks/query/use-list-files";
|
||||
import { cn } from "#/utils/utils";
|
||||
import { FileExplorerHeader } from "./file-explorer-header";
|
||||
import { useVSCodeUrl } from "#/hooks/query/use-vscode-url";
|
||||
import { BrandButton } from "../settings/brand-button";
|
||||
import VSCodeIcon from "#/assets/vscode-alt.svg?react";
|
||||
|
||||
interface FileExplorerProps {
|
||||
isOpen: boolean;
|
||||
onToggle: () => void;
|
||||
}
|
||||
|
||||
export function FileExplorer({ isOpen, onToggle }: FileExplorerProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { curAgentState } = useSelector((state: RootState) => state.agent);
|
||||
|
||||
const { data: paths, refetch, error } = useListFiles();
|
||||
const { data: vscodeUrl } = useVSCodeUrl({
|
||||
enabled: !RUNTIME_INACTIVE_STATES.includes(curAgentState),
|
||||
});
|
||||
|
||||
const handleOpenVSCode = () => {
|
||||
if (vscodeUrl?.vscode_url) {
|
||||
window.open(vscodeUrl.vscode_url, "_blank");
|
||||
} else if (vscodeUrl?.error) {
|
||||
toast.error(
|
||||
`open-vscode-error-${new Date().getTime()}`,
|
||||
t(I18nKey.EXPLORER$VSCODE_SWITCHING_ERROR_MESSAGE, {
|
||||
error: vscodeUrl.error,
|
||||
}),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const refreshWorkspace = () => {
|
||||
if (!RUNTIME_INACTIVE_STATES.includes(curAgentState)) {
|
||||
refetch();
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
refreshWorkspace();
|
||||
}, [curAgentState]);
|
||||
|
||||
return (
|
||||
<div data-testid="file-explorer" className="relative h-full">
|
||||
<div
|
||||
className={cn(
|
||||
"bg-base-secondary h-full border-r-1 border-r-neutral-600 flex flex-col",
|
||||
!isOpen ? "w-12" : "w-60",
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col relative h-full px-3 py-2 overflow-hidden">
|
||||
<FileExplorerHeader
|
||||
isOpen={isOpen}
|
||||
onToggle={onToggle}
|
||||
onRefreshWorkspace={refreshWorkspace}
|
||||
/>
|
||||
{!error && (
|
||||
<div className="overflow-auto flex-grow min-h-0">
|
||||
<div style={{ display: !isOpen ? "none" : "block" }}>
|
||||
<ExplorerTree files={paths || []} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{error && (
|
||||
<div className="flex flex-col items-center justify-center h-full">
|
||||
<p className="text-neutral-300 text-sm">{error.message}</p>
|
||||
</div>
|
||||
)}
|
||||
{isOpen && (
|
||||
<BrandButton
|
||||
testId="open-vscode-button"
|
||||
type="button"
|
||||
variant="secondary"
|
||||
className="w-full text-content border-content"
|
||||
isDisabled={RUNTIME_INACTIVE_STATES.includes(curAgentState)}
|
||||
onClick={handleOpenVSCode}
|
||||
startContent={<VSCodeIcon width={20} height={20} />}
|
||||
>
|
||||
{t(I18nKey.VSCODE$OPEN)}
|
||||
</BrandButton>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import { FaFile } from "react-icons/fa";
|
||||
import { getExtension } from "#/utils/utils";
|
||||
import { EXTENSION_ICON_MAP } from "../../extension-icon-map.constant";
|
||||
|
||||
interface FileIconProps {
|
||||
filename: string;
|
||||
}
|
||||
|
||||
export function FileIcon({ filename }: FileIconProps) {
|
||||
const extension = getExtension(filename);
|
||||
return EXTENSION_ICON_MAP[extension] || <FaFile />;
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import { FolderIcon } from "./folder-icon";
|
||||
import { FileIcon } from "./file-icon";
|
||||
|
||||
interface FilenameProps {
|
||||
name: string;
|
||||
type: "folder" | "file";
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
export function Filename({ name, type, isOpen }: FilenameProps) {
|
||||
return (
|
||||
<div className="cursor-pointer text-nowrap rounded-[5px] p-1 nowrap flex items-center gap-2 aria-selected:bg-neutral-600 aria-selected:text-white hover:text-white">
|
||||
<div className="flex-shrink-0">
|
||||
{type === "folder" && <FolderIcon isOpen={isOpen} />}
|
||||
{type === "file" && <FileIcon filename={name} />}
|
||||
</div>
|
||||
<div className="flex-grow">{name}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { FaFolder, FaFolderOpen } from "react-icons/fa";
|
||||
|
||||
interface FolderIconProps {
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
export function FolderIcon({ isOpen }: FolderIconProps) {
|
||||
return isOpen ? (
|
||||
<FaFolderOpen color="D9D3D0" className="icon" />
|
||||
) : (
|
||||
<FaFolder color="D9D3D0" className="icon" />
|
||||
);
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
import React from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
import { useFiles } from "#/context/files";
|
||||
import { cn } from "#/utils/utils";
|
||||
import { useListFiles } from "#/hooks/query/use-list-files";
|
||||
import { useListFile } from "#/hooks/query/use-list-file";
|
||||
import { Filename } from "./filename";
|
||||
import { RootState } from "#/store";
|
||||
|
||||
interface TreeNodeProps {
|
||||
path: string;
|
||||
defaultOpen?: boolean;
|
||||
}
|
||||
|
||||
function TreeNode({ path, defaultOpen = false }: TreeNodeProps) {
|
||||
const { setFileContent, setSelectedPath, files, selectedPath } = useFiles();
|
||||
const [isOpen, setIsOpen] = React.useState(defaultOpen);
|
||||
const { curAgentState } = useSelector((state: RootState) => state.agent);
|
||||
|
||||
const isDirectory = path.endsWith("/");
|
||||
|
||||
const { data: paths } = useListFiles({
|
||||
path,
|
||||
enabled: isDirectory && isOpen,
|
||||
});
|
||||
|
||||
const { data: fileContent, refetch } = useListFile({ path });
|
||||
|
||||
React.useEffect(() => {
|
||||
if (fileContent) {
|
||||
if (fileContent !== files[path]) {
|
||||
setFileContent(path, fileContent);
|
||||
}
|
||||
}
|
||||
}, [fileContent, path]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (selectedPath === path && !isDirectory) {
|
||||
refetch();
|
||||
}
|
||||
}, [curAgentState, selectedPath, path, isDirectory]);
|
||||
|
||||
const fileParts = path.split("/");
|
||||
const filename =
|
||||
fileParts[fileParts.length - 1] || fileParts[fileParts.length - 2];
|
||||
|
||||
const handleClick = async () => {
|
||||
if (isDirectory) setIsOpen((prev) => !prev);
|
||||
else {
|
||||
setSelectedPath(path);
|
||||
await refetch();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"text-sm text-neutral-400",
|
||||
path === selectedPath && "bg-gray-700",
|
||||
)}
|
||||
>
|
||||
<button
|
||||
type={isDirectory ? "button" : "submit"}
|
||||
name="file"
|
||||
value={path}
|
||||
onClick={handleClick}
|
||||
className="flex items-center justify-between w-full px-1"
|
||||
>
|
||||
<Filename
|
||||
name={filename}
|
||||
type={isDirectory ? "folder" : "file"}
|
||||
isOpen={isOpen}
|
||||
/>
|
||||
</button>
|
||||
|
||||
{isOpen && paths && (
|
||||
<div className="ml-5">
|
||||
{paths.map((child, index) => (
|
||||
<TreeNode key={index} path={child} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(TreeNode);
|
||||
@@ -1,33 +0,0 @@
|
||||
import React from "react";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { useCreateConversation } from "#/hooks/mutation/use-create-conversation";
|
||||
import { setInitialPrompt } from "#/state/initial-query-slice";
|
||||
|
||||
const INITIAL_PROMPT = "";
|
||||
|
||||
export function CodeNotInGitLink() {
|
||||
const dispatch = useDispatch();
|
||||
const { t } = useTranslation();
|
||||
const { mutate: createConversation } = useCreateConversation();
|
||||
|
||||
const handleStartFromScratch = () => {
|
||||
// Set the initial prompt and create a new conversation
|
||||
dispatch(setInitialPrompt(INITIAL_PROMPT));
|
||||
createConversation({ q: INITIAL_PROMPT });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="text-xs text-neutral-400">
|
||||
{t(I18nKey.GITHUB$CODE_NOT_IN_GITHUB)}{" "}
|
||||
<span
|
||||
onClick={handleStartFromScratch}
|
||||
className="underline cursor-pointer"
|
||||
>
|
||||
{t(I18nKey.GITHUB$START_FROM_SCRATCH)}
|
||||
</span>{" "}
|
||||
{t(I18nKey.GITHUB$VSCODE_LINK_DESCRIPTION)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -28,7 +28,7 @@ export function HomeHeader() {
|
||||
testId="header-launch-button"
|
||||
variant="primary"
|
||||
type="button"
|
||||
onClick={() => createConversation({})}
|
||||
onClick={() => createConversation({ conversation_trigger: "gui" })}
|
||||
isDisabled={isCreatingConversation}
|
||||
>
|
||||
{!isCreatingConversation && "Launch from Scratch"}
|
||||
|
||||
@@ -142,7 +142,12 @@ export function RepositorySelectionForm({
|
||||
isLoadingRepositories ||
|
||||
isRepositoriesError
|
||||
}
|
||||
onClick={() => createConversation({ selectedRepository })}
|
||||
onClick={() =>
|
||||
createConversation({
|
||||
selectedRepository,
|
||||
conversation_trigger: "gui",
|
||||
})
|
||||
}
|
||||
>
|
||||
{!isCreatingConversation && "Launch"}
|
||||
{isCreatingConversation && t("HOME$LOADING")}
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
import { SuggestedTaskType } from "./task.types";
|
||||
|
||||
export const getMergeConflictPrompt = (
|
||||
issueNumber: number,
|
||||
repo: string,
|
||||
) => `You are working on Pull Request #${issueNumber} in repository ${repo}. You need to fix the merge conflicts.
|
||||
Use the GitHub API to retrieve the PR details. Check out the branch from that pull request and look at the diff versus the base branch of the PR to understand the PR's intention.
|
||||
Then resolve the merge conflicts. If you aren't sure what the right solution is, look back through the commit history at the commits that introduced the conflict and resolve them accordingly.`;
|
||||
|
||||
export const getFailingChecksPrompt = (
|
||||
issueNumber: number,
|
||||
repo: string,
|
||||
) => `You are working on Pull Request #${issueNumber} in repository ${repo}. You need to fix the failing CI checks.
|
||||
Use the GitHub API to retrieve the PR details. Check out the branch from that pull request and look at the diff versus the base branch of the PR to understand the PR's intention.
|
||||
Then use the GitHub API to look at the GitHub Actions that are failing on the most recent commit. Try and reproduce the failure locally.
|
||||
Get things working locally, then push your changes. Sleep for 30 seconds at a time until the GitHub actions have run again. If they are still failing, repeat the process.`;
|
||||
|
||||
export const getUnresolvedCommentsPrompt = (
|
||||
issueNumber: number,
|
||||
repo: string,
|
||||
) => `You are working on Pull Request #${issueNumber} in repository ${repo}. You need to resolve the remaining comments from reviewers.
|
||||
Use the GitHub API to retrieve the PR details. Check out the branch from that pull request and look at the diff versus the base branch of the PR to understand the PR's intention.
|
||||
Then use the GitHub API to retrieve all the feedback on the PR so far. If anything hasn't been addressed, address it and commit your changes back to the same branch.`;
|
||||
|
||||
export const getOpenIssuePrompt = (
|
||||
issueNumber: number,
|
||||
repo: string,
|
||||
) => `You are working on Issue #${issueNumber} in repository ${repo}. Your goal is to fix the issue
|
||||
Use the GitHub API to retrieve the issue details and any comments on the issue. Then check out a new branch and investigate what changes will need to be made
|
||||
Finally, make the required changes and open up a pull request. Be sure to reference the issue in the PR description`;
|
||||
|
||||
export const getPromptForQuery = (
|
||||
type: SuggestedTaskType,
|
||||
issueNumber: number,
|
||||
repo: string,
|
||||
) => {
|
||||
switch (type) {
|
||||
case "MERGE_CONFLICTS":
|
||||
return getMergeConflictPrompt(issueNumber, repo);
|
||||
case "FAILING_CHECKS":
|
||||
return getFailingChecksPrompt(issueNumber, repo);
|
||||
case "UNRESOLVED_COMMENTS":
|
||||
return getUnresolvedCommentsPrompt(issueNumber, repo);
|
||||
case "OPEN_ISSUE":
|
||||
return getOpenIssuePrompt(issueNumber, repo);
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
};
|
||||
@@ -4,8 +4,8 @@ import { useIsCreatingConversation } from "#/hooks/use-is-creating-conversation"
|
||||
import { useCreateConversation } from "#/hooks/mutation/use-create-conversation";
|
||||
import { cn } from "#/utils/utils";
|
||||
import { useUserRepositories } from "#/hooks/query/use-user-repositories";
|
||||
import { getPromptForQuery } from "./get-prompt-for-query";
|
||||
import { TaskIssueNumber } from "./task-issue-number";
|
||||
import { Provider } from "#/types/settings";
|
||||
|
||||
const getTaskTypeMap = (
|
||||
t: (key: string) => string,
|
||||
@@ -26,31 +26,37 @@ export function TaskCard({ task }: TaskCardProps) {
|
||||
const isCreatingConversation = useIsCreatingConversation();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const getRepo = (repo: string) => {
|
||||
const getRepo = (repo: string, git_provider: Provider) => {
|
||||
const repositoriesList = repositories?.pages.flatMap((page) => page.data);
|
||||
const selectedRepo = repositoriesList?.find(
|
||||
(repository) => repository.full_name === repo,
|
||||
(repository) =>
|
||||
repository.full_name === repo &&
|
||||
repository.git_provider === git_provider,
|
||||
);
|
||||
|
||||
return selectedRepo;
|
||||
};
|
||||
|
||||
const handleLaunchConversation = () => {
|
||||
const repo = getRepo(task.repo);
|
||||
const query = getPromptForQuery(
|
||||
task.task_type,
|
||||
task.issue_number,
|
||||
task.repo,
|
||||
);
|
||||
const repo = getRepo(task.repo, task.git_provider);
|
||||
|
||||
return createConversation({
|
||||
conversation_trigger: "suggested_task",
|
||||
selectedRepository: repo,
|
||||
q: query,
|
||||
suggested_task: task,
|
||||
});
|
||||
};
|
||||
|
||||
const hrefType = task.task_type === "OPEN_ISSUE" ? "issues" : "pull";
|
||||
const href = `https://github.com/${task.repo}/${hrefType}/${task.issue_number}`;
|
||||
// Determine the correct URL format based on git provider
|
||||
let href: string;
|
||||
if (task.git_provider === "gitlab") {
|
||||
const issueType =
|
||||
task.task_type === "OPEN_ISSUE" ? "issues" : "merge_requests";
|
||||
href = `https://gitlab.com/${task.repo}/-/${issueType}/${task.issue_number}`;
|
||||
} else {
|
||||
const hrefType = task.task_type === "OPEN_ISSUE" ? "issues" : "pull";
|
||||
href = `https://github.com/${task.repo}/${hrefType}/${task.issue_number}`;
|
||||
}
|
||||
|
||||
return (
|
||||
<li className="py-3 border-b border-[#717888] flex items-center pr-6">
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { Provider } from "#/types/settings";
|
||||
|
||||
export type SuggestedTaskType =
|
||||
| "MERGE_CONFLICTS"
|
||||
| "FAILING_CHECKS"
|
||||
@@ -5,6 +7,7 @@ export type SuggestedTaskType =
|
||||
| "OPEN_ISSUE"; // This is a task type identifier, not a UI string
|
||||
|
||||
export interface SuggestedTask {
|
||||
git_provider: Provider;
|
||||
issue_number: number;
|
||||
repo: string;
|
||||
title: string;
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { InputSkeleton } from "../input-skeleton";
|
||||
import { SwitchSkeleton } from "../switch-skeleton";
|
||||
|
||||
export function AppSettingsInputsSkeleton() {
|
||||
return (
|
||||
<div
|
||||
data-testid="app-settings-skeleton"
|
||||
className="px-11 py-9 flex flex-col gap-6"
|
||||
>
|
||||
<InputSkeleton />
|
||||
<SwitchSkeleton />
|
||||
<SwitchSkeleton />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { AvailableLanguages } from "#/i18n";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { SettingsDropdownInput } from "../settings-dropdown-input";
|
||||
|
||||
interface LanguageInputProps {
|
||||
name: string;
|
||||
onChange: (value: string) => void;
|
||||
defaultKey: string;
|
||||
}
|
||||
|
||||
export function LanguageInput({
|
||||
defaultKey,
|
||||
onChange,
|
||||
name,
|
||||
}: LanguageInputProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<SettingsDropdownInput
|
||||
testId={name}
|
||||
name={name}
|
||||
onInputChange={onChange}
|
||||
label={t(I18nKey.SETTINGS$LANGUAGE)}
|
||||
items={AvailableLanguages.map((l) => ({
|
||||
key: l.value,
|
||||
label: l.label,
|
||||
}))}
|
||||
defaultSelectedKey={defaultKey}
|
||||
isClearable={false}
|
||||
wrapperClassName="w-[680px]"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { cn } from "#/utils/utils";
|
||||
|
||||
interface BrandButtonProps {
|
||||
testId?: string;
|
||||
name?: string;
|
||||
variant: "primary" | "secondary" | "danger";
|
||||
type: React.ButtonHTMLAttributes<HTMLButtonElement>["type"];
|
||||
isDisabled?: boolean;
|
||||
@@ -12,6 +13,7 @@ interface BrandButtonProps {
|
||||
|
||||
export function BrandButton({
|
||||
testId,
|
||||
name,
|
||||
children,
|
||||
variant,
|
||||
type,
|
||||
@@ -22,6 +24,7 @@ export function BrandButton({
|
||||
}: React.PropsWithChildren<BrandButtonProps>) {
|
||||
return (
|
||||
<button
|
||||
name={name}
|
||||
data-testid={testId}
|
||||
disabled={isDisabled}
|
||||
// The type is alreadt passed as a prop to the button component
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { BrandButton } from "../brand-button";
|
||||
|
||||
interface ConfigureGitHubRepositoriesAnchorProps {
|
||||
slug: string;
|
||||
}
|
||||
|
||||
export function ConfigureGitHubRepositoriesAnchor({
|
||||
slug,
|
||||
}: ConfigureGitHubRepositoriesAnchorProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<a
|
||||
data-testid="configure-github-repositories-button"
|
||||
href={`https://github.com/apps/${slug}/installations/new`}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
className="px-11 py-9"
|
||||
>
|
||||
<BrandButton type="button" variant="secondary">
|
||||
{t(I18nKey.GITHUB$CONFIGURE_REPOS)}
|
||||
</BrandButton>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { InputSkeleton } from "../input-skeleton";
|
||||
import { SubtextSkeleton } from "../subtext-skeleton";
|
||||
|
||||
export function GitSettingInputsSkeleton() {
|
||||
return (
|
||||
<div className="px-11 py-9 flex flex-col gap-12">
|
||||
<div className="flex flex-col gap-6">
|
||||
<InputSkeleton />
|
||||
<SubtextSkeleton />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-6">
|
||||
<InputSkeleton />
|
||||
<SubtextSkeleton />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Trans } from "react-i18next";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
|
||||
export function GitHubTokenHelpAnchor() {
|
||||
return (
|
||||
<p data-testid="github-token-help-anchor" className="text-xs">
|
||||
<Trans
|
||||
i18nKey={I18nKey.GITHUB$TOKEN_HELP_TEXT}
|
||||
components={[
|
||||
<a
|
||||
key="github-token-help-anchor-link"
|
||||
aria-label="GitHub token help link"
|
||||
href="https://github.com/settings/tokens/new?description=openhands-app&scopes=repo,user,workflow"
|
||||
target="_blank"
|
||||
className="underline underline-offset-2"
|
||||
rel="noopener noreferrer"
|
||||
/>,
|
||||
<a
|
||||
key="github-token-help-anchor-link-2"
|
||||
aria-label="GitHub token see more link"
|
||||
href="https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token"
|
||||
target="_blank"
|
||||
className="underline underline-offset-2"
|
||||
rel="noopener noreferrer"
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
</p>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { SettingsInput } from "../settings-input";
|
||||
import { GitHubTokenHelpAnchor } from "./github-token-help-anchor";
|
||||
import { KeyStatusIcon } from "../key-status-icon";
|
||||
|
||||
interface GitHubTokenInputProps {
|
||||
onChange: (value: string) => void;
|
||||
isGitHubTokenSet: boolean;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export function GitHubTokenInput({
|
||||
onChange,
|
||||
isGitHubTokenSet,
|
||||
name,
|
||||
}: GitHubTokenInputProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<SettingsInput
|
||||
testId={name}
|
||||
name={name}
|
||||
onChange={onChange}
|
||||
label={t(I18nKey.GITHUB$TOKEN_LABEL)}
|
||||
type="password"
|
||||
className="w-[680px]"
|
||||
placeholder={isGitHubTokenSet ? "<hidden>" : ""}
|
||||
startContent={
|
||||
isGitHubTokenSet && (
|
||||
<KeyStatusIcon
|
||||
testId="gh-set-token-indicator"
|
||||
isSet={isGitHubTokenSet}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
<GitHubTokenHelpAnchor />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Trans } from "react-i18next";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
|
||||
export function GitLabTokenHelpAnchor() {
|
||||
return (
|
||||
<p data-testid="gitlab-token-help-anchor" className="text-xs">
|
||||
<Trans
|
||||
i18nKey={I18nKey.GITLAB$TOKEN_HELP_TEXT}
|
||||
components={[
|
||||
<a
|
||||
key="gitlab-token-help-anchor-link"
|
||||
aria-label="Gitlab token help link"
|
||||
href="https://gitlab.com/-/user_settings/personal_access_tokens?name=openhands-app&scopes=api,read_user,read_repository,write_repository"
|
||||
target="_blank"
|
||||
className="underline underline-offset-2"
|
||||
rel="noopener noreferrer"
|
||||
/>,
|
||||
<a
|
||||
key="gitlab-token-help-anchor-link-2"
|
||||
aria-label="GitLab token see more link"
|
||||
href="https://docs.gitlab.com/user/profile/personal_access_tokens/"
|
||||
target="_blank"
|
||||
className="underline underline-offset-2"
|
||||
rel="noopener noreferrer"
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
</p>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { SettingsInput } from "../settings-input";
|
||||
import { GitLabTokenHelpAnchor } from "./gitlab-token-help-anchor";
|
||||
import { KeyStatusIcon } from "../key-status-icon";
|
||||
|
||||
interface GitLabTokenInputProps {
|
||||
onChange: (value: string) => void;
|
||||
isGitLabTokenSet: boolean;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export function GitLabTokenInput({
|
||||
onChange,
|
||||
isGitLabTokenSet,
|
||||
name,
|
||||
}: GitLabTokenInputProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<SettingsInput
|
||||
testId={name}
|
||||
name={name}
|
||||
onChange={onChange}
|
||||
label={t(I18nKey.GITLAB$TOKEN_LABEL)}
|
||||
type="password"
|
||||
className="w-[680px]"
|
||||
placeholder={isGitLabTokenSet ? "<hidden>" : ""}
|
||||
startContent={
|
||||
isGitLabTokenSet && (
|
||||
<KeyStatusIcon
|
||||
testId="gl-set-token-indicator"
|
||||
isSet={isGitLabTokenSet}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
<GitLabTokenHelpAnchor />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export function InputSkeleton() {
|
||||
return (
|
||||
<div className="flex flex-col gap-2.5">
|
||||
<div className="w-[70px] h-[20px] skeleton" />
|
||||
<div className="w-[680px] h-[40px] skeleton" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -2,12 +2,13 @@ import SuccessIcon from "#/icons/success.svg?react";
|
||||
import { cn } from "#/utils/utils";
|
||||
|
||||
interface KeyStatusIconProps {
|
||||
testId?: string;
|
||||
isSet: boolean;
|
||||
}
|
||||
|
||||
export function KeyStatusIcon({ isSet }: KeyStatusIconProps) {
|
||||
export function KeyStatusIcon({ testId, isSet }: KeyStatusIconProps) {
|
||||
return (
|
||||
<span data-testid={isSet ? "set-indicator" : "unset-indicator"}>
|
||||
<span data-testid={testId || (isSet ? "set-indicator" : "unset-indicator")}>
|
||||
<SuccessIcon className={cn(isSet ? "text-success" : "text-danger")} />
|
||||
</span>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { InputSkeleton } from "../input-skeleton";
|
||||
import { SubtextSkeleton } from "../subtext-skeleton";
|
||||
import { SwitchSkeleton } from "../switch-skeleton";
|
||||
|
||||
export function LlmSettingsInputsSkeleton() {
|
||||
return (
|
||||
<div
|
||||
data-testid="app-settings-skeleton"
|
||||
className="px-11 py-9 flex flex-col gap-6"
|
||||
>
|
||||
<SwitchSkeleton />
|
||||
<InputSkeleton />
|
||||
<InputSkeleton />
|
||||
<InputSkeleton />
|
||||
<SubtextSkeleton />
|
||||
<SwitchSkeleton />
|
||||
<SwitchSkeleton />
|
||||
<InputSkeleton />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ModalBackdrop } from "#/components/shared/modals/modal-backdrop";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { BrandButton } from "../brand-button";
|
||||
|
||||
interface ResetSettingsModalProps {
|
||||
onReset: () => void;
|
||||
}
|
||||
|
||||
export function ResetSettingsModal({ onReset }: ResetSettingsModalProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<ModalBackdrop>
|
||||
<div className="bg-base-secondary p-4 rounded-xl flex flex-col gap-4 border border-tertiary">
|
||||
<p>{t(I18nKey.SETTINGS$RESET_CONFIRMATION)}</p>
|
||||
<div className="w-full flex gap-2" data-testid="reset-settings-modal">
|
||||
<BrandButton
|
||||
testId="confirm-button"
|
||||
type="submit"
|
||||
name="reset-settings"
|
||||
variant="primary"
|
||||
className="grow"
|
||||
>
|
||||
Reset
|
||||
</BrandButton>
|
||||
|
||||
<BrandButton
|
||||
testId="cancel-button"
|
||||
type="button"
|
||||
variant="secondary"
|
||||
className="grow"
|
||||
onClick={onReset}
|
||||
>
|
||||
Cancel
|
||||
</BrandButton>
|
||||
</div>
|
||||
</div>
|
||||
</ModalBackdrop>
|
||||
);
|
||||
}
|
||||
@@ -6,6 +6,7 @@ interface SettingsSwitchProps {
|
||||
name?: string;
|
||||
onToggle?: (value: boolean) => void;
|
||||
defaultIsToggled?: boolean;
|
||||
isToggled?: boolean;
|
||||
isBeta?: boolean;
|
||||
}
|
||||
|
||||
@@ -15,6 +16,7 @@ export function SettingsSwitch({
|
||||
name,
|
||||
onToggle,
|
||||
defaultIsToggled,
|
||||
isToggled: controlledIsToggled,
|
||||
isBeta,
|
||||
}: React.PropsWithChildren<SettingsSwitchProps>) {
|
||||
const [isToggled, setIsToggled] = React.useState(defaultIsToggled ?? false);
|
||||
@@ -25,17 +27,18 @@ export function SettingsSwitch({
|
||||
};
|
||||
|
||||
return (
|
||||
<label className="flex items-center gap-2 w-fit">
|
||||
<label className="flex items-center gap-2 w-fit cursor-pointer">
|
||||
<input
|
||||
hidden
|
||||
data-testid={testId}
|
||||
name={name}
|
||||
type="checkbox"
|
||||
onChange={(e) => handleToggle(e.target.checked)}
|
||||
checked={controlledIsToggled ?? isToggled}
|
||||
defaultChecked={defaultIsToggled}
|
||||
/>
|
||||
|
||||
<StyledSwitchComponent isToggled={isToggled} />
|
||||
<StyledSwitchComponent isToggled={controlledIsToggled ?? isToggled} />
|
||||
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-sm">{children}</span>
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export function SubtextSkeleton() {
|
||||
return <div className="w-[250px] h-[20px] skeleton" />;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export function SwitchSkeleton() {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-[48px] h-[24px] skeleton-round" />
|
||||
<div className="w-[100px] h-[20px] skeleton" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -18,7 +18,7 @@ function Terminal() {
|
||||
return (
|
||||
<div className="h-full p-2 min-h-0 flex-grow">
|
||||
{isRuntimeInactive && (
|
||||
<div className="text-sm text-gray-400 mb-2">
|
||||
<div className="w-full h-full flex items-center text-center justify-center text-2xl text-tertiary-light">
|
||||
{t("DIFF_VIEWER$WAITING_FOR_RUNTIME")}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -4,6 +4,8 @@ import { I18nKey } from "#/i18n/declaration";
|
||||
import AllHandsLogo from "#/assets/branding/all-hands-logo.svg?react";
|
||||
import { ModalBackdrop } from "#/components/shared/modals/modal-backdrop";
|
||||
import { ModalBody } from "#/components/shared/modals/modal-body";
|
||||
import { TOSCheckbox } from "./tos-checkbox";
|
||||
import { handleCaptureConsent } from "#/utils/handle-capture-consent";
|
||||
import { BrandButton } from "../settings/brand-button";
|
||||
import GitHubLogo from "#/assets/branding/github-logo.svg?react";
|
||||
import GitLabLogo from "#/assets/branding/gitlab-logo.svg?react";
|
||||
@@ -17,6 +19,7 @@ interface AuthModalProps {
|
||||
|
||||
export function AuthModal({ githubAuthUrl, appMode }: AuthModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const [isTosAccepted, setIsTosAccepted] = React.useState(false);
|
||||
|
||||
const gitlabAuthUrl = useAuthUrl({
|
||||
appMode: appMode || null,
|
||||
@@ -25,14 +28,14 @@ export function AuthModal({ githubAuthUrl, appMode }: AuthModalProps) {
|
||||
|
||||
const handleGitHubAuth = () => {
|
||||
if (githubAuthUrl) {
|
||||
// Always start the OIDC flow, let the backend handle TOS check
|
||||
handleCaptureConsent(true);
|
||||
window.location.href = githubAuthUrl;
|
||||
}
|
||||
};
|
||||
|
||||
const handleGitLabAuth = () => {
|
||||
if (gitlabAuthUrl) {
|
||||
// Always start the OIDC flow, let the backend handle TOS check
|
||||
handleCaptureConsent(true);
|
||||
window.location.href = gitlabAuthUrl;
|
||||
}
|
||||
};
|
||||
@@ -47,8 +50,11 @@ export function AuthModal({ githubAuthUrl, appMode }: AuthModalProps) {
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<TOSCheckbox onChange={() => setIsTosAccepted((prev) => !prev)} />
|
||||
|
||||
<div className="flex flex-col gap-3 w-full">
|
||||
<BrandButton
|
||||
isDisabled={!isTosAccepted}
|
||||
type="button"
|
||||
variant="primary"
|
||||
onClick={handleGitHubAuth}
|
||||
@@ -59,6 +65,7 @@ export function AuthModal({ githubAuthUrl, appMode }: AuthModalProps) {
|
||||
</BrandButton>
|
||||
|
||||
<BrandButton
|
||||
isDisabled={!isTosAccepted}
|
||||
type="button"
|
||||
variant="primary"
|
||||
onClick={handleGitLabAuth}
|
||||
|
||||
@@ -10,6 +10,7 @@ interface ContainerProps {
|
||||
icon?: React.ReactNode;
|
||||
isBeta?: boolean;
|
||||
isLoading?: boolean;
|
||||
rightContent?: React.ReactNode;
|
||||
}[];
|
||||
children: React.ReactNode;
|
||||
className?: React.HTMLAttributes<HTMLDivElement>["className"];
|
||||
@@ -30,16 +31,19 @@ export function Container({
|
||||
>
|
||||
{labels && (
|
||||
<div className="flex text-xs h-[36px]">
|
||||
{labels.map(({ label: l, to, icon, isBeta, isLoading }) => (
|
||||
<NavTab
|
||||
key={to}
|
||||
to={to}
|
||||
label={l}
|
||||
icon={icon}
|
||||
isBeta={isBeta}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
))}
|
||||
{labels.map(
|
||||
({ label: l, to, icon, isBeta, isLoading, rightContent }) => (
|
||||
<NavTab
|
||||
key={to}
|
||||
to={to}
|
||||
label={l}
|
||||
icon={icon}
|
||||
isBeta={isBeta}
|
||||
isLoading={isLoading}
|
||||
rightContent={rightContent}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{!labels && label && (
|
||||
|
||||
@@ -9,9 +9,17 @@ interface NavTabProps {
|
||||
icon: React.ReactNode;
|
||||
isBeta?: boolean;
|
||||
isLoading?: boolean;
|
||||
rightContent?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function NavTab({ to, label, icon, isBeta, isLoading }: NavTabProps) {
|
||||
export function NavTab({
|
||||
to,
|
||||
label,
|
||||
icon,
|
||||
isBeta,
|
||||
isLoading,
|
||||
rightContent,
|
||||
}: NavTabProps) {
|
||||
return (
|
||||
<NavLink
|
||||
end
|
||||
@@ -24,15 +32,17 @@ export function NavTab({ to, label, icon, isBeta, isLoading }: NavTabProps) {
|
||||
)}
|
||||
>
|
||||
{({ isActive }) => (
|
||||
<>
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={cn(isActive && "text-logo")}>{icon}</div>
|
||||
{label}
|
||||
{isBeta && <BetaBadge />}
|
||||
</div>
|
||||
|
||||
{isLoading && <LoadingSpinner size="small" />}
|
||||
</>
|
||||
<div className="flex items-center gap-2">
|
||||
{rightContent}
|
||||
{isLoading && <LoadingSpinner size="small" />}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</NavLink>
|
||||
);
|
||||
|
||||
@@ -19,7 +19,7 @@ export function RefreshIconButton({ onClick }: RefreshIconButtonProps) {
|
||||
/>
|
||||
}
|
||||
testId="refresh"
|
||||
ariaLabel={t(I18nKey.WORKSPACE$REFRESH)}
|
||||
ariaLabel={t("BUTTON$REFRESH" as I18nKey)}
|
||||
onClick={onClick}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
import { IoIosArrowForward, IoIosArrowBack } from "react-icons/io";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { IconButton } from "./icon-button";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
|
||||
interface ToggleWorkspaceIconButtonProps {
|
||||
onClick: () => void;
|
||||
isHidden: boolean;
|
||||
}
|
||||
|
||||
export function ToggleWorkspaceIconButton({
|
||||
onClick,
|
||||
isHidden,
|
||||
}: ToggleWorkspaceIconButtonProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
icon={
|
||||
isHidden ? (
|
||||
<IoIosArrowForward
|
||||
size={20}
|
||||
className="text-neutral-400 hover:text-neutral-100 transition"
|
||||
/>
|
||||
) : (
|
||||
<IoIosArrowBack
|
||||
size={20}
|
||||
className="text-neutral-400 hover:text-neutral-100 transition"
|
||||
/>
|
||||
)
|
||||
}
|
||||
testId="toggle"
|
||||
ariaLabel={
|
||||
isHidden ? t(I18nKey.WORKSPACE$OPEN) : t(I18nKey.WORKSPACE$CLOSE)
|
||||
}
|
||||
onClick={onClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -14,12 +14,14 @@ interface ModelSelectorProps {
|
||||
isDisabled?: boolean;
|
||||
models: Record<string, { separator: string; models: string[] }>;
|
||||
currentModel?: string;
|
||||
onChange?: (model: string | null) => void;
|
||||
}
|
||||
|
||||
export function ModelSelector({
|
||||
isDisabled,
|
||||
models,
|
||||
currentModel,
|
||||
onChange,
|
||||
}: ModelSelectorProps) {
|
||||
const [, setLitellmId] = React.useState<string | null>(null);
|
||||
const [selectedProvider, setSelectedProvider] = React.useState<string | null>(
|
||||
@@ -55,6 +57,7 @@ export function ModelSelector({
|
||||
}
|
||||
setLitellmId(fullModel);
|
||||
setSelectedModel(model);
|
||||
onChange?.(fullModel);
|
||||
};
|
||||
|
||||
const clear = () => {
|
||||
|
||||
@@ -52,7 +52,7 @@ export function TaskForm({ ref }: TaskFormProps) {
|
||||
const formData = new FormData(event.currentTarget);
|
||||
|
||||
const q = formData.get("q")?.toString();
|
||||
createConversation({ q });
|
||||
createConversation({ q, conversation_trigger: "gui" });
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
interface FilesContextType {
|
||||
/**
|
||||
* List of file paths in the workspace
|
||||
*/
|
||||
paths: string[];
|
||||
/**
|
||||
* Set the list of file paths in the workspace
|
||||
* @param paths The list of file paths in the workspace
|
||||
* @returns void
|
||||
*/
|
||||
setPaths: (paths: string[]) => void;
|
||||
/**
|
||||
* A map of file paths to their contents
|
||||
*/
|
||||
files: Record<string, string>;
|
||||
/**
|
||||
* Set the content of a file
|
||||
* @param path The path of the file
|
||||
* @param content The content of the file
|
||||
* @returns void
|
||||
*/
|
||||
setFileContent: (path: string, content: string) => void;
|
||||
selectedPath: string | null;
|
||||
setSelectedPath: (path: string | null) => void;
|
||||
}
|
||||
|
||||
const FilesContext = React.createContext<FilesContextType | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
interface FilesProviderProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
function FilesProvider({ children }: FilesProviderProps) {
|
||||
const [paths, setPaths] = React.useState<string[]>([]);
|
||||
const [files, setFiles] = React.useState<Record<string, string>>({});
|
||||
const [selectedPath, setSelectedPath] = React.useState<string | null>(null);
|
||||
|
||||
const setFileContent = React.useCallback((path: string, content: string) => {
|
||||
setFiles((prev) => ({ ...prev, [path]: content }));
|
||||
}, []);
|
||||
|
||||
const value = React.useMemo(
|
||||
() => ({
|
||||
paths,
|
||||
setPaths,
|
||||
files,
|
||||
setFileContent,
|
||||
selectedPath,
|
||||
setSelectedPath,
|
||||
}),
|
||||
[paths, setPaths, files, setFileContent, selectedPath, setSelectedPath],
|
||||
);
|
||||
|
||||
return <FilesContext value={value}>{children}</FilesContext>;
|
||||
}
|
||||
|
||||
function useFiles() {
|
||||
const context = React.useContext(FilesContext);
|
||||
if (context === undefined) {
|
||||
throw new Error("useFiles must be used within a FilesProvider");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
export { FilesProvider, useFiles };
|
||||
@@ -6,6 +6,8 @@ import OpenHands from "#/api/open-hands";
|
||||
import { setInitialPrompt } from "#/state/initial-query-slice";
|
||||
import { RootState } from "#/store";
|
||||
import { GitRepository } from "#/types/git";
|
||||
import { ConversationTrigger } from "#/api/open-hands.types";
|
||||
import { SuggestedTask } from "#/components/features/home/tasks/task.types";
|
||||
|
||||
export const useCreateConversation = () => {
|
||||
const navigate = useNavigate();
|
||||
@@ -19,16 +21,20 @@ export const useCreateConversation = () => {
|
||||
return useMutation({
|
||||
mutationKey: ["create-conversation"],
|
||||
mutationFn: async (variables: {
|
||||
conversation_trigger: ConversationTrigger;
|
||||
q?: string;
|
||||
selectedRepository?: GitRepository | null;
|
||||
suggested_task?: SuggestedTask;
|
||||
}) => {
|
||||
if (variables.q) dispatch(setInitialPrompt(variables.q));
|
||||
|
||||
return OpenHands.createConversation(
|
||||
variables.conversation_trigger,
|
||||
variables.selectedRepository || undefined,
|
||||
variables.q,
|
||||
files,
|
||||
replayJson || undefined,
|
||||
variables.suggested_task || undefined,
|
||||
);
|
||||
},
|
||||
onSuccess: async ({ conversation_id: conversationId }, { q }) => {
|
||||
|
||||
@@ -4,15 +4,7 @@ import OpenHands from "#/api/open-hands";
|
||||
import { PostSettings, PostApiSettings } from "#/types/settings";
|
||||
import { useSettings } from "../query/use-settings";
|
||||
|
||||
const saveSettingsMutationFn = async (
|
||||
settings: Partial<PostSettings> | null,
|
||||
) => {
|
||||
// If settings is null, we're resetting
|
||||
if (settings === null) {
|
||||
await OpenHands.resetSettings();
|
||||
return;
|
||||
}
|
||||
|
||||
const saveSettingsMutationFn = async (settings: Partial<PostSettings>) => {
|
||||
const apiSettings: Partial<PostApiSettings> = {
|
||||
llm_model: settings.LLM_MODEL,
|
||||
llm_base_url: settings.LLM_BASE_URL,
|
||||
@@ -39,12 +31,7 @@ export const useSaveSettings = () => {
|
||||
const { data: currentSettings } = useSettings();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (settings: Partial<PostSettings> | null) => {
|
||||
if (settings === null) {
|
||||
await saveSettingsMutationFn(null);
|
||||
return;
|
||||
}
|
||||
|
||||
mutationFn: async (settings: Partial<PostSettings>) => {
|
||||
const newSettings = { ...currentSettings, ...settings };
|
||||
await saveSettingsMutationFn(newSettings);
|
||||
},
|
||||
|
||||
@@ -2,16 +2,6 @@ import { useQuery } from "@tanstack/react-query";
|
||||
import { useConfig } from "./use-config";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
|
||||
// Instead of directly using useLocation, we'll check the current path manually
|
||||
// This avoids the Router context requirement
|
||||
const isOnTosPage = () => {
|
||||
// Only run this check in browser environment
|
||||
if (typeof window !== "undefined") {
|
||||
return window.location.pathname === "/accept-tos";
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export const useBalance = () => {
|
||||
const { data: config } = useConfig();
|
||||
|
||||
@@ -19,8 +9,6 @@ export const useBalance = () => {
|
||||
queryKey: ["user", "balance"],
|
||||
queryFn: OpenHands.getBalance,
|
||||
enabled:
|
||||
!isOnTosPage() &&
|
||||
config?.APP_MODE === "saas" &&
|
||||
config?.FEATURE_FLAGS.ENABLE_BILLING,
|
||||
config?.APP_MODE === "saas" && config?.FEATURE_FLAGS.ENABLE_BILLING,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,21 +1,10 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
|
||||
// Instead of directly using useLocation, we'll check the current path manually
|
||||
// This avoids the Router context requirement
|
||||
const isOnTosPage = () => {
|
||||
// Only run this check in browser environment
|
||||
if (typeof window !== "undefined") {
|
||||
return window.location.pathname === "/accept-tos";
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export const useConfig = () =>
|
||||
useQuery({
|
||||
queryKey: ["config"],
|
||||
queryFn: OpenHands.getConfig,
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
gcTime: 1000 * 60 * 15, // 15 minutes,
|
||||
enabled: !isOnTosPage(),
|
||||
gcTime: 1000 * 60 * 15, // 15 minutes
|
||||
});
|
||||
|
||||
@@ -4,16 +4,6 @@ import OpenHands from "#/api/open-hands";
|
||||
import { useConfig } from "./use-config";
|
||||
import { useAuth } from "#/context/auth-context";
|
||||
|
||||
// Instead of directly using useLocation, we'll check the current path manually
|
||||
// This avoids the Router context requirement
|
||||
const isOnTosPage = () => {
|
||||
// Only run this check in browser environment
|
||||
if (typeof window !== "undefined") {
|
||||
return window.location.pathname === "/accept-tos";
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export const useIsAuthed = () => {
|
||||
const { providersAreSet } = useAuth();
|
||||
const { data: config } = useConfig();
|
||||
@@ -23,7 +13,7 @@ export const useIsAuthed = () => {
|
||||
return useQuery({
|
||||
queryKey: ["user", "authenticated", providersAreSet, appMode],
|
||||
queryFn: () => OpenHands.authenticate(appMode!),
|
||||
enabled: !!appMode && !isOnTosPage(),
|
||||
enabled: !!appMode,
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
gcTime: 1000 * 60 * 15, // 15 minutes
|
||||
retry: false,
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useConversation } from "#/context/conversation-context";
|
||||
import { FileService } from "#/api/file-service/file-service.api";
|
||||
|
||||
interface UseListFileConfig {
|
||||
path: string;
|
||||
}
|
||||
|
||||
export const useListFile = (config: UseListFileConfig) => {
|
||||
const { conversationId } = useConversation();
|
||||
return useQuery({
|
||||
queryKey: ["files", conversationId, config.path],
|
||||
queryFn: () => FileService.getFile(conversationId, config.path),
|
||||
enabled: false, // don't fetch by default, trigger manually via `refetch`
|
||||
});
|
||||
};
|
||||
@@ -1,29 +0,0 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useSelector } from "react-redux";
|
||||
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;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
const DEFAULT_CONFIG: UseListFilesConfig = {
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
export const useListFiles = (config: UseListFilesConfig = DEFAULT_CONFIG) => {
|
||||
const { conversationId } = useConversation();
|
||||
const { curAgentState } = useSelector((state: RootState) => state.agent);
|
||||
const runtimeIsActive = !RUNTIME_INACTIVE_STATES.includes(curAgentState);
|
||||
|
||||
return useQuery({
|
||||
queryKey: ["files", conversationId, config?.path],
|
||||
queryFn: () => FileService.getFiles(conversationId, config?.path),
|
||||
enabled: runtimeIsActive && !!config?.enabled,
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
gcTime: 1000 * 60 * 15, // 15 minutes
|
||||
});
|
||||
};
|
||||
@@ -4,18 +4,9 @@ import posthog from "posthog-js";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import { useAuth } from "#/context/auth-context";
|
||||
import { DEFAULT_SETTINGS } from "#/services/settings";
|
||||
import { Settings } from "#/types/settings";
|
||||
|
||||
// Instead of directly using useLocation, we'll check the current path manually
|
||||
// This avoids the Router context requirement
|
||||
const isOnTosPage = () => {
|
||||
// Only run this check in browser environment
|
||||
if (typeof window !== "undefined") {
|
||||
return window.location.pathname === "/accept-tos";
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const getSettingsQueryFn = async () => {
|
||||
const getSettingsQueryFn = async (): Promise<Settings> => {
|
||||
const apiSettings = await OpenHands.getSettings();
|
||||
|
||||
return {
|
||||
@@ -49,7 +40,6 @@ export const useSettings = () => {
|
||||
retry: (_, error) => error.status !== 404,
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
gcTime: 1000 * 60 * 15, // 15 minutes
|
||||
enabled: !isOnTosPage(),
|
||||
meta: {
|
||||
disableToast: true,
|
||||
},
|
||||
|
||||
@@ -26,6 +26,17 @@ export enum I18nKey {
|
||||
ANALYTICS$DESCRIPTION = "ANALYTICS$DESCRIPTION",
|
||||
ANALYTICS$SEND_ANONYMOUS_DATA = "ANALYTICS$SEND_ANONYMOUS_DATA",
|
||||
ANALYTICS$CONFIRM_PREFERENCES = "ANALYTICS$CONFIRM_PREFERENCES",
|
||||
SETTINGS$SAVING = "SETTINGS$SAVING",
|
||||
SETTINGS$SAVE_CHANGES = "SETTINGS$SAVE_CHANGES",
|
||||
SETTINGS$NAV_GIT = "SETTINGS$NAV_GIT",
|
||||
SETTINGS$NAV_APPLICATION = "SETTINGS$NAV_APPLICATION",
|
||||
SETTINGS$NAV_CREDITS = "SETTINGS$NAV_CREDITS",
|
||||
SETTINGS$NAV_API_KEYS = "SETTINGS$NAV_API_KEYS",
|
||||
SETTINGS$NAV_LLM = "SETTINGS$NAV_LLM",
|
||||
GIT$MERGE_REQUEST = "GIT$MERGE_REQUEST",
|
||||
GIT$GITLAB_API = "GIT$GITLAB_API",
|
||||
GIT$PULL_REQUEST = "GIT$PULL_REQUEST",
|
||||
GIT$GITHUB_API = "GIT$GITHUB_API",
|
||||
BUTTON$COPY = "BUTTON$COPY",
|
||||
BUTTON$COPIED = "BUTTON$COPIED",
|
||||
APP$TITLE = "APP$TITLE",
|
||||
@@ -34,8 +45,11 @@ export enum I18nKey {
|
||||
SETTINGS$TITLE = "SETTINGS$TITLE",
|
||||
CONVERSATION$START_NEW = "CONVERSATION$START_NEW",
|
||||
ACCOUNT_SETTINGS$TITLE = "ACCOUNT_SETTINGS$TITLE",
|
||||
WORKSPACE$TITLE = "WORKSPACE$TITLE",
|
||||
WORKSPACE$TERMINAL_TAB_LABEL = "WORKSPACE$TERMINAL_TAB_LABEL",
|
||||
WORKSPACE$BROWSER_TAB_LABEL = "WORKSPACE$BROWSER_TAB_LABEL",
|
||||
WORKSPACE$JUPYTER_TAB_LABEL = "WORKSPACE$JUPYTER_TAB_LABEL",
|
||||
WORKSPACE$CODE_EDITOR_TAB_LABEL = "WORKSPACE$CODE_EDITOR_TAB_LABEL",
|
||||
WORKSPACE$TITLE = "WORKSPACE$TITLE",
|
||||
TERMINAL$WAITING_FOR_CLIENT = "TERMINAL$WAITING_FOR_CLIENT",
|
||||
CODE_EDITOR$FILE_SAVED_SUCCESSFULLY = "CODE_EDITOR$FILE_SAVED_SUCCESSFULLY",
|
||||
CODE_EDITOR$SAVING_LABEL = "CODE_EDITOR$SAVING_LABEL",
|
||||
@@ -63,14 +77,12 @@ export enum I18nKey {
|
||||
ERROR$GENERIC = "ERROR$GENERIC",
|
||||
GITHUB$AUTH_SCOPE = "GITHUB$AUTH_SCOPE",
|
||||
FILE_SERVICE$INVALID_FILE_PATH = "FILE_SERVICE$INVALID_FILE_PATH",
|
||||
WORKSPACE$PLANNER_TAB_LABEL = "WORKSPACE$PLANNER_TAB_LABEL",
|
||||
WORKSPACE$JUPYTER_TAB_LABEL = "WORKSPACE$JUPYTER_TAB_LABEL",
|
||||
WORKSPACE$CODE_EDITOR_TAB_LABEL = "WORKSPACE$CODE_EDITOR_TAB_LABEL",
|
||||
WORKSPACE$BROWSER_TAB_LABEL = "WORKSPACE$BROWSER_TAB_LABEL",
|
||||
WORKSPACE$REFRESH = "WORKSPACE$REFRESH",
|
||||
WORKSPACE$OPEN = "WORKSPACE$OPEN",
|
||||
WORKSPACE$CLOSE = "WORKSPACE$CLOSE",
|
||||
VSCODE$OPEN = "VSCODE$OPEN",
|
||||
VSCODE$TITLE = "VSCODE$TITLE",
|
||||
VSCODE$LOADING = "VSCODE$LOADING",
|
||||
VSCODE$URL_NOT_AVAILABLE = "VSCODE$URL_NOT_AVAILABLE",
|
||||
VSCODE$FETCH_ERROR = "VSCODE$FETCH_ERROR",
|
||||
VSCODE$IFRAME_PERMISSIONS = "VSCODE$IFRAME_PERMISSIONS",
|
||||
INCREASE_TEST_COVERAGE = "INCREASE_TEST_COVERAGE",
|
||||
AUTO_MERGE_PRS = "AUTO_MERGE_PRS",
|
||||
FIX_README = "FIX_README",
|
||||
@@ -82,7 +94,6 @@ export enum I18nKey {
|
||||
API$DONT_KNOW_KEY = "API$DONT_KNOW_KEY",
|
||||
BUTTON$SAVE = "BUTTON$SAVE",
|
||||
BUTTON$CLOSE = "BUTTON$CLOSE",
|
||||
BUTTON$RESET_TO_DEFAULTS = "BUTTON$RESET_TO_DEFAULTS",
|
||||
MODAL$CONFIRM_RESET_TITLE = "MODAL$CONFIRM_RESET_TITLE",
|
||||
MODAL$CONFIRM_RESET_MESSAGE = "MODAL$CONFIRM_RESET_MESSAGE",
|
||||
MODAL$END_SESSION_TITLE = "MODAL$END_SESSION_TITLE",
|
||||
@@ -95,6 +106,9 @@ export enum I18nKey {
|
||||
GITHUB$TOKEN_LABEL = "GITHUB$TOKEN_LABEL",
|
||||
GITHUB$TOKEN_OPTIONAL = "GITHUB$TOKEN_OPTIONAL",
|
||||
GITHUB$GET_TOKEN = "GITHUB$GET_TOKEN",
|
||||
GITHUB$TOKEN_HELP_TEXT = "GITHUB$TOKEN_HELP_TEXT",
|
||||
GITHUB$TOKEN_LINK_TEXT = "GITHUB$TOKEN_LINK_TEXT",
|
||||
GITHUB$INSTRUCTIONS_LINK_TEXT = "GITHUB$INSTRUCTIONS_LINK_TEXT",
|
||||
COMMON$HERE = "COMMON$HERE",
|
||||
ANALYTICS$ENABLE = "ANALYTICS$ENABLE",
|
||||
GITHUB$TOKEN_INVALID = "GITHUB$TOKEN_INVALID",
|
||||
@@ -135,10 +149,6 @@ export enum I18nKey {
|
||||
SESSION$SOCKET_NOT_INITIALIZED_ERROR_MESSAGE = "SESSION$SOCKET_NOT_INITIALIZED_ERROR_MESSAGE",
|
||||
EXPLORER$UPLOAD_ERROR_MESSAGE = "EXPLORER$UPLOAD_ERROR_MESSAGE",
|
||||
EXPLORER$LABEL_DROP_FILES = "EXPLORER$LABEL_DROP_FILES",
|
||||
EXPLORER$LABEL_WORKSPACE = "EXPLORER$LABEL_WORKSPACE",
|
||||
EXPLORER$EMPTY_WORKSPACE_MESSAGE = "EXPLORER$EMPTY_WORKSPACE_MESSAGE",
|
||||
EXPLORER$LOADING_WORKSPACE_MESSAGE = "EXPLORER$LOADING_WORKSPACE_MESSAGE",
|
||||
EXPLORER$REFRESH_ERROR_MESSAGE = "EXPLORER$REFRESH_ERROR_MESSAGE",
|
||||
EXPLORER$UPLOAD_SUCCESS_MESSAGE = "EXPLORER$UPLOAD_SUCCESS_MESSAGE",
|
||||
EXPLORER$NO_FILES_UPLOADED_MESSAGE = "EXPLORER$NO_FILES_UPLOADED_MESSAGE",
|
||||
EXPLORER$UPLOAD_PARTIAL_SUCCESS_MESSAGE = "EXPLORER$UPLOAD_PARTIAL_SUCCESS_MESSAGE",
|
||||
@@ -289,6 +299,7 @@ export enum I18nKey {
|
||||
BUTTON$CREATE = "BUTTON$CREATE",
|
||||
BUTTON$DELETE = "BUTTON$DELETE",
|
||||
BUTTON$COPY_TO_CLIPBOARD = "BUTTON$COPY_TO_CLIPBOARD",
|
||||
BUTTON$REFRESH = "BUTTON$REFRESH",
|
||||
ERROR$REQUIRED_FIELD = "ERROR$REQUIRED_FIELD",
|
||||
PLANNER$EMPTY_MESSAGE = "PLANNER$EMPTY_MESSAGE",
|
||||
FEEDBACK$PUBLIC_LABEL = "FEEDBACK$PUBLIC_LABEL",
|
||||
@@ -321,7 +332,6 @@ export enum I18nKey {
|
||||
SETTINGS_FORM$ENABLE_CONFIRMATION_MODE_LABEL = "SETTINGS_FORM$ENABLE_CONFIRMATION_MODE_LABEL",
|
||||
SETTINGS_FORM$SAVE_LABEL = "SETTINGS_FORM$SAVE_LABEL",
|
||||
SETTINGS_FORM$CLOSE_LABEL = "SETTINGS_FORM$CLOSE_LABEL",
|
||||
SETTINGS_FORM$RESET_TO_DEFAULTS_LABEL = "SETTINGS_FORM$RESET_TO_DEFAULTS_LABEL",
|
||||
SETTINGS_FORM$CANCEL_LABEL = "SETTINGS_FORM$CANCEL_LABEL",
|
||||
SETTINGS_FORM$END_SESSION_LABEL = "SETTINGS_FORM$END_SESSION_LABEL",
|
||||
SETTINGS_FORM$CHANGING_WORKSPACE_WARNING_MESSAGE = "SETTINGS_FORM$CHANGING_WORKSPACE_WARNING_MESSAGE",
|
||||
@@ -340,6 +350,7 @@ export enum I18nKey {
|
||||
STATUS$LLM_RETRY = "STATUS$LLM_RETRY",
|
||||
AGENT_ERROR$BAD_ACTION = "AGENT_ERROR$BAD_ACTION",
|
||||
AGENT_ERROR$ACTION_TIMEOUT = "AGENT_ERROR$ACTION_TIMEOUT",
|
||||
AGENT_ERROR$TOO_MANY_CONVERSATIONS = "AGENT_ERROR$TOO_MANY_CONVERSATIONS",
|
||||
PROJECT_MENU_CARD_CONTEXT_MENU$CONNECT_TO_GITHUB_LABEL = "PROJECT_MENU_CARD_CONTEXT_MENU$CONNECT_TO_GITHUB_LABEL",
|
||||
PROJECT_MENU_CARD_CONTEXT_MENU$PUSH_TO_GITHUB_LABEL = "PROJECT_MENU_CARD_CONTEXT_MENU$PUSH_TO_GITHUB_LABEL",
|
||||
PROJECT_MENU_CARD_CONTEXT_MENU$DOWNLOAD_FILES_LABEL = "PROJECT_MENU_CARD_CONTEXT_MENU$DOWNLOAD_FILES_LABEL",
|
||||
@@ -349,9 +360,6 @@ export enum I18nKey {
|
||||
BROWSER$SCREENSHOT_ALT = "BROWSER$SCREENSHOT_ALT",
|
||||
ERROR_TOAST$CLOSE_BUTTON_LABEL = "ERROR_TOAST$CLOSE_BUTTON_LABEL",
|
||||
FILE_EXPLORER$UPLOAD = "FILE_EXPLORER$UPLOAD",
|
||||
FILE_EXPLORER$REFRESH_WORKSPACE = "FILE_EXPLORER$REFRESH_WORKSPACE",
|
||||
FILE_EXPLORER$OPEN_WORKSPACE = "FILE_EXPLORER$OPEN_WORKSPACE",
|
||||
FILE_EXPLORER$CLOSE_WORKSPACE = "FILE_EXPLORER$CLOSE_WORKSPACE",
|
||||
ACTION_MESSAGE$RUN = "ACTION_MESSAGE$RUN",
|
||||
ACTION_MESSAGE$RUN_IPYTHON = "ACTION_MESSAGE$RUN_IPYTHON",
|
||||
ACTION_MESSAGE$READ = "ACTION_MESSAGE$READ",
|
||||
@@ -443,6 +451,9 @@ export enum I18nKey {
|
||||
MODEL_SELECTOR$OTHERS = "MODEL_SELECTOR$OTHERS",
|
||||
GITLAB$TOKEN_LABEL = "GITLAB$TOKEN_LABEL",
|
||||
GITLAB$GET_TOKEN = "GITLAB$GET_TOKEN",
|
||||
GITLAB$TOKEN_HELP_TEXT = "GITLAB$TOKEN_HELP_TEXT",
|
||||
GITLAB$TOKEN_LINK_TEXT = "GITLAB$TOKEN_LINK_TEXT",
|
||||
GITLAB$INSTRUCTIONS_LINK_TEXT = "GITLAB$INSTRUCTIONS_LINK_TEXT",
|
||||
GITLAB$OR_SEE = "GITLAB$OR_SEE",
|
||||
COMMON$DOCUMENTATION = "COMMON$DOCUMENTATION",
|
||||
DIFF_VIEWER$LOADING = "DIFF_VIEWER$LOADING",
|
||||
@@ -458,8 +469,4 @@ export enum I18nKey {
|
||||
SYSTEM_MESSAGE_MODAL$TOOLS_TAB = "SYSTEM_MESSAGE_MODAL$TOOLS_TAB",
|
||||
SYSTEM_MESSAGE_MODAL$PARAMETERS = "SYSTEM_MESSAGE_MODAL$PARAMETERS",
|
||||
SYSTEM_MESSAGE_MODAL$NO_TOOLS = "SYSTEM_MESSAGE_MODAL$NO_TOOLS",
|
||||
TOS$ACCEPT_TERMS_OF_SERVICE = "TOS$ACCEPT_TERMS_OF_SERVICE",
|
||||
TOS$ACCEPT_TERMS_DESCRIPTION = "TOS$ACCEPT_TERMS_DESCRIPTION",
|
||||
TOS$CONTINUE = "TOS$CONTINUE",
|
||||
TOS$ERROR_ACCEPTING = "TOS$ERROR_ACCEPTING",
|
||||
}
|
||||
|
||||
@@ -389,6 +389,171 @@
|
||||
"tr": "Tercihleri Onayla",
|
||||
"de": "Einstellungen bestätigen"
|
||||
},
|
||||
"SETTINGS$SAVING": {
|
||||
"en": "Saving...",
|
||||
"ja": "保存中...",
|
||||
"zh-CN": "保存中...",
|
||||
"zh-TW": "儲存中...",
|
||||
"ko-KR": "저장 중...",
|
||||
"no": "Lagrer...",
|
||||
"it": "Salvataggio in corso...",
|
||||
"pt": "Salvando...",
|
||||
"es": "Guardando...",
|
||||
"ar": "جار الحفظ...",
|
||||
"fr": "Enregistrement en cours...",
|
||||
"tr": "Kayıt yapılıyor...",
|
||||
"de": "Speichern..."
|
||||
},
|
||||
"SETTINGS$SAVE_CHANGES": {
|
||||
"en": "Save Changes",
|
||||
"ja": "変更を保存",
|
||||
"zh-CN": "保存更改",
|
||||
"zh-TW": "儲存變更",
|
||||
"ko-KR": "변경 사항 저장",
|
||||
"no": "Lagre endringer",
|
||||
"it": "Salva modifiche",
|
||||
"pt": "Salvar alterações",
|
||||
"es": "Guardar cambios",
|
||||
"ar": "حفظ التغييرات",
|
||||
"fr": "Enregistrer les modifications",
|
||||
"tr": "Değişiklikleri Kaydet",
|
||||
"de": "Änderungen speichern"
|
||||
},
|
||||
"SETTINGS$NAV_GIT": {
|
||||
"en": "Git",
|
||||
"ja": "Git",
|
||||
"zh-CN": "Git",
|
||||
"zh-TW": "Git",
|
||||
"ko-KR": "Git",
|
||||
"no": "Git",
|
||||
"it": "Git",
|
||||
"pt": "Git",
|
||||
"es": "Git",
|
||||
"ar": "Git",
|
||||
"fr": "Git",
|
||||
"tr": "Git",
|
||||
"de": "Git"
|
||||
},
|
||||
"SETTINGS$NAV_APPLICATION": {
|
||||
"en": "Application",
|
||||
"ja": "アプリケーション",
|
||||
"zh-CN": "应用程序",
|
||||
"zh-TW": "應用程式",
|
||||
"ko-KR": "애플리케이션",
|
||||
"no": "Applikasjon",
|
||||
"it": "Applicazione",
|
||||
"pt": "Aplicação",
|
||||
"es": "Aplicación",
|
||||
"ar": "التطبيق",
|
||||
"fr": "Application",
|
||||
"tr": "Uygulama",
|
||||
"de": "Anwendung"
|
||||
},
|
||||
"SETTINGS$NAV_CREDITS": {
|
||||
"en": "Credits",
|
||||
"ja": "クレジット",
|
||||
"zh-CN": "积分",
|
||||
"zh-TW": "點數",
|
||||
"ko-KR": "크레딧",
|
||||
"no": "Kreditter",
|
||||
"it": "Crediti",
|
||||
"pt": "Créditos",
|
||||
"es": "Créditos",
|
||||
"ar": "الرصيد",
|
||||
"fr": "Crédits",
|
||||
"tr": "Krediler",
|
||||
"de": "Guthaben"
|
||||
},
|
||||
"SETTINGS$NAV_API_KEYS": {
|
||||
"en": "API Keys",
|
||||
"ja": "APIキー",
|
||||
"zh-CN": "API密钥",
|
||||
"zh-TW": "API金鑰",
|
||||
"ko-KR": "API 키",
|
||||
"no": "API-nøkler",
|
||||
"it": "Chiavi API",
|
||||
"pt": "Chaves de API",
|
||||
"es": "Claves API",
|
||||
"ar": "مفاتيح API",
|
||||
"fr": "Clés API",
|
||||
"tr": "API Anahtarları",
|
||||
"de": "API-Schlüssel"
|
||||
},
|
||||
"SETTINGS$NAV_LLM": {
|
||||
"en": "LLM",
|
||||
"ja": "LLM",
|
||||
"zh-CN": "LLM",
|
||||
"zh-TW": "LLM",
|
||||
"ko-KR": "LLM",
|
||||
"no": "LLM",
|
||||
"it": "LLM",
|
||||
"pt": "LLM",
|
||||
"es": "LLM",
|
||||
"ar": "LLM",
|
||||
"fr": "LLM",
|
||||
"tr": "LLM",
|
||||
"de": "LLM"
|
||||
},
|
||||
"GIT$MERGE_REQUEST": {
|
||||
"en": "Merge Request",
|
||||
"ja": "マージリクエスト",
|
||||
"zh-CN": "合并请求",
|
||||
"zh-TW": "合併請求",
|
||||
"ko-KR": "머지 요청",
|
||||
"no": "Fletteforespørsel",
|
||||
"it": "Richiesta di fusione",
|
||||
"pt": "Solicitação de mesclagem",
|
||||
"es": "Solicitud de fusión",
|
||||
"ar": "طلب الدمج",
|
||||
"fr": "Demande de fusion",
|
||||
"tr": "Birleştirme İsteği",
|
||||
"de": "Merge-Anfrage"
|
||||
},
|
||||
"GIT$GITLAB_API": {
|
||||
"en": "GitLab API",
|
||||
"ja": "GitLab API",
|
||||
"zh-CN": "GitLab API",
|
||||
"zh-TW": "GitLab API",
|
||||
"ko-KR": "GitLab API",
|
||||
"no": "GitLab API",
|
||||
"it": "API GitLab",
|
||||
"pt": "API do GitLab",
|
||||
"es": "API de GitLab",
|
||||
"ar": "واجهة برمجة تطبيقات GitLab",
|
||||
"fr": "API GitLab",
|
||||
"tr": "GitLab API",
|
||||
"de": "GitLab API"
|
||||
},
|
||||
"GIT$PULL_REQUEST": {
|
||||
"en": "Pull Request",
|
||||
"ja": "プルリクエスト",
|
||||
"zh-CN": "拉取请求",
|
||||
"zh-TW": "拉取請求",
|
||||
"ko-KR": "풀 리퀘스트",
|
||||
"no": "Trekkforespørsel",
|
||||
"it": "Richiesta di pull",
|
||||
"pt": "Solicitação de pull",
|
||||
"es": "Solicitud de extracción",
|
||||
"ar": "طلب السحب",
|
||||
"fr": "Demande de tirage",
|
||||
"tr": "Çekme İsteği",
|
||||
"de": "Pull Request"
|
||||
},
|
||||
"GIT$GITHUB_API": {
|
||||
"en": "GitHub API",
|
||||
"ja": "GitHub API",
|
||||
"zh-CN": "GitHub API",
|
||||
"zh-TW": "GitHub API",
|
||||
"ko-KR": "GitHub API",
|
||||
"no": "GitHub API",
|
||||
"it": "API GitHub",
|
||||
"pt": "API do GitHub",
|
||||
"es": "API de GitHub",
|
||||
"ar": "واجهة برمجة تطبيقات GitHub",
|
||||
"fr": "API GitHub",
|
||||
"tr": "GitHub API",
|
||||
"de": "GitHub API"
|
||||
},
|
||||
"BUTTON$COPY": {
|
||||
"en": "Copy to clipboard",
|
||||
"ja": "クリップボードにコピー",
|
||||
@@ -511,21 +676,7 @@
|
||||
"tr": "Hesap ayarları",
|
||||
"de": "Kontoeinstellungen"
|
||||
},
|
||||
"WORKSPACE$TITLE": {
|
||||
"en": "Workspace",
|
||||
"zh-CN": "工作区",
|
||||
"de": "Arbeitsbereich",
|
||||
"ko-KR": "워크스페이스",
|
||||
"no": "Arbeidsområde",
|
||||
"zh-TW": "工作區",
|
||||
"ar": "مساحة العمل",
|
||||
"fr": "Espace de travail",
|
||||
"it": "Area di lavoro",
|
||||
"pt": "Espaço de trabalho",
|
||||
"es": "Espacio de trabajo",
|
||||
"ja": "ワークスペース",
|
||||
"tr": "Çalışma Alanı"
|
||||
},
|
||||
|
||||
"WORKSPACE$TERMINAL_TAB_LABEL": {
|
||||
"en": "Terminal",
|
||||
"zh-CN": "终端",
|
||||
@@ -541,6 +692,66 @@
|
||||
"tr": "Terminal",
|
||||
"ja": "ターミナル"
|
||||
},
|
||||
"WORKSPACE$BROWSER_TAB_LABEL": {
|
||||
"en": "Browser",
|
||||
"zh-CN": "浏览器",
|
||||
"de": "Browser",
|
||||
"ko-KR": "브라우저",
|
||||
"no": "Nettleser",
|
||||
"zh-TW": "瀏覽器",
|
||||
"it": "Browser",
|
||||
"pt": "Navegador",
|
||||
"es": "Navegador",
|
||||
"ar": "المتصفح",
|
||||
"fr": "Navigateur",
|
||||
"tr": "Tarayıcı",
|
||||
"ja": "ブラウザ"
|
||||
},
|
||||
"WORKSPACE$JUPYTER_TAB_LABEL": {
|
||||
"en": "Jupyter",
|
||||
"zh-CN": "Jupyter",
|
||||
"de": "Jupyter",
|
||||
"ko-KR": "Jupyter",
|
||||
"no": "Jupyter",
|
||||
"zh-TW": "Jupyter",
|
||||
"it": "Jupyter",
|
||||
"pt": "Jupyter",
|
||||
"es": "Jupyter",
|
||||
"ar": "Jupyter",
|
||||
"fr": "Jupyter",
|
||||
"tr": "Jupyter",
|
||||
"ja": "Jupyter"
|
||||
},
|
||||
"WORKSPACE$CODE_EDITOR_TAB_LABEL": {
|
||||
"en": "Code Editor",
|
||||
"zh-CN": "代码编辑器",
|
||||
"de": "Code-Editor",
|
||||
"ko-KR": "코드 에디터",
|
||||
"no": "Kodeeditor",
|
||||
"zh-TW": "程式碼編輯器",
|
||||
"it": "Editor di codice",
|
||||
"pt": "Editor de código",
|
||||
"es": "Editor de código",
|
||||
"ar": "محرر الكود",
|
||||
"fr": "Éditeur de code",
|
||||
"tr": "Kod Düzenleyici",
|
||||
"ja": "コードエディタ"
|
||||
},
|
||||
"WORKSPACE$TITLE": {
|
||||
"en": "Workspace",
|
||||
"zh-CN": "工作区",
|
||||
"de": "Arbeitsbereich",
|
||||
"ko-KR": "작업 공간",
|
||||
"no": "Arbeidsområde",
|
||||
"zh-TW": "工作區",
|
||||
"it": "Area di lavoro",
|
||||
"pt": "Espaço de trabalho",
|
||||
"es": "Espacio de trabajo",
|
||||
"ar": "مساحة العمل",
|
||||
"fr": "Espace de travail",
|
||||
"tr": "Çalışma Alanı",
|
||||
"ja": "ワークスペース"
|
||||
},
|
||||
"TERMINAL$WAITING_FOR_CLIENT": {
|
||||
"en": "Waiting for client to become ready...",
|
||||
"ja": "クライアントの準備を待機中...",
|
||||
@@ -946,111 +1157,13 @@
|
||||
"tr": "Geçersiz dosya yolu. Lütfen dosya adını kontrol edin ve tekrar deneyin.",
|
||||
"ja": "ファイルパスが無効です。ファイル名を確認して、もう一度お試しください。"
|
||||
},
|
||||
"WORKSPACE$PLANNER_TAB_LABEL": {
|
||||
"en": "Planner",
|
||||
"zh-CN": "规划器",
|
||||
"de": "Planer",
|
||||
"ko-KR": "플래너",
|
||||
"no": "Planlegger",
|
||||
"zh-TW": "規劃工具",
|
||||
"ar": "المخطط",
|
||||
"fr": "Planificateur",
|
||||
"it": "Pianificatore",
|
||||
"pt": "Planejador",
|
||||
"es": "Planificador",
|
||||
"ja": "プランナー",
|
||||
"tr": "Planlayıcı"
|
||||
},
|
||||
"WORKSPACE$JUPYTER_TAB_LABEL": {
|
||||
"en": "Jupyter",
|
||||
"zh-CN": "Jupyter",
|
||||
"de": "Jupyter",
|
||||
"ko-KR": "Jupyter",
|
||||
"no": "Jupyter",
|
||||
"zh-TW": "Jupyter",
|
||||
"ar": "Jupyter",
|
||||
"fr": "Jupyter",
|
||||
"it": "Jupyter",
|
||||
"pt": "Jupyter",
|
||||
"es": "Jupyter",
|
||||
"ja": "Jupyter",
|
||||
"tr": "Jupyter"
|
||||
},
|
||||
"WORKSPACE$CODE_EDITOR_TAB_LABEL": {
|
||||
"en": "Code Editor",
|
||||
"zh-CN": "代码编辑器",
|
||||
"de": "Code-Editor",
|
||||
"ko-KR": "코드 편집기",
|
||||
"no": "Kode editor",
|
||||
"zh-TW": "程式碼編輯器",
|
||||
"ar": "محرر الكود",
|
||||
"fr": "Éditeur de code",
|
||||
"it": "Editor di codice",
|
||||
"pt": "Editor de código",
|
||||
"es": "Editor de código",
|
||||
"ja": "コードエディタ",
|
||||
"tr": "Kod Editörü"
|
||||
},
|
||||
"WORKSPACE$BROWSER_TAB_LABEL": {
|
||||
"en": "Browser",
|
||||
"zh-CN": "浏览器",
|
||||
"de": "Browser",
|
||||
"ko-KR": "브라우저",
|
||||
"no": "Nettleser",
|
||||
"zh-TW": "瀏覽器",
|
||||
"it": "Browser",
|
||||
"pt": "Navegador",
|
||||
"es": "Navegador",
|
||||
"ar": "المتصفح",
|
||||
"fr": "Navigateur",
|
||||
"tr": "Tarayıcı",
|
||||
"ja": "ブラウザ"
|
||||
},
|
||||
"WORKSPACE$REFRESH": {
|
||||
"en": "Refresh",
|
||||
"de": "Aktualisieren",
|
||||
"zh-CN": "刷新",
|
||||
"zh-TW": "重新整理",
|
||||
"ko-KR": "새로고침",
|
||||
"no": "Oppdater",
|
||||
"it": "Aggiorna",
|
||||
"pt": "Atualizar",
|
||||
"es": "Actualizar",
|
||||
"ar": "تحديث",
|
||||
"fr": "Rafraîchir",
|
||||
"tr": "Yenile",
|
||||
"ja": "更新"
|
||||
},
|
||||
"WORKSPACE$OPEN": {
|
||||
"en": "Open workspace",
|
||||
"de": "Arbeitsbereich öffnen",
|
||||
"zh-CN": "打开工作区",
|
||||
"zh-TW": "開啟工作區",
|
||||
"ko-KR": "작업 공간 열기",
|
||||
"no": "Åpne arbeidsområde",
|
||||
"it": "Apri area di lavoro",
|
||||
"pt": "Abrir espaço de trabalho",
|
||||
"es": "Abrir espacio de trabajo",
|
||||
"ar": "فتح مساحة العمل",
|
||||
"fr": "Ouvrir l'espace de travail",
|
||||
"tr": "Çalışma alanını aç",
|
||||
"ja": "ワークスペースを開く"
|
||||
},
|
||||
"WORKSPACE$CLOSE": {
|
||||
"en": "Close workspace",
|
||||
"de": "Arbeitsbereich schließen",
|
||||
"zh-CN": "关闭工作区",
|
||||
"zh-TW": "關閉工作區",
|
||||
"ko-KR": "작업 공간 닫기",
|
||||
"no": "Lukk arbeidsområde",
|
||||
"it": "Chiudi area di lavoro",
|
||||
"pt": "Fechar espaço de trabalho",
|
||||
"es": "Cerrar espacio de trabajo",
|
||||
"ar": "إغلاق مساحة العمل",
|
||||
"fr": "Fermer l'espace de travail",
|
||||
"tr": "Çalışma alanını kapat",
|
||||
"ja": "ワークスペースを閉じる"
|
||||
},
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
"VSCODE$OPEN": {
|
||||
"en": "Open in VS Code",
|
||||
"ja": "VS Codeで開く",
|
||||
@@ -1066,6 +1179,81 @@
|
||||
"fr": "Ouvrir dans VS Code",
|
||||
"tr": "VS Code'da aç"
|
||||
},
|
||||
"VSCODE$TITLE": {
|
||||
"en": "VS Code",
|
||||
"ja": "VS Code",
|
||||
"zh-CN": "VS Code",
|
||||
"zh-TW": "VS Code",
|
||||
"ko-KR": "VS Code",
|
||||
"de": "VS Code",
|
||||
"no": "VS Code",
|
||||
"it": "VS Code",
|
||||
"pt": "VS Code",
|
||||
"es": "VS Code",
|
||||
"ar": "VS Code",
|
||||
"fr": "VS Code",
|
||||
"tr": "VS Code"
|
||||
},
|
||||
"VSCODE$LOADING": {
|
||||
"en": "Loading VS Code...",
|
||||
"ja": "VS Codeを読み込み中...",
|
||||
"zh-CN": "正在加载VS Code...",
|
||||
"zh-TW": "正在加載VS Code...",
|
||||
"ko-KR": "VS Code 로딩 중...",
|
||||
"de": "VS Code wird geladen...",
|
||||
"no": "Laster VS Code...",
|
||||
"it": "Caricamento di VS Code...",
|
||||
"pt": "Carregando VS Code...",
|
||||
"es": "Cargando VS Code...",
|
||||
"ar": "جاري تحميل VS Code...",
|
||||
"fr": "Chargement de VS Code...",
|
||||
"tr": "VS Code yükleniyor..."
|
||||
},
|
||||
"VSCODE$URL_NOT_AVAILABLE": {
|
||||
"en": "VS Code URL not available",
|
||||
"ja": "VS Code URLが利用できません",
|
||||
"zh-CN": "VS Code URL不可用",
|
||||
"zh-TW": "VS Code URL不可用",
|
||||
"ko-KR": "VS Code URL을 사용할 수 없습니다",
|
||||
"de": "VS Code URL nicht verfügbar",
|
||||
"no": "VS Code URL ikke tilgjengelig",
|
||||
"it": "URL di VS Code non disponibile",
|
||||
"pt": "URL do VS Code não disponível",
|
||||
"es": "URL de VS Code no disponible",
|
||||
"ar": "رابط VS Code غير متوفر",
|
||||
"fr": "URL VS Code non disponible",
|
||||
"tr": "VS Code URL'si mevcut değil"
|
||||
},
|
||||
"VSCODE$FETCH_ERROR": {
|
||||
"en": "Failed to fetch VS Code URL",
|
||||
"ja": "VS Code URLの取得に失敗しました",
|
||||
"zh-CN": "获取VS Code URL失败",
|
||||
"zh-TW": "獲取VS Code URL失敗",
|
||||
"ko-KR": "VS Code URL을 가져오지 못했습니다",
|
||||
"de": "Fehler beim Abrufen der VS Code URL",
|
||||
"no": "Kunne ikke hente VS Code URL",
|
||||
"it": "Impossibile recuperare l'URL di VS Code",
|
||||
"pt": "Falha ao buscar URL do VS Code",
|
||||
"es": "Error al obtener la URL de VS Code",
|
||||
"ar": "فشل في جلب رابط VS Code",
|
||||
"fr": "Échec de la récupération de l'URL VS Code",
|
||||
"tr": "VS Code URL'si alınamadı"
|
||||
},
|
||||
"VSCODE$IFRAME_PERMISSIONS": {
|
||||
"en": "clipboard-read; clipboard-write",
|
||||
"ja": "clipboard-read; clipboard-write",
|
||||
"zh-CN": "clipboard-read; clipboard-write",
|
||||
"zh-TW": "clipboard-read; clipboard-write",
|
||||
"ko-KR": "clipboard-read; clipboard-write",
|
||||
"de": "clipboard-read; clipboard-write",
|
||||
"no": "clipboard-read; clipboard-write",
|
||||
"it": "clipboard-read; clipboard-write",
|
||||
"pt": "clipboard-read; clipboard-write",
|
||||
"es": "clipboard-read; clipboard-write",
|
||||
"ar": "clipboard-read; clipboard-write",
|
||||
"fr": "clipboard-read; clipboard-write",
|
||||
"tr": "clipboard-read; clipboard-write"
|
||||
},
|
||||
"INCREASE_TEST_COVERAGE": {
|
||||
"en": "Increase test coverage",
|
||||
"ja": "テストカバレッジを向上",
|
||||
@@ -1231,21 +1419,6 @@
|
||||
"tr": "Kapat",
|
||||
"de": "Schließen"
|
||||
},
|
||||
"BUTTON$RESET_TO_DEFAULTS": {
|
||||
"en": "Reset to defaults",
|
||||
"ja": "デフォルトにリセット",
|
||||
"zh-CN": "重置为默认值",
|
||||
"zh-TW": "還原為預設值",
|
||||
"ko-KR": "기본값으로 재설정",
|
||||
"no": "Tilbakestill til standard",
|
||||
"it": "Ripristina valori predefiniti",
|
||||
"pt": "Restaurar padrões",
|
||||
"es": "Restablecer valores predeterminados",
|
||||
"ar": "إعادة التعيين إلى الإعدادات الافتراضية",
|
||||
"fr": "Réinitialiser aux valeurs par défaut",
|
||||
"tr": "Varsayılanlara sıfırla",
|
||||
"de": "Auf Standardwerte zurücksetzen"
|
||||
},
|
||||
"MODAL$CONFIRM_RESET_TITLE": {
|
||||
"en": "Are you sure?",
|
||||
"ja": "本当によろしいですか?",
|
||||
@@ -1426,6 +1599,51 @@
|
||||
"tr": "Jetonunuzu alın",
|
||||
"de": "Token abrufen"
|
||||
},
|
||||
"GITHUB$TOKEN_HELP_TEXT": {
|
||||
"en": "Get your <0>GitHub token</0> or <1>click here for instructions</1>",
|
||||
"ja": "<0>GitHubトークン</0>を取得するか、<1>手順についてはここをクリック</1>",
|
||||
"zh-CN": "获取您的<0>GitHub令牌</0>或<1>点击此处获取说明</1>",
|
||||
"zh-TW": "取得您的<0>GitHub權杖</0>或<1>點擊此處獲取說明</1>",
|
||||
"ko-KR": "<0>GitHub 토큰</0>을 받거나 <1>지침을 보려면 여기를 클릭</1>",
|
||||
"no": "Få din <0>GitHub-token</0> eller <1>klikk her for instruksjoner</1>",
|
||||
"it": "Ottieni il tuo <0>token GitHub</0> o <1>clicca qui per istruzioni</1>",
|
||||
"pt": "Obtenha seu <0>token GitHub</0> ou <1>clique aqui para instruções</1>",
|
||||
"es": "Obtenga su <0>token de GitHub</0> o <1>haga clic aquí para obtener instrucciones</1>",
|
||||
"ar": "احصل على <0>رمز GitHub</0> الخاص بك أو <1>انقر هنا للحصول على تعليمات</1>",
|
||||
"fr": "Obtenez votre <0>jeton GitHub</0> ou <1>cliquez ici pour les instructions</1>",
|
||||
"tr": "<0>GitHub jetonu</0> alın veya <1>talimatlar için buraya tıklayın</1>",
|
||||
"de": "Holen Sie sich Ihren <0>GitHub-Token</0> oder <1>klicken Sie hier für Anweisungen</1>"
|
||||
},
|
||||
"GITHUB$TOKEN_LINK_TEXT": {
|
||||
"en": "GitHub token",
|
||||
"ja": "GitHubトークン",
|
||||
"zh-CN": "GitHub令牌",
|
||||
"zh-TW": "GitHub權杖",
|
||||
"ko-KR": "GitHub 토큰",
|
||||
"no": "GitHub-token",
|
||||
"it": "token GitHub",
|
||||
"pt": "token GitHub",
|
||||
"es": "token de GitHub",
|
||||
"ar": "رمز GitHub",
|
||||
"fr": "jeton GitHub",
|
||||
"tr": "GitHub jetonu",
|
||||
"de": "GitHub-Token"
|
||||
},
|
||||
"GITHUB$INSTRUCTIONS_LINK_TEXT": {
|
||||
"en": "click here for instructions",
|
||||
"ja": "手順についてはここをクリック",
|
||||
"zh-CN": "点击此处获取说明",
|
||||
"zh-TW": "點擊此處獲取說明",
|
||||
"ko-KR": "지침을 보려면 여기를 클릭",
|
||||
"no": "klikk her for instruksjoner",
|
||||
"it": "clicca qui per istruzioni",
|
||||
"pt": "clique aqui para instruções",
|
||||
"es": "haga clic aquí para obtener instrucciones",
|
||||
"ar": "انقر هنا للحصول على تعليمات",
|
||||
"fr": "cliquez ici pour les instructions",
|
||||
"tr": "talimatlar için buraya tıklayın",
|
||||
"de": "klicken Sie hier für Anweisungen"
|
||||
},
|
||||
"COMMON$HERE": {
|
||||
"en": "here",
|
||||
"ja": "こちら",
|
||||
@@ -2005,66 +2223,10 @@
|
||||
"tr": "Dosyaları buraya bırakın",
|
||||
"ja": "ここにファイルをドロップ"
|
||||
},
|
||||
"EXPLORER$LABEL_WORKSPACE": {
|
||||
"en": "Workspace",
|
||||
"zh-CN": "工作区",
|
||||
"de": "Arbeitsbereich",
|
||||
"zh-TW": "工作區",
|
||||
"es": "Espacio de trabajo",
|
||||
"fr": "Espace de travail",
|
||||
"it": "Area di lavoro",
|
||||
"pt": "Espaço de trabalho",
|
||||
"ko-KR": "워크스페이스",
|
||||
"ar": "مساحة العمل",
|
||||
"tr": "Çalışma alanı",
|
||||
"no": "Arbeidsområde",
|
||||
"ja": "ワークスペース"
|
||||
},
|
||||
"EXPLORER$EMPTY_WORKSPACE_MESSAGE": {
|
||||
"en": "No files in workspace",
|
||||
"zh-CN": "工作区为空",
|
||||
"de": "Keine Dateien im Arbeitsbereich",
|
||||
"zh-TW": "工作區為空",
|
||||
"es": "No hay archivos en el espacio de trabajo",
|
||||
"fr": "Aucun fichier dans l'espace de travail",
|
||||
"it": "Nessun file nell'area di lavoro",
|
||||
"pt": "Nenhum arquivo no espaço de trabalho",
|
||||
"ko-KR": "워크스페이스가 비어 있습니다",
|
||||
"ar": "لا توجد ملفات في مساحة العمل",
|
||||
"tr": "Çalışma alanında dosya yok",
|
||||
"no": "Ingen filer i arbeidsområdet",
|
||||
"ja": "ワークスペースにファイルがありません"
|
||||
},
|
||||
"EXPLORER$LOADING_WORKSPACE_MESSAGE": {
|
||||
"en": "Loading workspace...",
|
||||
"zh-CN": "加载工作区中...",
|
||||
"de": "Arbeitsbereich wird geladen...",
|
||||
"zh-TW": "載入工作區中...",
|
||||
"es": "Cargando espacio de trabajo...",
|
||||
"fr": "Chargement de l'espace de travail...",
|
||||
"it": "Caricamento dell'area di lavoro...",
|
||||
"pt": "Carregando espaço de trabalho...",
|
||||
"ko-KR": "워크스페이스 로딩 중...",
|
||||
"ar": "جارٍ تحميل مساحة العمل...",
|
||||
"tr": "Çalışma alanı yükleniyor...",
|
||||
"no": "Laster arbeidsområde...",
|
||||
"ja": "ワークスペースを読み込み中..."
|
||||
},
|
||||
"EXPLORER$REFRESH_ERROR_MESSAGE": {
|
||||
"en": "Error refreshing workspace",
|
||||
"zh-CN": "刷新时出错",
|
||||
"de": "Fehler beim Aktualisieren des Arbeitsbereichs",
|
||||
"zh-TW": "重新整理時發生錯誤",
|
||||
"es": "Error al actualizar el espacio de trabajo",
|
||||
"fr": "Erreur lors de l'actualisation de l'espace de travail",
|
||||
"it": "Errore durante l'aggiornamento dell'area di lavoro",
|
||||
"pt": "Erro ao atualizar o espaço de trabalho",
|
||||
"ko-KR": "새로고침 중 오류 발생",
|
||||
"ar": "خطأ في تحديث مساحة العمل",
|
||||
"tr": "Çalışma alanı yenilenirken hata oluştu",
|
||||
"no": "Feil ved oppdatering av arbeidsområde",
|
||||
"ja": "ワークスペースの更新中にエラーが発生しました"
|
||||
},
|
||||
|
||||
|
||||
|
||||
|
||||
"EXPLORER$UPLOAD_SUCCESS_MESSAGE": {
|
||||
"en": "Successfully uploaded {{count}} file(s)",
|
||||
"zh-CN": "上传成功",
|
||||
@@ -4073,6 +4235,21 @@
|
||||
"BUTTON$COPY_TO_CLIPBOARD": {
|
||||
"en": "Copy to Clipboard"
|
||||
},
|
||||
"BUTTON$REFRESH": {
|
||||
"en": "Refresh",
|
||||
"ja": "更新",
|
||||
"zh-CN": "刷新",
|
||||
"zh-TW": "重新整理",
|
||||
"ko-KR": "새로고침",
|
||||
"no": "Oppdater",
|
||||
"it": "Aggiorna",
|
||||
"pt": "Atualizar",
|
||||
"es": "Actualizar",
|
||||
"ar": "تحديث",
|
||||
"fr": "Rafraîchir",
|
||||
"tr": "Yenile",
|
||||
"de": "Aktualisieren"
|
||||
},
|
||||
"ERROR$REQUIRED_FIELD": {
|
||||
"en": "This field is required"
|
||||
},
|
||||
@@ -4541,21 +4718,6 @@
|
||||
"pt": "Fechar",
|
||||
"tr": "Kapat"
|
||||
},
|
||||
"SETTINGS_FORM$RESET_TO_DEFAULTS_LABEL": {
|
||||
"en": "Reset to defaults",
|
||||
"es": "Reiniciar valores por defect",
|
||||
"zh-CN": "重置为默认值",
|
||||
"zh-TW": "還原為預設值",
|
||||
"ko-KR": "기본값으로 재설정",
|
||||
"ja": "デフォルトに戻す",
|
||||
"no": "Tilbakestill til standardverdier",
|
||||
"ar": "إعادة التعيين إلى الإعدادات الافتراضية",
|
||||
"de": "Auf Standardwerte zurücksetzen",
|
||||
"fr": "Réinitialiser aux valeurs par défaut",
|
||||
"it": "Ripristina valori predefiniti",
|
||||
"pt": "Restaurar padrões",
|
||||
"tr": "Varsayılanlara sıfırla"
|
||||
},
|
||||
"SETTINGS_FORM$CANCEL_LABEL": {
|
||||
"en": "Cancel",
|
||||
"es": "Cancelar",
|
||||
@@ -4826,6 +4988,9 @@
|
||||
"es": "La acción expiró",
|
||||
"tr": "İşlem zaman aşımına uğradı"
|
||||
},
|
||||
"AGENT_ERROR$TOO_MANY_CONVERSATIONS": {
|
||||
"en": "Too many conversations at once."
|
||||
},
|
||||
"PROJECT_MENU_CARD_CONTEXT_MENU$CONNECT_TO_GITHUB_LABEL": {
|
||||
"en": "Connect to GitHub",
|
||||
"es": "Conectar a GitHub",
|
||||
@@ -4962,51 +5127,9 @@
|
||||
"es": "Subir archivo",
|
||||
"tr": "Dosya yükle"
|
||||
},
|
||||
"FILE_EXPLORER$REFRESH_WORKSPACE": {
|
||||
"en": "Refresh workspace",
|
||||
"zh-CN": "刷新工作区",
|
||||
"zh-TW": "重新整理工作區",
|
||||
"ko-KR": "워크스페이스 새로고침",
|
||||
"ja": "ワークスペースを更新",
|
||||
"no": "Oppdater arbeidsområde",
|
||||
"ar": "تحديث مساحة العمل",
|
||||
"de": "Arbeitsbereich aktualisieren",
|
||||
"fr": "Actualiser l'espace de travail",
|
||||
"it": "Aggiorna area di lavoro",
|
||||
"pt": "Atualizar área de trabalho",
|
||||
"es": "Actualizar espacio de trabajo",
|
||||
"tr": "Çalışma alanını yenile"
|
||||
},
|
||||
"FILE_EXPLORER$OPEN_WORKSPACE": {
|
||||
"en": "Open workspace",
|
||||
"zh-CN": "打开工作区",
|
||||
"zh-TW": "開啟工作區",
|
||||
"ko-KR": "워크스페이스 열기",
|
||||
"ja": "ワークスペースを開く",
|
||||
"no": "Åpne arbeidsområde",
|
||||
"ar": "فتح مساحة العمل",
|
||||
"de": "Arbeitsbereich öffnen",
|
||||
"fr": "Ouvrir l'espace de travail",
|
||||
"it": "Apri area di lavoro",
|
||||
"pt": "Abrir área de trabalho",
|
||||
"es": "Abrir espacio de trabajo",
|
||||
"tr": "Çalışma alanını aç"
|
||||
},
|
||||
"FILE_EXPLORER$CLOSE_WORKSPACE": {
|
||||
"en": "Close workspace",
|
||||
"zh-CN": "关闭工作区",
|
||||
"zh-TW": "關閉工作區",
|
||||
"ko-KR": "워크스페이스 닫기",
|
||||
"ja": "ワークスペースを閉じる",
|
||||
"no": "Lukk arbeidsområde",
|
||||
"ar": "إغلاق مساحة العمل",
|
||||
"de": "Arbeitsbereich schließen",
|
||||
"fr": "Fermer l'espace de travail",
|
||||
"it": "Chiudi area di lavoro",
|
||||
"pt": "Fechar área de trabalho",
|
||||
"es": "Cerrar espacio de trabajo",
|
||||
"tr": "Çalışma alanını kapat"
|
||||
},
|
||||
|
||||
|
||||
|
||||
"ACTION_MESSAGE$RUN": {
|
||||
"en": "Running <cmd>{{action.payload.args.command}}</cmd>",
|
||||
"zh-CN": "运行 <cmd>{{action.payload.args.command}}</cmd>",
|
||||
@@ -6361,6 +6484,51 @@
|
||||
"tr": "Üzerinde bir jeton oluştur",
|
||||
"de": "Token generieren auf"
|
||||
},
|
||||
"GITLAB$TOKEN_HELP_TEXT": {
|
||||
"en": "Get your <0>GitLab token</0> or <1>click here for instructions</1>",
|
||||
"ja": "<0>GitLabトークン</0>を取得するか、<1>手順についてはここをクリック</1>",
|
||||
"zh-CN": "获取您的<0>GitLab令牌</0>或<1>点击此处获取说明</1>",
|
||||
"zh-TW": "取得您的<0>GitLab權杖</0>或<1>點擊此處獲取說明</1>",
|
||||
"ko-KR": "<0>GitLab 토큰</0>을 받거나 <1>지침을 보려면 여기를 클릭</1>",
|
||||
"no": "Få din <0>GitLab-token</0> eller <1>klikk her for instruksjoner</1>",
|
||||
"it": "Ottieni il tuo <0>token GitLab</0> o <1>clicca qui per istruzioni</1>",
|
||||
"pt": "Obtenha seu <0>token GitLab</0> ou <1>clique aqui para instruções</1>",
|
||||
"es": "Obtenga su <0>token de GitLab</0> o <1>haga clic aquí para obtener instrucciones</1>",
|
||||
"ar": "احصل على <0>رمز GitLab</0> الخاص بك أو <1>انقر هنا للحصول على تعليمات</1>",
|
||||
"fr": "Obtenez votre <0>jeton GitLab</0> ou <1>cliquez ici pour les instructions</1>",
|
||||
"tr": "<0>GitLab jetonu</0> alın veya <1>talimatlar için buraya tıklayın</1>",
|
||||
"de": "Holen Sie sich Ihren <0>GitLab-Token</0> oder <1>klicken Sie hier für Anweisungen</1>"
|
||||
},
|
||||
"GITLAB$TOKEN_LINK_TEXT": {
|
||||
"en": "GitLab token",
|
||||
"ja": "GitLabトークン",
|
||||
"zh-CN": "GitLab令牌",
|
||||
"zh-TW": "GitLab權杖",
|
||||
"ko-KR": "GitLab 토큰",
|
||||
"no": "GitLab-token",
|
||||
"it": "token GitLab",
|
||||
"pt": "token GitLab",
|
||||
"es": "token de GitLab",
|
||||
"ar": "رمز GitLab",
|
||||
"fr": "jeton GitLab",
|
||||
"tr": "GitLab jetonu",
|
||||
"de": "GitLab-Token"
|
||||
},
|
||||
"GITLAB$INSTRUCTIONS_LINK_TEXT": {
|
||||
"en": "click here for instructions",
|
||||
"ja": "手順についてはここをクリック",
|
||||
"zh-CN": "点击此处获取说明",
|
||||
"zh-TW": "點擊此處獲取說明",
|
||||
"ko-KR": "지침을 보려면 여기를 클릭",
|
||||
"no": "klikk her for instruksjoner",
|
||||
"it": "clicca qui per istruzioni",
|
||||
"pt": "clique aqui para instruções",
|
||||
"es": "haga clic aquí para obtener instrucciones",
|
||||
"ar": "انقر هنا للحصول على تعليمات",
|
||||
"fr": "cliquez ici pour les instructions",
|
||||
"tr": "talimatlar için buraya tıklayın",
|
||||
"de": "klicken Sie hier für Anweisungen"
|
||||
},
|
||||
"GITLAB$OR_SEE": {
|
||||
"en": "or see the",
|
||||
"ja": "または参照",
|
||||
@@ -6586,53 +6754,4 @@
|
||||
"es": "No hay herramientas disponibles para este agente",
|
||||
"tr": "Bu ajan için kullanılabilir araç yok"
|
||||
}
|
||||
,
|
||||
"TOS$ACCEPT_TERMS_OF_SERVICE": {
|
||||
"en": "Accept Terms of Service",
|
||||
"ja": "利用規約に同意する",
|
||||
"zh-CN": "接受服务条款",
|
||||
"zh-TW": "接受服務條款",
|
||||
"ko-KR": "서비스 약관 동의",
|
||||
"fr": "Accepter les conditions d'utilisation",
|
||||
"es": "Aceptar términos de servicio",
|
||||
"de": "Nutzungsbedingungen akzeptieren",
|
||||
"it": "Accetta i termini di servizio",
|
||||
"pt": "Aceitar termos de serviço"
|
||||
},
|
||||
"TOS$ACCEPT_TERMS_DESCRIPTION": {
|
||||
"en": "Please review and accept our terms of service before continuing",
|
||||
"ja": "続行する前に利用規約を確認して同意してください",
|
||||
"zh-CN": "请在继续之前查看并接受我们的服务条款",
|
||||
"zh-TW": "請在繼續之前查看並接受我們的服務條款",
|
||||
"ko-KR": "계속하기 전에 서비스 약관을 검토하고 동의해 주세요",
|
||||
"fr": "Veuillez examiner et accepter nos conditions d'utilisation avant de continuer",
|
||||
"es": "Por favor, revise y acepte nuestros términos de servicio antes de continuar",
|
||||
"de": "Bitte überprüfen und akzeptieren Sie unsere Nutzungsbedingungen, bevor Sie fortfahren",
|
||||
"it": "Si prega di rivedere e accettare i nostri termini di servizio prima di continuare",
|
||||
"pt": "Por favor, revise e aceite nossos termos de serviço antes de continuar"
|
||||
},
|
||||
"TOS$CONTINUE": {
|
||||
"en": "Continue",
|
||||
"ja": "続行",
|
||||
"zh-CN": "继续",
|
||||
"zh-TW": "繼續",
|
||||
"ko-KR": "계속",
|
||||
"fr": "Continuer",
|
||||
"es": "Continuar",
|
||||
"de": "Fortfahren",
|
||||
"it": "Continua",
|
||||
"pt": "Continuar"
|
||||
},
|
||||
"TOS$ERROR_ACCEPTING": {
|
||||
"en": "Error accepting Terms of Service",
|
||||
"ja": "利用規約の承諾中にエラーが発生しました",
|
||||
"zh-CN": "接受服务条款时出错",
|
||||
"zh-TW": "接受服務條款時出錯",
|
||||
"ko-KR": "서비스 약관 수락 중 오류 발생",
|
||||
"fr": "Erreur lors de l'acceptation des conditions d'utilisation",
|
||||
"es": "Error al aceptar los Términos de Servicio",
|
||||
"de": "Fehler beim Akzeptieren der Nutzungsbedingungen",
|
||||
"it": "Errore nell'accettazione dei Termini di Servizio",
|
||||
"pt": "Erro ao aceitar os Termos de Serviço"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,15 @@ const MOCK_USER_PREFERENCES: {
|
||||
settings: null,
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the user settings to the default settings
|
||||
*
|
||||
* Useful for resetting the settings in tests
|
||||
*/
|
||||
export const resetTestHandlersMockSettings = () => {
|
||||
MOCK_USER_PREFERENCES.settings = MOCK_DEFAULT_USER_SETTINGS;
|
||||
};
|
||||
|
||||
const conversations: Conversation[] = [
|
||||
{
|
||||
conversation_id: "1",
|
||||
@@ -80,6 +89,7 @@ const openHandsHandlers = [
|
||||
HttpResponse.json([
|
||||
"gpt-3.5-turbo",
|
||||
"gpt-4o",
|
||||
"gpt-4o-mini",
|
||||
"anthropic/claude-3.5",
|
||||
"anthropic/claude-3-5-sonnet-20241022",
|
||||
]),
|
||||
@@ -173,6 +183,7 @@ export const handlers = [
|
||||
return HttpResponse.json(settings);
|
||||
}),
|
||||
http.post("/api/settings", async ({ request }) => {
|
||||
await delay();
|
||||
const body = await request.json();
|
||||
|
||||
if (body) {
|
||||
|
||||
@@ -7,6 +7,7 @@ const TASKS_1: SuggestedTask[] = [
|
||||
title: "Fix merge conflicts",
|
||||
repo: "octocat/hello-world",
|
||||
task_type: "MERGE_CONFLICTS",
|
||||
git_provider: "github",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -16,54 +17,63 @@ const TASKS_2: SuggestedTask[] = [
|
||||
title: "Fix broken CI checks",
|
||||
repo: "octocat/earth",
|
||||
task_type: "FAILING_CHECKS",
|
||||
git_provider: "github",
|
||||
},
|
||||
{
|
||||
issue_number: 281,
|
||||
title: "Fix issue",
|
||||
repo: "octocat/earth",
|
||||
task_type: "UNRESOLVED_COMMENTS",
|
||||
git_provider: "github",
|
||||
},
|
||||
{
|
||||
issue_number: 293,
|
||||
title: "Update documentation",
|
||||
repo: "octocat/earth",
|
||||
task_type: "OPEN_ISSUE",
|
||||
git_provider: "github",
|
||||
},
|
||||
{
|
||||
issue_number: 305,
|
||||
title: "Refactor user service",
|
||||
repo: "octocat/earth",
|
||||
task_type: "FAILING_CHECKS",
|
||||
git_provider: "github",
|
||||
},
|
||||
{
|
||||
issue_number: 312,
|
||||
title: "Fix styling bug",
|
||||
repo: "octocat/earth",
|
||||
task_type: "FAILING_CHECKS",
|
||||
git_provider: "github",
|
||||
},
|
||||
{
|
||||
issue_number: 327,
|
||||
title: "Add unit tests",
|
||||
repo: "octocat/earth",
|
||||
task_type: "FAILING_CHECKS",
|
||||
git_provider: "github",
|
||||
},
|
||||
{
|
||||
issue_number: 331,
|
||||
title: "Implement dark mode",
|
||||
repo: "octocat/earth",
|
||||
task_type: "FAILING_CHECKS",
|
||||
git_provider: "github",
|
||||
},
|
||||
{
|
||||
issue_number: 345,
|
||||
title: "Optimize build process",
|
||||
repo: "octocat/earth",
|
||||
task_type: "FAILING_CHECKS",
|
||||
git_provider: "github",
|
||||
},
|
||||
{
|
||||
issue_number: 352,
|
||||
title: "Update dependencies",
|
||||
repo: "octocat/earth",
|
||||
task_type: "FAILING_CHECKS",
|
||||
git_provider: "github",
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -8,19 +8,20 @@ import {
|
||||
export default [
|
||||
layout("routes/root-layout.tsx", [
|
||||
index("routes/home.tsx"),
|
||||
route("accept-tos", "routes/accept-tos.tsx"),
|
||||
route("settings", "routes/settings.tsx", [
|
||||
index("routes/account-settings.tsx"),
|
||||
index("routes/llm-settings.tsx"),
|
||||
route("git", "routes/git-settings.tsx"),
|
||||
route("app", "routes/app-settings.tsx"),
|
||||
route("billing", "routes/billing.tsx"),
|
||||
route("api-keys", "routes/api-keys.tsx"),
|
||||
]),
|
||||
route("conversations/:conversationId", "routes/conversation.tsx", [
|
||||
index("routes/editor.tsx"),
|
||||
route("workspace", "routes/editor-tab.tsx"),
|
||||
route("browser", "routes/browser-tab.tsx"),
|
||||
route("jupyter", "routes/jupyter-tab.tsx"),
|
||||
route("served", "routes/served-tab.tsx"),
|
||||
route("terminal", "routes/terminal-tab.tsx"),
|
||||
route("vscode", "routes/vscode-tab.tsx"),
|
||||
]),
|
||||
]),
|
||||
] satisfies RouteConfig;
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate, useSearchParams } from "react-router";
|
||||
import { toast } from "react-toastify";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import AllHandsLogo from "#/assets/branding/all-hands-logo.svg?react";
|
||||
import { TOSCheckbox } from "#/components/features/waitlist/tos-checkbox";
|
||||
import { BrandButton } from "#/components/features/settings/brand-button";
|
||||
import { handleCaptureConsent } from "#/utils/handle-capture-consent";
|
||||
import { openHands } from "#/api/open-hands-axios";
|
||||
|
||||
export default function AcceptTOS() {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const [isTosAccepted, setIsTosAccepted] = React.useState(false);
|
||||
const [isSubmitting, setIsSubmitting] = React.useState(false);
|
||||
|
||||
// Get the redirect URL from the query parameters
|
||||
const redirectUrl = searchParams.get("redirect_url") || "/";
|
||||
|
||||
const handleAcceptTOS = async () => {
|
||||
if (isTosAccepted && !isSubmitting) {
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
|
||||
// Set consent for analytics
|
||||
handleCaptureConsent(true);
|
||||
|
||||
// Call the API to record TOS acceptance in the database
|
||||
const response = await openHands.post("/api/accept_tos", {
|
||||
redirect_url: redirectUrl,
|
||||
});
|
||||
|
||||
// Get the redirect URL from the response
|
||||
const finalRedirectUrl = response.data.redirect_url || redirectUrl;
|
||||
|
||||
// Check if the redirect URL is an external URL (starts with http or https)
|
||||
if (
|
||||
finalRedirectUrl.startsWith("http://") ||
|
||||
finalRedirectUrl.startsWith("https://")
|
||||
) {
|
||||
// For external URLs, redirect using window.location
|
||||
window.location.href = finalRedirectUrl;
|
||||
} else {
|
||||
// For internal routes, use navigate
|
||||
navigate(finalRedirectUrl);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(t(I18nKey.TOS$ERROR_ACCEPTING), error);
|
||||
toast.error(t(I18nKey.ERROR$GENERIC));
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-full">
|
||||
<div className="border border-tertiary p-8 rounded-lg max-w-md w-full flex flex-col gap-6 items-center">
|
||||
<AllHandsLogo width={68} height={46} />
|
||||
|
||||
<div className="flex flex-col gap-2 w-full items-center text-center">
|
||||
<h1 className="text-2xl font-bold">
|
||||
{t(I18nKey.TOS$ACCEPT_TERMS_OF_SERVICE)}
|
||||
</h1>
|
||||
<p className="text-sm text-gray-500">
|
||||
{t(I18nKey.TOS$ACCEPT_TERMS_DESCRIPTION)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<TOSCheckbox onChange={() => setIsTosAccepted((prev) => !prev)} />
|
||||
|
||||
<BrandButton
|
||||
isDisabled={!isTosAccepted || isSubmitting}
|
||||
type="button"
|
||||
variant="primary"
|
||||
onClick={handleAcceptTOS}
|
||||
className="w-full"
|
||||
>
|
||||
{isSubmitting ? t(I18nKey.HOME$LOADING) : t(I18nKey.TOS$CONTINUE)}
|
||||
</BrandButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,585 +0,0 @@
|
||||
import React from "react";
|
||||
import { Link } from "react-router";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { BrandButton } from "#/components/features/settings/brand-button";
|
||||
import { HelpLink } from "#/components/features/settings/help-link";
|
||||
import { KeyStatusIcon } from "#/components/features/settings/key-status-icon";
|
||||
import { SettingsDropdownInput } from "#/components/features/settings/settings-dropdown-input";
|
||||
import { SettingsInput } from "#/components/features/settings/settings-input";
|
||||
import { SettingsSwitch } from "#/components/features/settings/settings-switch";
|
||||
import { LoadingSpinner } from "#/components/shared/loading-spinner";
|
||||
import { ModalBackdrop } from "#/components/shared/modals/modal-backdrop";
|
||||
import { ModelSelector } from "#/components/shared/modals/settings/model-selector";
|
||||
import { useSaveSettings } from "#/hooks/mutation/use-save-settings";
|
||||
import { useAIConfigOptions } from "#/hooks/query/use-ai-config-options";
|
||||
import { useConfig } from "#/hooks/query/use-config";
|
||||
import { useSettings } from "#/hooks/query/use-settings";
|
||||
import { useAppLogout } from "#/hooks/use-app-logout";
|
||||
import { AvailableLanguages } from "#/i18n";
|
||||
import { DEFAULT_SETTINGS } from "#/services/settings";
|
||||
import { handleCaptureConsent } from "#/utils/handle-capture-consent";
|
||||
import { hasAdvancedSettingsSet } from "#/utils/has-advanced-settings-set";
|
||||
import { isCustomModel } from "#/utils/is-custom-model";
|
||||
import { organizeModelsAndProviders } from "#/utils/organize-models-and-providers";
|
||||
import { retrieveAxiosErrorMessage } from "#/utils/retrieve-axios-error-message";
|
||||
import {
|
||||
displayErrorToast,
|
||||
displaySuccessToast,
|
||||
} from "#/utils/custom-toast-handlers";
|
||||
import { ProviderOptions } from "#/types/settings";
|
||||
import { useAuth } from "#/context/auth-context";
|
||||
|
||||
// Define REMOTE_RUNTIME_OPTIONS for testing
|
||||
const REMOTE_RUNTIME_OPTIONS = [
|
||||
{ key: "1", label: "Standard" },
|
||||
{ key: "2", label: "Enhanced" },
|
||||
{ key: "4", label: "Premium" },
|
||||
];
|
||||
|
||||
function AccountSettings() {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
data: settings,
|
||||
isFetching: isFetchingSettings,
|
||||
isFetched,
|
||||
isSuccess: isSuccessfulSettings,
|
||||
} = useSettings();
|
||||
const { data: config } = useConfig();
|
||||
const {
|
||||
data: resources,
|
||||
isFetching: isFetchingResources,
|
||||
isSuccess: isSuccessfulResources,
|
||||
} = useAIConfigOptions();
|
||||
const { mutate: saveSettings } = useSaveSettings();
|
||||
const { handleLogout } = useAppLogout();
|
||||
const { providerTokensSet, providersAreSet } = useAuth();
|
||||
|
||||
const isFetching = isFetchingSettings || isFetchingResources;
|
||||
const isSuccess = isSuccessfulSettings && isSuccessfulResources;
|
||||
|
||||
const isSaas = config?.APP_MODE === "saas";
|
||||
const shouldHandleSpecialSaasCase =
|
||||
config?.FEATURE_FLAGS.HIDE_LLM_SETTINGS && isSaas;
|
||||
|
||||
const determineWhetherToToggleAdvancedSettings = () => {
|
||||
if (shouldHandleSpecialSaasCase) return true;
|
||||
|
||||
if (isSuccess) {
|
||||
return (
|
||||
isCustomModel(resources.models, settings.LLM_MODEL) ||
|
||||
hasAdvancedSettingsSet({
|
||||
...settings,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const hasAppSlug = !!config?.APP_SLUG;
|
||||
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(
|
||||
resources?.models || [],
|
||||
);
|
||||
|
||||
const [llmConfigMode, setLlmConfigMode] = React.useState<
|
||||
"basic" | "advanced"
|
||||
>(isAdvancedSettingsSet ? "advanced" : "basic");
|
||||
const [confirmationModeIsEnabled, setConfirmationModeIsEnabled] =
|
||||
React.useState(!!settings?.SECURITY_ANALYZER);
|
||||
const [resetSettingsModalIsOpen, setResetSettingsModalIsOpen] =
|
||||
React.useState(false);
|
||||
|
||||
const formRef = React.useRef<HTMLFormElement>(null);
|
||||
|
||||
const onSubmit = async (formData: FormData) => {
|
||||
const languageLabel = formData.get("language-input")?.toString();
|
||||
const languageValue = AvailableLanguages.find(
|
||||
({ label }) => label === languageLabel,
|
||||
)?.value;
|
||||
|
||||
const llmProvider = formData.get("llm-provider-input")?.toString();
|
||||
const llmModel = formData.get("llm-model-input")?.toString();
|
||||
const fullLlmModel = `${llmProvider}/${llmModel}`.toLowerCase();
|
||||
const customLlmModel = formData.get("llm-custom-model-input")?.toString();
|
||||
|
||||
const rawRemoteRuntimeResourceFactor = formData
|
||||
.get("runtime-settings-input")
|
||||
?.toString();
|
||||
const remoteRuntimeResourceFactor = REMOTE_RUNTIME_OPTIONS.find(
|
||||
({ label }) => label === rawRemoteRuntimeResourceFactor,
|
||||
)?.key;
|
||||
|
||||
const userConsentsToAnalytics =
|
||||
formData.get("enable-analytics-switch")?.toString() === "on";
|
||||
const enableMemoryCondenser =
|
||||
formData.get("enable-memory-condenser-switch")?.toString() === "on";
|
||||
const enableSoundNotifications =
|
||||
formData.get("enable-sound-notifications-switch")?.toString() === "on";
|
||||
const llmBaseUrl = formData.get("base-url-input")?.toString().trim() || "";
|
||||
const inputApiKey = formData.get("llm-api-key-input")?.toString() || "";
|
||||
const llmApiKey =
|
||||
inputApiKey === "" && isLLMKeySet
|
||||
? undefined // don't update if it's already set and input is empty
|
||||
: inputApiKey; // otherwise use the input value
|
||||
|
||||
const githubToken = formData.get("github-token-input")?.toString();
|
||||
const gitlabToken = formData.get("gitlab-token-input")?.toString();
|
||||
// we don't want the user to be able to modify these settings in SaaS
|
||||
const finalLlmModel = shouldHandleSpecialSaasCase
|
||||
? undefined
|
||||
: customLlmModel || fullLlmModel;
|
||||
const finalLlmBaseUrl = shouldHandleSpecialSaasCase
|
||||
? undefined
|
||||
: llmBaseUrl;
|
||||
const finalLlmApiKey = shouldHandleSpecialSaasCase ? undefined : llmApiKey;
|
||||
|
||||
const newSettings = {
|
||||
provider_tokens:
|
||||
githubToken || gitlabToken
|
||||
? {
|
||||
github: githubToken || "",
|
||||
gitlab: gitlabToken || "",
|
||||
}
|
||||
: undefined,
|
||||
LANGUAGE: languageValue,
|
||||
user_consents_to_analytics: userConsentsToAnalytics,
|
||||
ENABLE_DEFAULT_CONDENSER: enableMemoryCondenser,
|
||||
ENABLE_SOUND_NOTIFICATIONS: enableSoundNotifications,
|
||||
LLM_MODEL: finalLlmModel,
|
||||
LLM_BASE_URL: finalLlmBaseUrl,
|
||||
llm_api_key: finalLlmApiKey,
|
||||
AGENT: formData.get("agent-input")?.toString(),
|
||||
SECURITY_ANALYZER:
|
||||
formData.get("security-analyzer-input")?.toString() || "",
|
||||
REMOTE_RUNTIME_RESOURCE_FACTOR:
|
||||
remoteRuntimeResourceFactor !== null
|
||||
? Number(remoteRuntimeResourceFactor)
|
||||
: DEFAULT_SETTINGS.REMOTE_RUNTIME_RESOURCE_FACTOR,
|
||||
CONFIRMATION_MODE: confirmationModeIsEnabled,
|
||||
};
|
||||
|
||||
saveSettings(newSettings, {
|
||||
onSuccess: () => {
|
||||
handleCaptureConsent(userConsentsToAnalytics);
|
||||
displaySuccessToast(t(I18nKey.SETTINGS$SAVED));
|
||||
setLlmConfigMode(isAdvancedSettingsSet ? "advanced" : "basic");
|
||||
},
|
||||
onError: (error) => {
|
||||
const errorMessage = retrieveAxiosErrorMessage(error);
|
||||
displayErrorToast(errorMessage || t(I18nKey.ERROR$GENERIC));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
saveSettings(null, {
|
||||
onSuccess: () => {
|
||||
displaySuccessToast(t(I18nKey.SETTINGS$RESET));
|
||||
setResetSettingsModalIsOpen(false);
|
||||
setLlmConfigMode("basic");
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
// If settings is still loading by the time the state is set, it will always
|
||||
// default to basic settings. This is a workaround to ensure the correct
|
||||
// settings are displayed.
|
||||
setLlmConfigMode(isAdvancedSettingsSet ? "advanced" : "basic");
|
||||
}, [isAdvancedSettingsSet]);
|
||||
|
||||
if (isFetched && !settings) {
|
||||
return <div>Failed to fetch settings. Please try reloading.</div>;
|
||||
}
|
||||
|
||||
const onToggleAdvancedMode = (isToggled: boolean) => {
|
||||
setLlmConfigMode(isToggled ? "advanced" : "basic");
|
||||
if (!isToggled) {
|
||||
// reset advanced state
|
||||
setConfirmationModeIsEnabled(!!settings?.SECURITY_ANALYZER);
|
||||
}
|
||||
};
|
||||
|
||||
if (isFetching || !settings) {
|
||||
return (
|
||||
<div className="flex grow p-4">
|
||||
<LoadingSpinner size="large" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<form
|
||||
data-testid="account-settings-form"
|
||||
ref={formRef}
|
||||
action={onSubmit}
|
||||
className="flex flex-col grow overflow-auto"
|
||||
>
|
||||
<div className="flex flex-col gap-12 px-11 py-9">
|
||||
{!shouldHandleSpecialSaasCase && (
|
||||
<section
|
||||
data-testid="llm-settings-section"
|
||||
className="flex flex-col gap-6"
|
||||
>
|
||||
<div className="flex items-center gap-7">
|
||||
<h2 className="text-[28px] leading-8 tracking-[-0.02em] font-bold">
|
||||
{t(I18nKey.SETTINGS$LLM_SETTINGS)}
|
||||
</h2>
|
||||
{!shouldHandleSpecialSaasCase && (
|
||||
<SettingsSwitch
|
||||
testId="advanced-settings-switch"
|
||||
defaultIsToggled={isAdvancedSettingsSet}
|
||||
onToggle={onToggleAdvancedMode}
|
||||
>
|
||||
{t(I18nKey.SETTINGS$ADVANCED)}
|
||||
</SettingsSwitch>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{llmConfigMode === "basic" && !shouldHandleSpecialSaasCase && (
|
||||
<ModelSelector
|
||||
models={modelsAndProviders}
|
||||
currentModel={settings.LLM_MODEL}
|
||||
/>
|
||||
)}
|
||||
|
||||
{llmConfigMode === "advanced" && !shouldHandleSpecialSaasCase && (
|
||||
<SettingsInput
|
||||
testId="llm-custom-model-input"
|
||||
name="llm-custom-model-input"
|
||||
label={t(I18nKey.SETTINGS$CUSTOM_MODEL)}
|
||||
defaultValue={settings.LLM_MODEL}
|
||||
placeholder="anthropic/claude-3-5-sonnet-20241022"
|
||||
type="text"
|
||||
className="w-[680px]"
|
||||
/>
|
||||
)}
|
||||
{llmConfigMode === "advanced" && !shouldHandleSpecialSaasCase && (
|
||||
<SettingsInput
|
||||
testId="base-url-input"
|
||||
name="base-url-input"
|
||||
label={t(I18nKey.SETTINGS$BASE_URL)}
|
||||
defaultValue={settings.LLM_BASE_URL}
|
||||
placeholder="https://api.openai.com"
|
||||
type="text"
|
||||
className="w-[680px]"
|
||||
/>
|
||||
)}
|
||||
|
||||
{!shouldHandleSpecialSaasCase && (
|
||||
<SettingsInput
|
||||
testId="llm-api-key-input"
|
||||
name="llm-api-key-input"
|
||||
label={t(I18nKey.SETTINGS_FORM$API_KEY)}
|
||||
type="password"
|
||||
className="w-[680px]"
|
||||
placeholder={isLLMKeySet ? "<hidden>" : ""}
|
||||
startContent={
|
||||
isLLMKeySet && <KeyStatusIcon isSet={isLLMKeySet} />
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!shouldHandleSpecialSaasCase && (
|
||||
<HelpLink
|
||||
testId="llm-api-key-help-anchor"
|
||||
text={t(I18nKey.SETTINGS$DONT_KNOW_API_KEY)}
|
||||
linkText={t(I18nKey.SETTINGS$CLICK_FOR_INSTRUCTIONS)}
|
||||
href="https://docs.all-hands.dev/modules/usage/installation#getting-an-api-key"
|
||||
/>
|
||||
)}
|
||||
|
||||
{llmConfigMode === "advanced" && (
|
||||
<SettingsDropdownInput
|
||||
testId="agent-input"
|
||||
name="agent-input"
|
||||
label={t(I18nKey.SETTINGS$AGENT)}
|
||||
items={
|
||||
resources?.agents.map((agent) => ({
|
||||
key: agent,
|
||||
label: agent,
|
||||
})) || []
|
||||
}
|
||||
wrapperClassName="w-[680px]"
|
||||
defaultSelectedKey={settings.AGENT}
|
||||
isClearable={false}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isSaas && llmConfigMode === "advanced" && (
|
||||
<SettingsDropdownInput
|
||||
testId="runtime-settings-input"
|
||||
name="runtime-settings-input"
|
||||
label={
|
||||
<>
|
||||
{t(I18nKey.SETTINGS$RUNTIME_SETTINGS)}
|
||||
<a href="mailto:contact@all-hands.dev">
|
||||
{t(I18nKey.SETTINGS$GET_IN_TOUCH)}
|
||||
</a>
|
||||
)
|
||||
</>
|
||||
}
|
||||
items={REMOTE_RUNTIME_OPTIONS}
|
||||
defaultSelectedKey={settings.REMOTE_RUNTIME_RESOURCE_FACTOR?.toString()}
|
||||
isDisabled
|
||||
isClearable={false}
|
||||
/>
|
||||
)}
|
||||
|
||||
{llmConfigMode === "advanced" && (
|
||||
<SettingsSwitch
|
||||
testId="enable-confirmation-mode-switch"
|
||||
onToggle={setConfirmationModeIsEnabled}
|
||||
defaultIsToggled={!!settings.CONFIRMATION_MODE}
|
||||
isBeta
|
||||
>
|
||||
{t(I18nKey.SETTINGS$CONFIRMATION_MODE)}
|
||||
</SettingsSwitch>
|
||||
)}
|
||||
|
||||
{llmConfigMode === "advanced" && (
|
||||
<SettingsSwitch
|
||||
testId="enable-memory-condenser-switch"
|
||||
name="enable-memory-condenser-switch"
|
||||
defaultIsToggled={!!settings.ENABLE_DEFAULT_CONDENSER}
|
||||
>
|
||||
{t(I18nKey.SETTINGS$ENABLE_MEMORY_CONDENSATION)}
|
||||
</SettingsSwitch>
|
||||
)}
|
||||
|
||||
{llmConfigMode === "advanced" && confirmationModeIsEnabled && (
|
||||
<div>
|
||||
<SettingsDropdownInput
|
||||
testId="security-analyzer-input"
|
||||
name="security-analyzer-input"
|
||||
label={t(I18nKey.SETTINGS$SECURITY_ANALYZER)}
|
||||
items={
|
||||
resources?.securityAnalyzers.map((analyzer) => ({
|
||||
key: analyzer,
|
||||
label: analyzer,
|
||||
})) || []
|
||||
}
|
||||
defaultSelectedKey={settings.SECURITY_ANALYZER}
|
||||
isClearable
|
||||
showOptionalTag
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
)}
|
||||
|
||||
<section className="flex flex-col gap-6">
|
||||
<h2 className="text-[28px] leading-8 tracking-[-0.02em] font-bold">
|
||||
{t(I18nKey.SETTINGS$GIT_SETTINGS)}
|
||||
</h2>
|
||||
{isSaas && hasAppSlug && (
|
||||
<Link
|
||||
to={`https://github.com/apps/${config.APP_SLUG}/installations/new`}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
<BrandButton type="button" variant="secondary">
|
||||
{t(I18nKey.GITHUB$CONFIGURE_REPOS)}
|
||||
</BrandButton>
|
||||
</Link>
|
||||
)}
|
||||
{!isSaas && (
|
||||
<>
|
||||
<SettingsInput
|
||||
testId="github-token-input"
|
||||
name="github-token-input"
|
||||
label={t(I18nKey.GITHUB$TOKEN_LABEL)}
|
||||
type="password"
|
||||
className="w-[680px]"
|
||||
startContent={
|
||||
isGitHubTokenSet && (
|
||||
<KeyStatusIcon isSet={!!isGitHubTokenSet} />
|
||||
)
|
||||
}
|
||||
placeholder={isGitHubTokenSet ? "<hidden>" : ""}
|
||||
/>
|
||||
<p data-testid="github-token-help-anchor" className="text-xs">
|
||||
{" "}
|
||||
{t(I18nKey.GITHUB$GET_TOKEN)}{" "}
|
||||
<b>
|
||||
{" "}
|
||||
<a
|
||||
href="https://github.com/settings/tokens/new?description=openhands-app&scopes=repo,user,workflow"
|
||||
target="_blank"
|
||||
className="underline underline-offset-2"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
GitHub
|
||||
</a>{" "}
|
||||
</b>
|
||||
{t(I18nKey.COMMON$HERE)}{" "}
|
||||
<b>
|
||||
<a
|
||||
href="https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token"
|
||||
target="_blank"
|
||||
className="underline underline-offset-2"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{t(I18nKey.COMMON$CLICK_FOR_INSTRUCTIONS)}
|
||||
</a>
|
||||
</b>
|
||||
.
|
||||
</p>
|
||||
|
||||
<SettingsInput
|
||||
testId="gitlab-token-input"
|
||||
name="gitlab-token-input"
|
||||
label={t(I18nKey.GITLAB$TOKEN_LABEL)}
|
||||
type="password"
|
||||
className="w-[680px]"
|
||||
startContent={
|
||||
isGitLabTokenSet && (
|
||||
<KeyStatusIcon isSet={!!isGitLabTokenSet} />
|
||||
)
|
||||
}
|
||||
placeholder={isGitHubTokenSet ? "<hidden>" : ""}
|
||||
/>
|
||||
|
||||
<p data-testid="gitlab-token-help-anchor" className="text-xs">
|
||||
{" "}
|
||||
{t(I18nKey.GITLAB$GET_TOKEN)}{" "}
|
||||
<b>
|
||||
{" "}
|
||||
<a
|
||||
href="https://gitlab.com/-/user_settings/personal_access_tokens?name=openhands-app&scopes=api,read_user,read_repository,write_repository"
|
||||
target="_blank"
|
||||
className="underline underline-offset-2"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
GitLab
|
||||
</a>{" "}
|
||||
</b>
|
||||
{t(I18nKey.GITLAB$OR_SEE)}{" "}
|
||||
<b>
|
||||
<a
|
||||
href="https://docs.gitlab.com/user/profile/personal_access_tokens/"
|
||||
target="_blank"
|
||||
className="underline underline-offset-2"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{t(I18nKey.COMMON$DOCUMENTATION)}
|
||||
</a>
|
||||
</b>
|
||||
.
|
||||
</p>
|
||||
<BrandButton
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={handleLogout}
|
||||
isDisabled={!providersAreSet}
|
||||
>
|
||||
Disconnect Tokens
|
||||
</BrandButton>
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section className="flex flex-col gap-6">
|
||||
<h2 className="text-[28px] leading-8 tracking-[-0.02em] font-bold">
|
||||
{t(I18nKey.ACCOUNT_SETTINGS$ADDITIONAL_SETTINGS)}
|
||||
</h2>
|
||||
|
||||
<SettingsDropdownInput
|
||||
testId="language-input"
|
||||
name="language-input"
|
||||
label={t(I18nKey.SETTINGS$LANGUAGE)}
|
||||
items={AvailableLanguages.map((language) => ({
|
||||
key: language.value,
|
||||
label: language.label,
|
||||
}))}
|
||||
defaultSelectedKey={settings.LANGUAGE}
|
||||
wrapperClassName="w-[680px]"
|
||||
isClearable={false}
|
||||
/>
|
||||
|
||||
<SettingsSwitch
|
||||
testId="enable-analytics-switch"
|
||||
name="enable-analytics-switch"
|
||||
defaultIsToggled={!!isAnalyticsEnabled}
|
||||
>
|
||||
{t(I18nKey.ANALYTICS$ENABLE)}
|
||||
</SettingsSwitch>
|
||||
|
||||
<SettingsSwitch
|
||||
testId="enable-sound-notifications-switch"
|
||||
name="enable-sound-notifications-switch"
|
||||
defaultIsToggled={!!settings.ENABLE_SOUND_NOTIFICATIONS}
|
||||
>
|
||||
{t(I18nKey.SETTINGS$SOUND_NOTIFICATIONS)}
|
||||
</SettingsSwitch>
|
||||
</section>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<footer className="flex gap-6 p-6 justify-end border-t border-t-tertiary">
|
||||
<BrandButton
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={() => setResetSettingsModalIsOpen(true)}
|
||||
>
|
||||
{t(I18nKey.BUTTON$RESET_TO_DEFAULTS)}
|
||||
</BrandButton>
|
||||
<BrandButton
|
||||
type="button"
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
formRef.current?.requestSubmit();
|
||||
}}
|
||||
>
|
||||
{t(I18nKey.BUTTON$SAVE)}
|
||||
</BrandButton>
|
||||
</footer>
|
||||
|
||||
{resetSettingsModalIsOpen && (
|
||||
<ModalBackdrop>
|
||||
<div
|
||||
data-testid="reset-modal"
|
||||
className="bg-base-secondary p-4 rounded-xl flex flex-col gap-4 border border-tertiary"
|
||||
>
|
||||
<p>{t(I18nKey.SETTINGS$RESET_CONFIRMATION)}</p>
|
||||
<div className="w-full flex gap-2">
|
||||
<BrandButton
|
||||
type="button"
|
||||
variant="primary"
|
||||
className="grow"
|
||||
onClick={() => {
|
||||
handleReset();
|
||||
}}
|
||||
>
|
||||
Reset
|
||||
</BrandButton>
|
||||
|
||||
<BrandButton
|
||||
type="button"
|
||||
variant="secondary"
|
||||
className="grow"
|
||||
onClick={() => {
|
||||
setResetSettingsModalIsOpen(false);
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</BrandButton>
|
||||
</div>
|
||||
</div>
|
||||
</ModalBackdrop>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default AccountSettings;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user