Compare commits
59 Commits
rds-iam-au
...
hieptl/deb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1bbf93aeb2 | ||
|
|
c932cd0815 | ||
|
|
d3395172f8 | ||
|
|
1e8851b244 | ||
|
|
167fb3f429 | ||
|
|
df4d30addf | ||
|
|
37daf068c5 | ||
|
|
5452abe513 | ||
|
|
a8b6406dac | ||
|
|
509d4a9513 | ||
|
|
d099c21f5d | ||
|
|
4c89b5ad91 | ||
|
|
729c181313 | ||
|
|
2eb3a9e6ad | ||
|
|
2382baacc2 | ||
|
|
98ce55e2fc | ||
|
|
c929447624 | ||
|
|
2cc77fb034 | ||
|
|
d57462e8ca | ||
|
|
1e23017bb1 | ||
|
|
3493348fac | ||
|
|
e63d981192 | ||
|
|
e19b3dd1f0 | ||
|
|
c3da6c20bd | ||
|
|
a022f505a8 | ||
|
|
04196f8d53 | ||
|
|
dcf00c34fa | ||
|
|
d4e94b32e1 | ||
|
|
a1b81fe923 | ||
|
|
e6d799c51a | ||
|
|
fb6f688049 | ||
|
|
ef12adc107 | ||
|
|
8a7a5cce5e | ||
|
|
b883fe37e6 | ||
|
|
182b7adcab | ||
|
|
63829d0f45 | ||
|
|
830a9e027f | ||
|
|
120a5d6ebd | ||
|
|
6b1d1869f3 | ||
|
|
e376c2bfd1 | ||
|
|
f8f74858da | ||
|
|
848a884b04 | ||
|
|
88a58a1748 | ||
|
|
f59ea69b70 | ||
|
|
8f004a1f6d | ||
|
|
15b4690ebf | ||
|
|
df1c5bbf85 | ||
|
|
8adbb76bd7 | ||
|
|
0095672439 | ||
|
|
6a5d09660d | ||
|
|
a94906e15c | ||
|
|
12dc256b5a | ||
|
|
11edf33b97 | ||
|
|
fce66e94e7 | ||
|
|
5457392eae | ||
|
|
1e7024b60a | ||
|
|
3977d4fdd7 | ||
|
|
16004426a2 | ||
|
|
73eb53a379 |
4
.github/workflows/ghcr-build.yml
vendored
@@ -46,6 +46,7 @@ jobs:
|
||||
else
|
||||
json=$(jq -n -c '[
|
||||
{ image: "nikolaik/python-nodejs:python3.12-nodejs22", tag: "nikolaik" },
|
||||
{ image: "ghcr.io/all-hands-ai/python-nodejs:python3.13-nodejs22-trixie", tag: "trixie" },
|
||||
{ image: "ubuntu:24.04", tag: "ubuntu" }
|
||||
]')
|
||||
fi
|
||||
@@ -136,6 +137,7 @@ jobs:
|
||||
if: github.event.pull_request.head.repo.fork != true
|
||||
shell: bash
|
||||
run: |
|
||||
|
||||
./containers/build.sh -i runtime -o ${{ env.REPO_OWNER }} -t ${{ matrix.base_image.tag }} --dry
|
||||
|
||||
DOCKER_BUILD_JSON=$(jq -c . < docker-build-dry.json)
|
||||
@@ -211,6 +213,8 @@ jobs:
|
||||
latest=auto
|
||||
prefix=
|
||||
suffix=
|
||||
env:
|
||||
DOCKER_METADATA_PR_HEAD_SHA: true
|
||||
- name: Determine app image tag
|
||||
shell: bash
|
||||
run: |
|
||||
|
||||
61
.github/workflows/py-tests.yml
vendored
@@ -19,12 +19,16 @@ jobs:
|
||||
# Run python tests on Linux
|
||||
test-on-linux:
|
||||
name: Python Tests on Linux
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2204
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
env:
|
||||
INSTALL_DOCKER: "0" # Set to '0' to skip Docker installation
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.12"]
|
||||
permissions:
|
||||
# For coverage comment and python-coverage-comment-action branch
|
||||
pull-requests: write
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Docker Buildx
|
||||
@@ -48,10 +52,21 @@ jobs:
|
||||
- name: Build Environment
|
||||
run: make build
|
||||
- name: Run Unit Tests
|
||||
run: PYTHONPATH=".:$PYTHONPATH" poetry run pytest --forked -n auto -svv ./tests/unit
|
||||
run: PYTHONPATH=".:$PYTHONPATH" poetry run pytest --forked -n auto -s ./tests/unit --cov=openhands --cov-branch
|
||||
env:
|
||||
COVERAGE_FILE: ".coverage.${{ matrix.python_version }}"
|
||||
- name: Run Runtime Tests with CLIRuntime
|
||||
run: PYTHONPATH=".:$PYTHONPATH" TEST_RUNTIME=cli poetry run pytest -svv tests/runtime/test_bash.py
|
||||
|
||||
run: PYTHONPATH=".:$PYTHONPATH" TEST_RUNTIME=cli poetry run pytest -s tests/runtime/test_bash.py --cov=openhands --cov-branch
|
||||
env:
|
||||
COVERAGE_FILE: ".coverage.runtime.${{ matrix.python_version }}"
|
||||
- name: Store coverage file
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: coverage-openhands
|
||||
path: |
|
||||
.coverage.${{ matrix.python_version }}
|
||||
.coverage.runtime.${{ matrix.python_version }}
|
||||
include-hidden-files: true
|
||||
# Run specific Windows python tests
|
||||
test-on-windows:
|
||||
name: Python Tests on Windows
|
||||
@@ -85,7 +100,7 @@ jobs:
|
||||
DEBUG: "1"
|
||||
test-enterprise:
|
||||
name: Enterprise Python Unit Tests
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2204
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2404
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.12"]
|
||||
@@ -102,5 +117,37 @@ jobs:
|
||||
working-directory: ./enterprise
|
||||
run: poetry install --with dev,test
|
||||
- name: Run Unit Tests
|
||||
working-directory: ./enterprise
|
||||
run: PYTHONPATH=".:$PYTHONPATH" poetry run pytest --forked -n auto -svv -p no:ddtrace -p no:ddtrace.pytest_bdd -p no:ddtrace.pytest_benchmark ./tests/unit
|
||||
# Use base working directory for coverage paths to line up.
|
||||
run: PYTHONPATH=".:$PYTHONPATH" poetry run --project=enterprise pytest --forked -n auto -s -p no:ddtrace -p no:ddtrace.pytest_bdd -p no:ddtrace.pytest_benchmark ./enterprise/tests/unit --cov=enterprise --cov-branch
|
||||
env:
|
||||
COVERAGE_FILE: ".coverage.enterprise.${{ matrix.python_version }}"
|
||||
- name: Store coverage file
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: coverage-enterprise
|
||||
path: ".coverage.enterprise.${{ matrix.python_version }}"
|
||||
include-hidden-files: true
|
||||
coverage-comment:
|
||||
name: Coverage Comment
|
||||
if: github.event_name == 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
needs: [test-on-linux, test-enterprise]
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/download-artifact@v5
|
||||
id: download
|
||||
with:
|
||||
pattern: coverage-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Coverage comment
|
||||
id: coverage_comment
|
||||
uses: py-cov-action/python-coverage-comment-action@v3
|
||||
with:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
MERGE_COVERAGE_FILES: true
|
||||
|
||||
5
.github/workflows/pypi-release.yml
vendored
@@ -1,7 +1,7 @@
|
||||
# Publishes the OpenHands PyPi package
|
||||
name: Publish PyPi Package
|
||||
|
||||
# Triggered manually
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
@@ -9,6 +9,9 @@ on:
|
||||
description: 'Reason for manual trigger'
|
||||
required: true
|
||||
default: ''
|
||||
push:
|
||||
tags:
|
||||
- "*"
|
||||
|
||||
jobs:
|
||||
release:
|
||||
|
||||
@@ -45,7 +45,7 @@ jobs:
|
||||
"This issue has been labeled as **good first issue**, which means it's a great place to get started with the OpenHands project.\n\n" +
|
||||
"If you're interested in working on it, feel free to! No need to ask for permission.\n\n" +
|
||||
"Be sure to check out our [development setup guide](" + repoUrl + "/blob/main/Development.md) to get your environment set up, and follow our [contribution guidelines](" + repoUrl + "/blob/main/CONTRIBUTING.md) when you're ready to submit a fix.\n\n" +
|
||||
"Feel free to join our developer community on [Slack](dub.sh/openhands). You can ask for [help](https://openhands-ai.slack.com/archives/C078L0FUGUX), [feedback](https://openhands-ai.slack.com/archives/C086ARSNMGA), and even ask for a [PR review](https://openhands-ai.slack.com/archives/C08D8FJ5771).\n\n" +
|
||||
"Feel free to join our developer community on [Slack](https://all-hands.dev/joinslack). You can ask for [help](https://openhands-ai.slack.com/archives/C078L0FUGUX), [feedback](https://openhands-ai.slack.com/archives/C086ARSNMGA), and even ask for a [PR review](https://openhands-ai.slack.com/archives/C08D8FJ5771).\n\n" +
|
||||
"🙌 Happy hacking! 🙌\n\n" +
|
||||
"<!-- auto-comment:good-first-issue -->"
|
||||
});
|
||||
|
||||
@@ -113,19 +113,19 @@ individual, or aggression toward or disparagement of classes of individuals.
|
||||
**Consequence**: A permanent ban from any sort of public interaction within the
|
||||
community.
|
||||
|
||||
### Slack and Discord Etiquettes
|
||||
### Slack Etiquettes
|
||||
|
||||
These Slack and Discord etiquette guidelines are designed to foster an inclusive, respectful, and productive environment for all community members. By following these best practices, we ensure effective communication and collaboration while minimizing disruptions. Let’s work together to build a supportive and welcoming community!
|
||||
These Slack etiquette guidelines are designed to foster an inclusive, respectful, and productive environment for all community members. By following these best practices, we ensure effective communication and collaboration while minimizing disruptions. Let’s work together to build a supportive and welcoming community!
|
||||
|
||||
- Communicate respectfully and professionally, avoiding sarcasm or harsh language, and remember that tone can be difficult to interpret in text.
|
||||
- Use threads for specific discussions to keep channels organized and easier to follow.
|
||||
- Tag others only when their input is critical or urgent, and use @here, @channel or @everyone sparingly to minimize disruptions.
|
||||
- Be patient, as open-source contributors and maintainers often have other commitments and may need time to respond.
|
||||
- Post questions or discussions in the most relevant channel (e.g., for [slack - #general](https://openhands-ai.slack.com/archives/C06P5NCGSFP) for general topics, [slack - #questions](https://openhands-ai.slack.com/archives/C06U8UTKSAD) for queries/questions, [discord - #general](https://discord.com/channels/1222935860639563850/1222935861386018885)).
|
||||
- Post questions or discussions in the most relevant channel (e.g., for [slack - #general](https://openhands-ai.slack.com/archives/C06P5NCGSFP) for general topics, [slack - #questions](https://openhands-ai.slack.com/archives/C06U8UTKSAD) for queries/questions.
|
||||
- When asking for help or raising issues, include necessary details like links, screenshots, or clear explanations to provide context.
|
||||
- Keep discussions in public channels whenever possible to allow others to benefit from the conversation, unless the matter is sensitive or private.
|
||||
- Always adhere to [our standards](https://github.com/All-Hands-AI/OpenHands/blob/main/CODE_OF_CONDUCT.md#our-standards) to ensure a welcoming and collaborative environment.
|
||||
- If you choose to mute a channel, consider setting up alerts for topics that still interest you to stay engaged. For Slack, Go to Settings → Notifications → My Keywords to add specific keywords that will notify you when mentioned. For example, if you're here for discussions about LLMs, mute the channel if it’s too busy, but set notifications to alert you only when “LLMs” appears in messages. Also for Discord, go to the channel notifications and choose the option that best describes your need.
|
||||
- If you choose to mute a channel, consider setting up alerts for topics that still interest you to stay engaged. For Slack, Go to Settings → Notifications → My Keywords to add specific keywords that will notify you when mentioned. For example, if you're here for discussions about LLMs, mute the channel if it’s too busy, but set notifications to alert you only when “LLMs” appears in messages.
|
||||
|
||||
## Attribution
|
||||
|
||||
|
||||
@@ -159,7 +159,7 @@ poetry run pytest ./tests/unit/test_*.py
|
||||
To reduce build time (e.g., if no changes were made to the client-runtime component), you can use an existing Docker
|
||||
container image by setting the SANDBOX_RUNTIME_CONTAINER_IMAGE environment variable to the desired Docker image.
|
||||
|
||||
Example: `export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.57-nikolaik`
|
||||
Example: `export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.58-nikolaik`
|
||||
|
||||
## Develop inside Docker container
|
||||
|
||||
|
||||
18
README.md
@@ -11,8 +11,7 @@
|
||||
<a href="https://github.com/All-Hands-AI/OpenHands/stargazers"><img src="https://img.shields.io/github/stars/All-Hands-AI/OpenHands?style=for-the-badge&color=blue" alt="Stargazers"></a>
|
||||
<a href="https://github.com/All-Hands-AI/OpenHands/blob/main/LICENSE"><img src="https://img.shields.io/github/license/All-Hands-AI/OpenHands?style=for-the-badge&color=blue" alt="MIT License"></a>
|
||||
<br/>
|
||||
<a href="https://dub.sh/openhands"><img src="https://img.shields.io/badge/Slack-Join%20Us-red?logo=slack&logoColor=white&style=for-the-badge" alt="Join our Slack community"></a>
|
||||
<a href="https://discord.gg/ESHStjSjD4"><img src="https://img.shields.io/badge/Discord-Join%20Us-purple?logo=discord&logoColor=white&style=for-the-badge" alt="Join our Discord community"></a>
|
||||
<a href="https://all-hands.dev/joinslack"><img src="https://img.shields.io/badge/Slack-Join%20Us-red?logo=slack&logoColor=white&style=for-the-badge" alt="Join our Slack community"></a>
|
||||
<a href="https://github.com/All-Hands-AI/OpenHands/blob/main/CREDITS.md"><img src="https://img.shields.io/badge/Project-Credits-blue?style=for-the-badge&color=FFE165&logo=github&logoColor=white" alt="Credits"></a>
|
||||
<br/>
|
||||
<a href="https://docs.all-hands.dev/usage/getting-started"><img src="https://img.shields.io/badge/Documentation-000?logo=googledocs&logoColor=FFE165&style=for-the-badge" alt="Check out the documentation"></a>
|
||||
@@ -44,8 +43,6 @@ Learn more at [docs.all-hands.dev](https://docs.all-hands.dev), or [sign up for
|
||||
> [this short form](https://docs.google.com/forms/d/e/1FAIpQLSet3VbGaz8z32gW9Wm-Grl4jpt5WgMXPgJ4EDPVmCETCBpJtQ/viewform)
|
||||
> to join our Design Partner program, where you'll get early access to commercial features and the opportunity to provide input on our product roadmap.
|
||||
|
||||

|
||||
|
||||
## ☁️ OpenHands Cloud
|
||||
The easiest way to get started with OpenHands is on [OpenHands Cloud](https://app.all-hands.dev),
|
||||
which comes with $20 in free credits for new users.
|
||||
@@ -79,17 +76,17 @@ You'll find OpenHands running at [http://localhost:3000](http://localhost:3000)
|
||||
You can also run OpenHands directly with Docker:
|
||||
|
||||
```bash
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.57-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.58-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.57-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.58-nikolaik \
|
||||
-e LOG_ALL_EVENTS=true \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-v ~/.openhands:/.openhands \
|
||||
-p 3000:3000 \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.57
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.58
|
||||
```
|
||||
|
||||
</details>
|
||||
@@ -103,7 +100,7 @@ docker run -it --rm --pull=always \
|
||||
### Getting Started
|
||||
|
||||
When you open the application, you'll be asked to choose an LLM provider and add an API key.
|
||||
[Anthropic's Claude Sonnet 4](https://www.anthropic.com/api) (`anthropic/claude-sonnet-4-20250514`)
|
||||
[Anthropic's Claude Sonnet 4.5](https://www.anthropic.com/api) (`anthropic/claude-sonnet-4-5-20250929`)
|
||||
works best, but you have [many options](https://docs.all-hands.dev/usage/llms).
|
||||
|
||||
See the [Running OpenHands](https://docs.all-hands.dev/usage/installation) guide for
|
||||
@@ -140,10 +137,9 @@ troubleshooting resources, and advanced configuration options.
|
||||
## 🤝 How to Join the Community
|
||||
|
||||
OpenHands is a community-driven project, and we welcome contributions from everyone. We do most of our communication
|
||||
through Slack, so this is the best place to start, but we also are happy to have you contact us on Discord or Github:
|
||||
through Slack, so this is the best place to start, but we also are happy to have you contact us on Github:
|
||||
|
||||
- [Join our Slack workspace](https://dub.sh/openhands) - Here we talk about research, architecture, and future development.
|
||||
- [Join our Discord server](https://discord.gg/ESHStjSjD4) - This is a community-run server for general discussion, questions, and feedback.
|
||||
- [Join our Slack workspace](https://all-hands.dev/joinslack) - Here we talk about research, architecture, and future development.
|
||||
- [Read or post Github Issues](https://github.com/All-Hands-AI/OpenHands/issues) - Check out the issues we're working on, or add your own ideas.
|
||||
|
||||
See more about the community in [COMMUNITY.md](./COMMUNITY.md) or find details on contributing in [CONTRIBUTING.md](./CONTRIBUTING.md).
|
||||
|
||||
148
README_CN.md
@@ -1,148 +0,0 @@
|
||||
|
||||
<a name="readme-top"></a>
|
||||
|
||||
<div align="center">
|
||||
<img src="./docs/static/img/logo.png" alt="Logo" width="200">
|
||||
<h1 align="center">OpenHands: 少写代码,多做事</h1>
|
||||
</div>
|
||||
|
||||
|
||||
<div align="center">
|
||||
<a href="https://github.com/All-Hands-AI/OpenHands/graphs/contributors"><img src="https://img.shields.io/github/contributors/All-Hands-AI/OpenHands?style=for-the-badge&color=blue" alt="Contributors"></a>
|
||||
<a href="https://github.com/All-Hands-AI/OpenHands/stargazers"><img src="https://img.shields.io/github/stars/All-Hands-AI/OpenHands?style=for-the-badge&color=blue" alt="Stargazers"></a>
|
||||
<a href="https://github.com/All-Hands-AI/OpenHands/blob/main/LICENSE"><img src="https://img.shields.io/github/license/All-Hands-AI/OpenHands?style=for-the-badge&color=blue" alt="MIT License"></a>
|
||||
<br/>
|
||||
<a href="https://dub.sh/openhands"><img src="https://img.shields.io/badge/Slack-Join%20Us-red?logo=slack&logoColor=white&style=for-the-badge" alt="加入我们的Slack社区"></a>
|
||||
<a href="https://discord.gg/ESHStjSjD4"><img src="https://img.shields.io/badge/Discord-Join%20Us-purple?logo=discord&logoColor=white&style=for-the-badge" alt="加入我们的Discord社区"></a>
|
||||
<a href="https://github.com/All-Hands-AI/OpenHands/blob/main/CREDITS.md"><img src="https://img.shields.io/badge/Project-Credits-blue?style=for-the-badge&color=FFE165&logo=github&logoColor=white" alt="致谢"></a>
|
||||
<br/>
|
||||
<a href="https://docs.all-hands.dev/usage/getting-started"><img src="https://img.shields.io/badge/Documentation-000?logo=googledocs&logoColor=FFE165&style=for-the-badge" alt="查看文档"></a>
|
||||
<a href="https://arxiv.org/abs/2407.16741"><img src="https://img.shields.io/badge/Paper%20on%20Arxiv-000?logoColor=FFE165&logo=arxiv&style=for-the-badge" alt="Arxiv论文"></a>
|
||||
<a href="https://docs.google.com/spreadsheets/d/1wOUdFCMyY6Nt0AIqF705KN4JKOWgeI4wUGUP60krXXs/edit?gid=0#gid=0"><img src="https://img.shields.io/badge/Benchmark%20score-000?logoColor=FFE165&logo=huggingface&style=for-the-badge" alt="评估基准分数"></a>
|
||||
<hr>
|
||||
</div>
|
||||
|
||||
欢迎使用OpenHands(前身为OpenDevin),这是一个由AI驱动的软件开发代理平台。
|
||||
|
||||
OpenHands代理可以完成人类开发者能做的任何事情:修改代码、运行命令、浏览网页、调用API,甚至从StackOverflow复制代码片段。
|
||||
|
||||
在[docs.all-hands.dev](https://docs.all-hands.dev)了解更多信息,或[注册OpenHands Cloud](https://app.all-hands.dev)开始使用。
|
||||
|
||||
> [!IMPORTANT]
|
||||
> 在工作中使用OpenHands?我们很想与您交流!填写
|
||||
> [这份简短表格](https://docs.google.com/forms/d/e/1FAIpQLSet3VbGaz8z32gW9Wm-Grl4jpt5WgMXPgJ4EDPVmCETCBpJtQ/viewform)
|
||||
> 加入我们的设计合作伙伴计划,您将获得商业功能的早期访问权限,并有机会对我们的产品路线图提供意见。
|
||||
|
||||

|
||||
|
||||
## ☁️ OpenHands Cloud
|
||||
开始使用OpenHands的最简单方式是在[OpenHands Cloud](https://app.all-hands.dev)上,
|
||||
新用户可获得$50的免费额度。
|
||||
|
||||
## 💻 在本地运行OpenHands
|
||||
|
||||
OpenHands也可以使用Docker在本地系统上运行。
|
||||
查看[运行OpenHands](https://docs.all-hands.dev/usage/installation)指南了解
|
||||
系统要求和更多信息。
|
||||
|
||||
> [!WARNING]
|
||||
> 在公共网络上?请参阅我们的[强化Docker安装指南](https://docs.all-hands.dev/usage/runtimes/docker#hardened-docker-installation)
|
||||
> 通过限制网络绑定和实施其他安全措施来保护您的部署。
|
||||
|
||||
|
||||
```bash
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.57-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.57-nikolaik \
|
||||
-e LOG_ALL_EVENTS=true \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-v ~/.openhands:/.openhands \
|
||||
-p 3000:3000 \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.57
|
||||
```
|
||||
|
||||
> **注意**: 如果您在0.44版本之前使用过OpenHands,您可能需要运行 `mv ~/.openhands-state ~/.openhands` 来将对话历史迁移到新位置。
|
||||
|
||||
您将在[http://localhost:3000](http://localhost:3000)找到运行中的OpenHands!
|
||||
|
||||
打开应用程序时,您将被要求选择一个LLM提供商并添加API密钥。
|
||||
[Anthropic的Claude Sonnet 4](https://www.anthropic.com/api)(`anthropic/claude-sonnet-4-20250514`)
|
||||
效果最佳,但您还有[许多选择](https://docs.all-hands.dev/usage/llms)。
|
||||
|
||||
## 💡 运行OpenHands的其他方式
|
||||
|
||||
> [!CAUTION]
|
||||
> OpenHands旨在由单个用户在其本地工作站上运行。
|
||||
> 它不适合多租户部署,即多个用户共享同一实例。没有内置的身份验证、隔离或可扩展性。
|
||||
>
|
||||
> 如果您有兴趣在多租户环境中运行OpenHands,请
|
||||
> [与我们联系](https://docs.google.com/forms/d/e/1FAIpQLSet3VbGaz8z32gW9Wm-Grl4jpt5WgMXPgJ4EDPVmCETCBpJtQ/viewform)
|
||||
> 了解高级部署选项。
|
||||
|
||||
您还可以[将OpenHands连接到本地文件系统](https://docs.all-hands.dev/usage/runtimes/docker#connecting-to-your-filesystem),
|
||||
以可编程的[无头模式](https://docs.all-hands.dev/usage/how-to/headless-mode)运行OpenHands,
|
||||
通过[友好的CLI](https://docs.all-hands.dev/usage/how-to/cli-mode)与其交互,
|
||||
或使用[GitHub Action](https://docs.all-hands.dev/usage/how-to/github-action)在标记的问题上运行它。
|
||||
|
||||
访问[运行OpenHands](https://docs.all-hands.dev/usage/installation)获取更多信息和设置说明。
|
||||
|
||||
如果您想修改OpenHands源代码,请查看[Development.md](https://github.com/All-Hands-AI/OpenHands/blob/main/Development.md)。
|
||||
|
||||
遇到问题?[故障排除指南](https://docs.all-hands.dev/usage/troubleshooting)可以提供帮助。
|
||||
|
||||
## 📖 文档
|
||||
<a href="https://deepwiki.com/All-Hands-AI/OpenHands"><img src="https://deepwiki.com/badge.svg" alt="Ask DeepWiki" title="DeepWiki自动生成文档"></a>
|
||||
|
||||
要了解有关项目的更多信息,以及使用OpenHands的技巧,
|
||||
请查看我们的[文档](https://docs.all-hands.dev/usage/getting-started)。
|
||||
|
||||
在那里,您将找到有关如何使用不同LLM提供商、
|
||||
故障排除资源和高级配置选项的资源。
|
||||
|
||||
## 🤝 如何加入社区
|
||||
|
||||
OpenHands是一个社区驱动的项目,我们欢迎每个人的贡献。我们大部分沟通
|
||||
通过Slack进行,因此这是开始的最佳场所,但我们也很乐意您通过Discord或Github与我们联系:
|
||||
|
||||
- [加入我们的Slack工作空间](https://dub.sh/openhands) - 这里我们讨论研究、架构和未来发展。
|
||||
- [加入我们的Discord服务器](https://discord.gg/ESHStjSjD4) - 这是一个社区运营的服务器,用于一般讨论、问题和反馈。
|
||||
- [阅读或发布Github问题](https://github.com/All-Hands-AI/OpenHands/issues) - 查看我们正在处理的问题,或添加您自己的想法。
|
||||
|
||||
在[COMMUNITY.md](./COMMUNITY.md)中了解更多关于社区的信息,或在[CONTRIBUTING.md](./CONTRIBUTING.md)中找到有关贡献的详细信息。
|
||||
|
||||
## 📈 进展
|
||||
|
||||
在[这里](https://github.com/orgs/All-Hands-AI/projects/1)查看OpenHands月度路线图(每月月底在维护者会议上更新)。
|
||||
|
||||
<p align="center">
|
||||
<a href="https://star-history.com/#All-Hands-AI/OpenHands&Date">
|
||||
<img src="https://api.star-history.com/svg?repos=All-Hands-AI/OpenHands&type=Date" width="500" alt="Star History Chart">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
## 📜 许可证
|
||||
|
||||
根据MIT许可证分发。有关更多信息,请参阅[`LICENSE`](./LICENSE)。
|
||||
|
||||
## 🙏 致谢
|
||||
|
||||
OpenHands由大量贡献者构建,每一份贡献都备受感谢!我们还借鉴了其他开源项目,对他们的工作深表感谢。
|
||||
|
||||
有关OpenHands中使用的开源项目和许可证列表,请参阅我们的[CREDITS.md](./CREDITS.md)文件。
|
||||
|
||||
## 📚 引用
|
||||
|
||||
```
|
||||
@misc{openhands,
|
||||
title={{OpenHands: An Open Platform for AI Software Developers as Generalist Agents}},
|
||||
author={Xingyao Wang and Boxuan Li and Yufan Song and Frank F. Xu and Xiangru Tang and Mingchen Zhuge and Jiayi Pan and Yueqi Song and Bowen Li and Jaskirat Singh and Hoang H. Tran and Fuqiang Li and Ren Ma and Mingzhang Zheng and Bill Qian and Yanjun Shao and Niklas Muennighoff and Yizhe Zhang and Binyuan Hui and Junyang Lin and Robert Brennan and Hao Peng and Heng Ji and Graham Neubig},
|
||||
year={2024},
|
||||
eprint={2407.16741},
|
||||
archivePrefix={arXiv},
|
||||
primaryClass={cs.SE},
|
||||
url={https://arxiv.org/abs/2407.16741},
|
||||
}
|
||||
```
|
||||
60
README_JA.md
@@ -1,60 +0,0 @@
|
||||
<a name="readme-top"></a>
|
||||
|
||||
<div align="center">
|
||||
<img src="./docs/static/img/logo.png" alt="Logo" width="200">
|
||||
<h1 align="center">OpenHands: コードを減らして、もっと作ろう</h1>
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
<a href="https://github.com/All-Hands-AI/OpenHands/graphs/contributors"><img src="https://img.shields.io/github/contributors/All-Hands-AI/OpenHands?style=for-the-badge&color=blue" alt="Contributors"></a>
|
||||
<a href="https://github.com/All-Hands-AI/OpenHands/stargazers"><img src="https://img.shields.io/github/stars/All-Hands-AI/OpenHands?style=for-the-badge&color=blue" alt="Stargazers"></a>
|
||||
<a href="https://github.com/All-Hands-AI/OpenHands/blob/main/LICENSE"><img src="https://img.shields.io/github/license/All-Hands-AI/OpenHands?style=for-the-badge&color=blue" alt="MIT License"></a>
|
||||
<br/>
|
||||
<a href="https://dub.sh/openhands"><img src="https://img.shields.io/badge/Slack-Join%20Us-red?logo=slack&logoColor=white&style=for-the-badge" alt="Slackコミュニティに参加"></a>
|
||||
<a href="https://discord.gg/ESHStjSjD4"><img src="https://img.shields.io/badge/Discord-Join%20Us-purple?logo=discord&logoColor=white&style=for-the-badge" alt="Discordコミュニティに参加"></a>
|
||||
<a href="https://github.com/All-Hands-AI/OpenHands/blob/main/CREDITS.md"><img src="https://img.shields.io/badge/Project-Credits-blue?style=for-the-badge&color=FFE165&logo=github&logoColor=white" alt="クレジット"></a>
|
||||
<br/>
|
||||
<a href="https://docs.all-hands.dev/usage/getting-started"><img src="https://img.shields.io/badge/Documentation-000?logo=googledocs&logoColor=FFE165&style=for-the-badge" alt="ドキュメントを見る"></a>
|
||||
<a href="https://arxiv.org/abs/2407.16741"><img src="https://img.shields.io/badge/Paper%20on%20Arxiv-000?logoColor=FFE165&logo=arxiv&style=for-the-badge" alt="Arxiv論文"></a>
|
||||
<a href="https://docs.google.com/spreadsheets/d/1wOUdFCMyY6Nt0AIqF705KN4JKOWgeI4wUGUP60krXXs/edit?gid=0#gid=0"><img src="https://img.shields.io/badge/Benchmark%20score-000?logoColor=FFE165&logo=huggingface&style=for-the-badge" alt="評価ベンチマークスコア"></a>
|
||||
<hr>
|
||||
</div>
|
||||
|
||||
OpenHands(旧OpenDevin)へようこそ。これはAIが駆動するソフトウェア開発エージェントのプラットフォームです。
|
||||
|
||||
OpenHandsのエージェントは人間の開発者ができることは何でもこなします。コードを修正し、コマンドを実行し、ウェブを閲覧し、APIを呼び出し、StackOverflowからコードスニペットをコピーすることさえできます。
|
||||
|
||||
詳細は[docs.all-hands.dev](https://docs.all-hands.dev)をご覧いただくか、[OpenHands Cloud](https://app.all-hands.dev)に登録して始めましょう。
|
||||
|
||||
> [!IMPORTANT]
|
||||
> 仕事でOpenHandsを使っていますか?ぜひお話を聞かせてください。[こちらの短いフォーム](https://docs.google.com/forms/d/e/1FAIpQLSet3VbGaz8z32gW9Wm-Grl4jpt5WgMXPgJ4EDPVmCETCBpJtQ/viewform)にご記入いただき、Design Partnerプログラムにご参加ください。商用機能の早期アクセスや製品ロードマップへのフィードバックの機会を提供します。
|
||||
|
||||

|
||||
|
||||
## ☁️ OpenHands Cloud
|
||||
OpenHandsを始める最も簡単な方法は[OpenHands Cloud](https://app.all-hands.dev)を利用することです。新規ユーザーには50ドル分の無料クレジットが付与されます。
|
||||
|
||||
## 💻 OpenHandsをローカルで実行する
|
||||
|
||||
OpenHandsはDockerを利用してローカル環境でも実行できます。システム要件や詳細については[Running OpenHands](https://docs.all-hands.dev/usage/installation)ガイドをご覧ください。
|
||||
|
||||
> [!WARNING]
|
||||
> 公共ネットワークで実行していますか?[Hardened Docker Installation Guide](https://docs.all-hands.dev/usage/runtimes/docker#hardened-docker-installation)を参照して、ネットワークバインディングの制限や追加のセキュリティ対策を実施してください。
|
||||
|
||||
```bash
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.57-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.57-nikolaik \
|
||||
-e LOG_ALL_EVENTS=true \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-v ~/.openhands:/.openhands \
|
||||
-p 3000:3000 \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.57
|
||||
```
|
||||
|
||||
**注**: バージョン0.44以前のOpenHandsを使用していた場合は、会話履歴を移行するために `mv ~/.openhands-state ~/.openhands` を実行してください。
|
||||
|
||||
OpenHandsは[http://localhost:3000](http://localhost:3000)で起動します!
|
||||
@@ -489,6 +489,47 @@ type = "noop"
|
||||
# Run the runtime sandbox container in privileged mode for use with docker-in-docker
|
||||
#privileged = false
|
||||
|
||||
#################################### MCP #####################################
|
||||
# Configuration for Model Context Protocol (MCP) servers
|
||||
# MCP allows OpenHands to communicate with external tool servers
|
||||
##############################################################################
|
||||
[mcp]
|
||||
# SSE servers - Server-Sent Events transport (legacy)
|
||||
#sse_servers = [
|
||||
# # Basic SSE server with just a URL
|
||||
# "http://localhost:8080/mcp/sse",
|
||||
#
|
||||
# # SSE server with authentication
|
||||
# {url = "https://api.example.com/mcp/sse", api_key = "your-api-key"}
|
||||
#]
|
||||
|
||||
# SHTTP servers - Streamable HTTP transport (recommended)
|
||||
#shttp_servers = [
|
||||
# # Basic SHTTP server with default 60s timeout
|
||||
# "https://api.example.com/mcp/shttp",
|
||||
#
|
||||
# # SHTTP server with custom timeout for long-running tools
|
||||
# {
|
||||
# url = "https://api.example.com/mcp/shttp",
|
||||
# api_key = "your-api-key",
|
||||
# timeout = 180 # 3 minutes for processing-heavy tools (1-3600 seconds)
|
||||
# }
|
||||
#]
|
||||
|
||||
# Stdio servers - Direct process communication (development only)
|
||||
#stdio_servers = [
|
||||
# # Basic stdio server
|
||||
# {name = "filesystem", command = "npx", args = ["@modelcontextprotocol/server-filesystem", "/"]},
|
||||
#
|
||||
# # Stdio server with environment variables
|
||||
# {
|
||||
# name = "fetch",
|
||||
# command = "uvx",
|
||||
# args = ["mcp-server-fetch"],
|
||||
# env = {DEBUG = "true"}
|
||||
# }
|
||||
#]
|
||||
|
||||
#################################### Model Routing ############################
|
||||
# Configuration for experimental model routing feature
|
||||
# Enables intelligent switching between different LLM models for specific purposes
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
ARG OPENHANDS_BUILD_VERSION=dev
|
||||
FROM node:24.3.0-bookworm-slim AS frontend-builder
|
||||
FROM node:24.8-trixie-slim AS frontend-builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
@@ -9,7 +9,7 @@ RUN npm ci
|
||||
COPY frontend ./
|
||||
RUN npm run build
|
||||
|
||||
FROM python:3.12.10-slim AS base
|
||||
FROM python:3.13.7-slim-trixie AS base
|
||||
FROM base AS backend-builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
@@ -12,7 +12,7 @@ services:
|
||||
- SANDBOX_API_HOSTNAME=host.docker.internal
|
||||
- DOCKER_HOST_ADDR=host.docker.internal
|
||||
#
|
||||
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.57-nikolaik}
|
||||
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.58-nikolaik}
|
||||
- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234}
|
||||
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
|
||||
ports:
|
||||
|
||||
@@ -7,7 +7,7 @@ services:
|
||||
image: openhands:latest
|
||||
container_name: openhands-app-${DATE:-}
|
||||
environment:
|
||||
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-docker.all-hands.dev/all-hands-ai/runtime:0.57-nikolaik}
|
||||
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-docker.all-hands.dev/all-hands-ai/runtime:0.58-nikolaik}
|
||||
#- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234} # enable this only if you want a specific non-root sandbox user but you will have to manually adjust permissions of ~/.openhands for this user
|
||||
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
|
||||
ports:
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
"group": "OpenHands Cloud",
|
||||
"pages": [
|
||||
"usage/cloud/openhands-cloud",
|
||||
"usage/cloud/pro-subscription",
|
||||
{
|
||||
"group": "Integrations",
|
||||
"pages": [
|
||||
@@ -109,8 +110,7 @@
|
||||
},
|
||||
"usage/configuration-options",
|
||||
"usage/how-to/custom-sandbox-guide",
|
||||
"usage/search-engine-setup",
|
||||
"usage/mcp"
|
||||
"usage/search-engine-setup"
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -118,7 +118,13 @@
|
||||
{
|
||||
"group": "Customizations & Settings",
|
||||
"pages": [
|
||||
"usage/common-settings",
|
||||
{
|
||||
"group": "OpenHands Settings",
|
||||
"pages": [
|
||||
"usage/settings/secrets-settings",
|
||||
"usage/settings/mcp-settings"
|
||||
]
|
||||
},
|
||||
"usage/prompting/repository",
|
||||
{
|
||||
"group": "Microagents",
|
||||
@@ -208,7 +214,7 @@
|
||||
},
|
||||
"footer": {
|
||||
"socials": {
|
||||
"slack": "https://dub.sh/openhands",
|
||||
"slack": "https://all-hands.dev/joinslack",
|
||||
"github": "https://github.com/All-Hands-AI/OpenHands",
|
||||
"discord": "https://discord.gg/ESHStjSjD4"
|
||||
}
|
||||
|
||||
BIN
docs/static/img/api-key-generation.png
vendored
|
Before Width: | Height: | Size: 18 KiB |
BIN
docs/static/img/connect-repo-no-github.png
vendored
|
Before Width: | Height: | Size: 15 KiB |
BIN
docs/static/img/connect-repo.png
vendored
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 15 KiB |
BIN
docs/static/img/oh-features.png
vendored
|
Before Width: | Height: | Size: 144 KiB After Width: | Height: | Size: 212 KiB |
BIN
docs/static/img/screenshot.png
vendored
|
Before Width: | Height: | Size: 663 KiB |
@@ -8,9 +8,21 @@ description: This guide walks you through the process of installing OpenHands Cl
|
||||
|
||||
- Signed in to [OpenHands Cloud](https://app.all-hands.dev) with [a Bitbucket account](/usage/cloud/openhands-cloud).
|
||||
|
||||
## Adding Bitbucket Repository Access
|
||||
|
||||
Upon signing into OpenHands Cloud with a Bitbucket account, OpenHands will have access to your repositories.
|
||||
|
||||
## Working With Bitbucket Repos in Openhands Cloud
|
||||
|
||||
After signing in with a Bitbucket account, use the `Open Repository` section to select the appropriate repository and
|
||||
branch you'd like OpenHands to work on. Then click on `Launch` to start the conversation!
|
||||
|
||||

|
||||
|
||||
## IP Whitelisting
|
||||
|
||||
If your Bitbucket Cloud instance has IP restrictions, you'll need to whitelist the following IP addresses to allow OpenHands to access your repositories:
|
||||
If your Bitbucket Cloud instance has IP restrictions, you'll need to whitelist the following IP addresses to allow
|
||||
OpenHands to access your repositories:
|
||||
|
||||
### Core App IP
|
||||
```
|
||||
@@ -31,17 +43,6 @@ If your Bitbucket Cloud instance has IP restrictions, you'll need to whitelist t
|
||||
34.60.55.59
|
||||
```
|
||||
|
||||
## Adding Bitbucket Repository Access
|
||||
|
||||
Upon signing into OpenHands Cloud with a Bitbucket account, OpenHands will have access to your repositories.
|
||||
|
||||
## Working With Bitbucket Repos in Openhands Cloud
|
||||
|
||||
After signing in with a Bitbucket account, use the `select a repo` and `select a branch` dropdowns to select the
|
||||
appropriate repository and branch you'd like OpenHands to work on. Then click on `Launch` to start the conversation!
|
||||
|
||||

|
||||
|
||||
## Next Steps
|
||||
|
||||
- [Learn about the Cloud UI](/usage/cloud/cloud-ui).
|
||||
|
||||
@@ -12,13 +12,10 @@ For the available API endpoints, refer to the
|
||||
To use the OpenHands Cloud API, you'll need to generate an API key:
|
||||
|
||||
1. Log in to your [OpenHands Cloud](https://app.all-hands.dev) account.
|
||||
2. Navigate to the [Settings page](https://app.all-hands.dev/settings).
|
||||
3. Select the `API Keys` tab.
|
||||
4. Click `Create API Key`.
|
||||
5. Give your key a descriptive name (Example: "Development" or "Production") and select `Create`.
|
||||
6. Copy the generated API key and store it securely. It will only be shown once.
|
||||
|
||||

|
||||
2. Navigate to the [Settings > API Keys](https://app.all-hands.dev/settings/api-keys) page.
|
||||
3. Click `Create API Key`.
|
||||
4. Give your key a descriptive name (Example: "Development" or "Production") and select `Create`.
|
||||
5. Copy the generated API key and store it securely. It will only be shown once.
|
||||
|
||||
## API Usage
|
||||
|
||||
|
||||
@@ -8,24 +8,39 @@ description: The Cloud UI provides a web interface for interacting with OpenHand
|
||||
|
||||
The landing page is where you can:
|
||||
|
||||
- [Add GitHub repository access](/usage/cloud/github-installation#adding-github-repository-access) to OpenHands.
|
||||
- [Select a GitHub repo](/usage/cloud/github-installation#working-with-github-repos-in-openhands-cloud),
|
||||
[a GitLab repo](/usage/cloud/gitlab-installation#working-with-gitlab-repos-in-openhands-cloud) or
|
||||
[a Bitbucket repo](/usage/cloud/bitbucket-installation#working-with-bitbucket-repos-in-openhands-cloud) to start working on.
|
||||
- Launch an empty conversation using `New Conversation`.
|
||||
- See `Suggested Tasks` for repositories that OpenHands has access to.
|
||||
- Launch an empty conversation using `Launch from Scratch`.
|
||||
- See your `Recent Conversations`.
|
||||
|
||||
## Settings
|
||||
|
||||
The Settings page allows you to:
|
||||
Settings are divided across tabs, with each tab focusing on a specific area of configuration.
|
||||
|
||||
- [Configure GitHub repository access](/usage/cloud/github-installation#modifying-repository-access) for OpenHands.
|
||||
- [Install the OpenHands Slack app](/usage/cloud/slack-installation).
|
||||
- Set application settings like your preferred language, notifications and other preferences.
|
||||
- Add credits to your account.
|
||||
- [Generate custom secrets](/usage/common-settings#secrets-management).
|
||||
- [Create API keys to work with OpenHands programmatically](/usage/cloud/cloud-api).
|
||||
- Change your email address.
|
||||
- `User`
|
||||
- Change your email address.
|
||||
- `Integrations`
|
||||
- [Configure GitHub repository access](/usage/cloud/github-installation#modifying-repository-access) for OpenHands.
|
||||
- [Install the OpenHands Slack app](/usage/cloud/slack-installation).
|
||||
- `Application`
|
||||
- Set your preferred language, notifications and other preferences.
|
||||
- Toggle task suggestions on GitHub.
|
||||
- Toggle Solvability Analysis.
|
||||
- Set a maximum budget per conversation.
|
||||
- Configure the username and email that OpenHands uses for commits.
|
||||
- `LLM` (Available for [Pro subscription users](/usage/cloud/pro-subscription))
|
||||
- Choose to use another LLM or use different models from the OpenHands provider.
|
||||
- `Billing`
|
||||
- Add credits for using the OpenHands provider.
|
||||
- Cancel your `Pro subscription`.
|
||||
- `Secrets`
|
||||
- [Manage secrets](/usage/settings/secrets-settings).
|
||||
- `API Keys`
|
||||
- [Create API keys to work with OpenHands programmatically](/usage/cloud/cloud-api).
|
||||
- `MCP`
|
||||
- [Setup an MCP server](/usage/settings/mcp-settings)
|
||||
|
||||
## Key Features
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ description: This guide walks you through the process of installing OpenHands Cl
|
||||
|
||||
You can grant OpenHands access to specific GitHub repositories:
|
||||
|
||||
1. Click on `Add GitHub repos` on the landing page.
|
||||
1. Click on `+ Add GitHub Repos` in the repository selection dropdown.
|
||||
2. Select your organization and choose the specific repositories to grant OpenHands access to.
|
||||
<Accordion title="OpenHands permissions">
|
||||
- OpenHands requests short-lived tokens (8-hour expiration) with these permissions:
|
||||
@@ -34,20 +34,22 @@ You can grant OpenHands access to specific GitHub repositories:
|
||||
## Modifying Repository Access
|
||||
|
||||
You can modify GitHub repository access at any time by:
|
||||
- Selecting `Add GitHub repos` on the landing page or
|
||||
- Visiting the Settings page and selecting `Configure GitHub Repositories` under the `Integrations` tab
|
||||
- Selecting `+ Add GitHub Repos` in the repository selection dropdown or
|
||||
- Visiting the `Settings > Integrations` page and selecting `Configure GitHub Repositories`
|
||||
|
||||
## Working With GitHub Repos in Openhands Cloud
|
||||
|
||||
Once you've granted GitHub repository access, you can start working with your GitHub repository. Use the `select a repo`
|
||||
and `select a branch` dropdowns to select the appropriate repository and branch you'd like OpenHands to work on. Then
|
||||
click on `Launch` to start the conversation!
|
||||
Once you've granted GitHub repository access, you can start working with your GitHub repository. Use the
|
||||
`Open Repository` section to select the appropriate repository and branch you'd like OpenHands to work on. Then click
|
||||
on `Launch` to start the conversation!
|
||||
|
||||

|
||||
|
||||
## Working on Github Issues and Pull Requests Using Openhands
|
||||
## Working on GitHub Issues and Pull Requests Using Openhands
|
||||
|
||||
Giving GitHub repository access to OpenHands also allows you to work on GitHub issues and pull requests directly.
|
||||
To allow OpenHands to work directly from GitHub directly, you must
|
||||
[give OpenHands access to your repository](/usage/cloud/github-installation#modifying-repository-access). Once access is
|
||||
given, you can use OpenHands by labeling the issue or by tagging `@openhands`.
|
||||
|
||||
### Working with Issues
|
||||
|
||||
@@ -64,7 +66,12 @@ To get OpenHands to work on pull requests, mention `@openhands` in the comments
|
||||
- Request updates
|
||||
- Get code explanations
|
||||
|
||||
**Important Note**: The `@openhands` mention functionality in pull requests only works if the pull request is both *to* and *from* a repository that you have added through the interface. This is because OpenHands needs appropriate permissions to access both repositories.
|
||||
<Note>
|
||||
The `@openhands` mention functionality in pull requests only works if the pull request is both
|
||||
*to* and *from* a repository that you have added through the interface. This is because OpenHands needs appropriate
|
||||
permissions to access both repositories.
|
||||
</Note>
|
||||
|
||||
|
||||
## Next Steps
|
||||
|
||||
|
||||
@@ -14,16 +14,17 @@ Upon signing into OpenHands Cloud with a GitLab account, OpenHands will have acc
|
||||
|
||||
## Working With GitLab Repos in Openhands Cloud
|
||||
|
||||
After signing in with a Gitlab account, use the `select a repo` and `select a branch` dropdowns to select the
|
||||
appropriate repository and branch you'd like OpenHands to work on. Then click on `Launch` to start the conversation!
|
||||
After signing in with a Gitlab account, use the `Open Repository` section to select the appropriate repository and
|
||||
branch you'd like OpenHands to work on. Then click on `Launch` to start the conversation!
|
||||
|
||||

|
||||

|
||||
|
||||
## Using Tokens with Reduced Scopes
|
||||
|
||||
OpenHands requests an API-scoped token during OAuth authentication. By default, this token is provided to the agent.
|
||||
To restrict the agent's permissions, you can define a custom secret `GITLAB_TOKEN`, which will override the default token assigned to the agent.
|
||||
While the high-permission API token is still requested and used for other components of the application (e.g. opening merge requests), the agent will not have access to it.
|
||||
To restrict the agent's permissions, [you can define a custom secret](/usage/settings/secrets-settings) `GITLAB_TOKEN`,
|
||||
which will override the default token assigned to the agent. While the high-permission API token is still requested
|
||||
and used for other components of the application (e.g. opening merge requests), the agent will not have access to it.
|
||||
|
||||
## Working on GitLab Issues and Merge Requests Using Openhands
|
||||
|
||||
@@ -32,7 +33,8 @@ This feature works for personal projects and is available for group projects wit
|
||||
[Premium or Ultimate tier subscription](https://docs.gitlab.com/user/project/integrations/webhooks/#group-webhooks).
|
||||
|
||||
A webhook is automatically installed within a few minutes after the owner/maintainer of the project or group logs into
|
||||
OpenHands Cloud. If you decide to delete the webhook, then re-installing will require the support of All Hands AI but we are planning to improve this in a future release.
|
||||
OpenHands Cloud. If you decide to delete the webhook, then re-installing will require the support of All Hands AI but
|
||||
we are planning to improve this in a future release.
|
||||
</Note>
|
||||
|
||||
Giving GitLab repository access to OpenHands also allows you to work on GitLab issues and merge requests directly.
|
||||
|
||||
48
docs/usage/cloud/pro-subscription.mdx
Normal file
@@ -0,0 +1,48 @@
|
||||
---
|
||||
title: "Pro Subscription"
|
||||
description: "Learn about OpenHands Cloud Pro Subscription features and pricing"
|
||||
---
|
||||
|
||||
The OpenHands Pro Subscription unlocks additional features and better pricing when you run OpenHands conversations in
|
||||
OpenHands Cloud.
|
||||
|
||||
## Base Features
|
||||
|
||||
All users start on the Pay-as-you-go plan and have access to these base features when they sign up:
|
||||
|
||||
* **Run multiple OpenHands conversations on OpenHands Cloud runtimes.**
|
||||
* **API keys to the OpenHands LLM provider for use in OpenHands CLI or when running OpenHands on your own**
|
||||
* **$20 in initial OpenHands Cloud credits to get started.**
|
||||
* **Support for GitHub, GitLab, Bitbucket, Slack, and more.**
|
||||
|
||||
## What you get with a Pro Subscription
|
||||
|
||||
The $20/month Pro Subscription covers the cost of runtime compute in OpenHands Cloud, plus enables the following
|
||||
features:
|
||||
|
||||
* **Bring Your Own LLM Keys:** Bring your own API keys from OpenAI, Anthropic, Mistral, and other providers.
|
||||
* **Model Choice:** Unlocks access to OpenHands LLM provider models for use within OpenHands Cloud.
|
||||
* **No Markup Pricing on LLM usage:** When you use the OpenHands LLM provider in OpenHands Cloud, you pay for
|
||||
LLM usage at-cost (zero markup) based on API prices.
|
||||
|
||||
## Plan Comparison
|
||||
|
||||
Here are the key differences between Pay-as-you-go and Pro subscriptions:
|
||||
|
||||
### When running OpenHands conversations in OpenHands Cloud
|
||||
| | Pay-as-you-go | Pro Subscription |
|
||||
| :---- | ----- | ----- |
|
||||
| Monthly price | None \- no commitment | $20/month |
|
||||
| Can I bring my own LLM key? | No | ✅ Yes |
|
||||
| Do I pay for LLM usage? | ✅ Yes | ✅ Yes |
|
||||
| Can I select from different LLMs without bringing my own LLM key? | No \- defaults to Claude Sonnet 4 | ✅ Yes \- via OpenHands LLM provider <br/><br/>[*See models and pricing*](https://docs.all-hands.dev/usage/llms/openhands-llms#pricing) |
|
||||
| How much am I charged for LLM usage? | **Marked up pricing** \- 2x Claude Sonnet 4 API prices. *This markup helps cover the cost of runtime compute.* | **No markup** \- 1x API prices. *The $20 monthly subscription covers the cost of runtime compute.* |
|
||||
|
||||
|
||||
### When using the OpenHands LLM Provider outside of OpenHands Cloud
|
||||
The following applies to **both** the Pay-as-you-go and Pro subscription:
|
||||
| | Pay-as-you-go or Pro Subscription |
|
||||
| :---- | :---- |
|
||||
| Do I have access to multiple models via the OpenHands LLM provider? | ✅ Yes <br/><br/> [*See models and pricing*](https://docs.all-hands.dev/usage/llms/openhands-llms#pricing) |
|
||||
| Can I generate and refresh OpenHands LLM API keys? | ✅ Yes |
|
||||
| How much am I charged for LLM usage when I use the OpenHands LLM provider in other AI coding tools? | **No markup** \- pay 1x API prices <br/> [*See models and pricing*](https://docs.all-hands.dev/usage/llms/openhands-llms#pricing) <br/><br/> *Usage is deducted from your OpenHands Cloud credit balance.* <br/><br/> *The OpenHands LLM provider is available to all OpenHands Cloud users, and LLM usage is billed at-cost (zero markup). Use these models with OpenHands CLI, running OpenHands on your own, or even other AI coding agents\! [Learn more.](https://www.all-hands.dev/blog/access-state-of-the-art-llm-models-at-cost-via-openhands-gui-and-cli)* |
|
||||
@@ -13,7 +13,9 @@ description: This guide walks you through installing the OpenHands Slack app.
|
||||
</iframe>
|
||||
|
||||
<Info>
|
||||
OpenHands utilizes a large language model (LLM), which may generate responses that are inaccurate or incomplete. While we strive for accuracy, OpenHands' outputs are not guaranteed to be correct, and we encourage users to validate critical information independently.
|
||||
OpenHands utilizes a large language model (LLM), which may generate responses that are inaccurate or incomplete.
|
||||
While we strive for accuracy, OpenHands' outputs are not guaranteed to be correct, and we encourage users to
|
||||
validate critical information independently.
|
||||
</Info>
|
||||
|
||||
## Prerequisites
|
||||
@@ -39,7 +41,7 @@ OpenHands utilizes a large language model (LLM), which may generate responses th
|
||||
**Make sure your Slack workspace admin/owner has installed OpenHands Slack App first.**
|
||||
|
||||
Every user in the Slack workspace (including admins/owners) must link their OpenHands Cloud account to the OpenHands Slack App. To do this:
|
||||
1. Visit [integrations settings](https://app.all-hands.dev/settings/integrations) in OpenHands Cloud.
|
||||
1. Visit the [Settings > Integrations](https://app.all-hands.dev/settings/integrations) page in OpenHands Cloud.
|
||||
2. Click `Install OpenHands Slack App`.
|
||||
3. In the top right corner, select the workspace to install the OpenHands Slack app.
|
||||
4. Review permissions and click allow.
|
||||
@@ -57,7 +59,8 @@ To start a new conversation, you can mention `@openhands` in a new message or a
|
||||
|
||||
Once a conversation is started, all thread messages underneath it will be follow-up messages to OpenHands.
|
||||
|
||||
To send follow-up messages for the same conversation, mention `@openhands` in a thread reply to the original message. You must be the user who started the conversation.
|
||||
To send follow-up messages for the same conversation, mention `@openhands` in a thread reply to the original message.
|
||||
You must be the user who started the conversation.
|
||||
|
||||
## Example conversation
|
||||
|
||||
|
||||
@@ -105,7 +105,7 @@ The conversation history will be saved in `~/.openhands/sessions`.
|
||||
|
||||
1. Set the following environment variables in your terminal:
|
||||
- `SANDBOX_VOLUMES` to specify the directory you want OpenHands to access ([See using SANDBOX_VOLUMES for more info](../runtimes/docker#using-sandbox_volumes))
|
||||
- `LLM_MODEL` - the LLM model to use (e.g. `export LLM_MODEL="anthropic/claude-sonnet-4-20250514"`)
|
||||
- `LLM_MODEL` - the LLM model to use (e.g. `export LLM_MODEL="anthropic/claude-sonnet-4-20250514"` or `export LLM_MODEL="anthropic/claude-sonnet-4-5-20250929"`)
|
||||
- `LLM_API_KEY` - your API key (e.g. `export LLM_API_KEY="sk_test_12345"`)
|
||||
|
||||
2. Run the following command:
|
||||
@@ -113,7 +113,7 @@ The conversation history will be saved in `~/.openhands/sessions`.
|
||||
```bash
|
||||
docker run -it \
|
||||
--pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.57-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.58-nikolaik \
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e SANDBOX_VOLUMES=$SANDBOX_VOLUMES \
|
||||
-e LLM_API_KEY=$LLM_API_KEY \
|
||||
@@ -122,7 +122,7 @@ docker run -it \
|
||||
-v ~/.openhands:/.openhands \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app-$(date +%Y%m%d%H%M%S) \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.57 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.58 \
|
||||
python -m openhands.cli.entry --override-cli-mode true
|
||||
```
|
||||
|
||||
|
||||
@@ -85,11 +85,11 @@ You can use the Settings page at any time to:
|
||||
|
||||
- Setup the LLM provider and model for OpenHands.
|
||||
- [Setup the search engine](/usage/search-engine-setup).
|
||||
- [Configure MCP servers](/usage/mcp).
|
||||
- [Configure MCP servers](/usage/settings/mcp-settings).
|
||||
- [Connect to GitHub](/usage/how-to/gui-mode#github-setup), [connect to GitLab](/usage/how-to/gui-mode#gitlab-setup)
|
||||
and [connect to Bitbucket](/usage/how-to/gui-mode#bitbucket-setup).
|
||||
- Set application settings like your preferred language, notifications and other preferences.
|
||||
- [Manage custom secrets](/usage/common-settings#secrets-management).
|
||||
- [Manage custom secrets](/usage/settings/secrets-settings).
|
||||
|
||||
#### GitHub Setup
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ Set environment variables and run the Docker command:
|
||||
```bash
|
||||
# Set required environment variables
|
||||
export SANDBOX_VOLUMES="/path/to/workspace:/workspace:rw" # Format: host_path:container_path:mode
|
||||
export LLM_MODEL="anthropic/claude-sonnet-4-20250514"
|
||||
export LLM_MODEL="anthropic/claude-sonnet-4-20250514" # or "anthropic/claude-sonnet-4-5-20250929"
|
||||
export LLM_API_KEY="your-api-key"
|
||||
export SANDBOX_SELECTED_REPO="owner/repo-name" # Optional: requires GITHUB_TOKEN
|
||||
export GITHUB_TOKEN="your-token" # Required for repository operations
|
||||
@@ -61,7 +61,7 @@ export GITHUB_TOKEN="your-token" # Required for repository operations
|
||||
# Run OpenHands
|
||||
docker run -it \
|
||||
--pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.57-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.58-nikolaik \
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e SANDBOX_VOLUMES=$SANDBOX_VOLUMES \
|
||||
-e LLM_API_KEY=$LLM_API_KEY \
|
||||
@@ -73,7 +73,7 @@ docker run -it \
|
||||
-v ~/.openhands:/.openhands \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app-$(date +%Y%m%d%H%M%S) \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.57 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.58 \
|
||||
python -m openhands.core.main -t "write a bash script that prints hi"
|
||||
```
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ Based on these findings and community feedback, these are the latest models that
|
||||
### Cloud / API-Based Models
|
||||
|
||||
- [anthropic/claude-sonnet-4-20250514](https://www.anthropic.com/api) (recommended)
|
||||
- [anthropic/claude-sonnet-4-5-20250929](https://www.anthropic.com/api) (recommended)
|
||||
- [openai/gpt-5-2025-08-07](https://openai.com/api/) (recommended)
|
||||
- [gemini/gemini-2.5-pro](https://blog.google/technology/google-deepmind/gemini-model-thinking-updates-march-2025/)
|
||||
- [deepseek/deepseek-chat](https://api-docs.deepseek.com/)
|
||||
|
||||
@@ -68,23 +68,23 @@ Download and install the LM Studio desktop app from [lmstudio.ai](https://lmstud
|
||||
1. Check [the installation guide](/usage/local-setup) and ensure all prerequisites are met before running OpenHands, then run:
|
||||
|
||||
```bash
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.57-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.58-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.57-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.58-nikolaik \
|
||||
-e LOG_ALL_EVENTS=true \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-v ~/.openhands:/.openhands \
|
||||
-p 3000:3000 \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.57
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.58
|
||||
```
|
||||
|
||||
2. Wait until the server is running (see log below):
|
||||
```
|
||||
Digest: sha256:e72f9baecb458aedb9afc2cd5bc935118d1868719e55d50da73190d3a85c674f
|
||||
Status: Image is up to date for docker.all-hands.dev/all-hands-ai/openhands:0.57
|
||||
Status: Image is up to date for docker.all-hands.dev/all-hands-ai/openhands:0.58
|
||||
Starting OpenHands...
|
||||
Running OpenHands as root
|
||||
14:22:13 - openhands:INFO: server_config.py:50 - Using config class None
|
||||
|
||||
@@ -15,7 +15,7 @@ description: OpenHands LLM provider with access to state-of-the-art (SOTA) agent
|
||||
|
||||
When running OpenHands, you'll need to set the following in the OpenHands UI through the Settings under the `LLM` tab:
|
||||
- `LLM Provider` to `OpenHands`
|
||||
- `LLM Model` to the model you will be using (e.g. claude-sonnet-4-20250514)
|
||||
- `LLM Model` to the model you will be using (e.g. claude-sonnet-4-20250514 or claude-sonnet-4-5-20250929)
|
||||
- `API Key` to your OpenHands LLM API key copied from above
|
||||
|
||||
## Using OpenHands LLM Provider in the CLI
|
||||
@@ -36,6 +36,7 @@ Pricing follows official API provider rates. Below are the current pricing detai
|
||||
|-------|----------------------------|-----------------------------------|------------------------------|------------------|-------------------|
|
||||
| claude-opus-4-20250514 | $15.00 | $1.50 | $75.00 | 200,000 | 32,000 |
|
||||
| claude-sonnet-4-20250514 | $3.00 | $0.30 | $15.00 | 200,000 | 64,000 |
|
||||
| claude-sonnet-4-5-20250929 | $3.00 | $0.30 | $15.00 | 200,000 | 64,000 |
|
||||
| devstral-medium-2507 | $0.40 | N/A | $2.00 | 128,000 | 128,000 |
|
||||
| devstral-small-2505 | $0.10 | N/A | $0.30 | 128,000 | 128,000 |
|
||||
| devstral-small-2507 | $0.10 | N/A | $0.30 | 128,000 | 128,000 |
|
||||
|
||||
@@ -116,17 +116,17 @@ Note that you'll still need `uv` installed for the default MCP servers to work p
|
||||
<Accordion title="Docker Command (Click to expand)">
|
||||
|
||||
```bash
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.57-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.58-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.57-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.58-nikolaik \
|
||||
-e LOG_ALL_EVENTS=true \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-v ~/.openhands:/.openhands \
|
||||
-p 3000:3000 \
|
||||
--add-host host.docker.internal:host-gateway \
|
||||
--name openhands-app \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.57
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.58
|
||||
```
|
||||
|
||||
</Accordion>
|
||||
|
||||
@@ -10,12 +10,15 @@ Model Context Protocol (MCP) is a mechanism that allows OpenHands to communicate
|
||||
servers can provide additional functionality to the agent, such as specialized data processing, external API access,
|
||||
or custom tools. MCP is based on the open standard defined at [modelcontextprotocol.io](https://modelcontextprotocol.io).
|
||||
|
||||
## Supported MCPs
|
||||
|
||||
<Note>
|
||||
MCP is currently not available on OpenHands Cloud. This feature is only available when running OpenHands locally.
|
||||
</Note>
|
||||
OpenHands supports the following MCP transport protocols:
|
||||
|
||||
### How MCP Works
|
||||
* [Server-Sent Events (SSE)](https://modelcontextprotocol.io/specification/2024-11-05/basic/transports#http-with-sse)
|
||||
* [Streamable HTTP (SHTTP)](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#streamable-http)
|
||||
* [Standard Input/Output (stdio)](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#stdio)
|
||||
|
||||
## How MCP Works
|
||||
|
||||
When OpenHands starts, it:
|
||||
|
||||
@@ -33,15 +36,90 @@ The agent can then use these tools just like any built-in tool. When the agent c
|
||||
## Configuration
|
||||
|
||||
MCP configuration can be defined in:
|
||||
* The OpenHands UI through the Settings under the `MCP` tab.
|
||||
* The OpenHands UI in the `Settings > MCP` page.
|
||||
* The `config.toml` file under the `[mcp]` section if not using the UI.
|
||||
|
||||
### Configuration Options
|
||||
|
||||
#### SSE Servers
|
||||
|
||||
SSE servers are configured using either a string URL or an object with the following properties:
|
||||
|
||||
- `url` (required)
|
||||
- Type: `str`
|
||||
- Description: The URL of the SSE server.
|
||||
|
||||
- `api_key` (optional)
|
||||
- Type: `str`
|
||||
- Description: API key for authentication.
|
||||
|
||||
#### SHTTP Servers
|
||||
|
||||
SHTTP (Streamable HTTP) servers are configured using either a string URL or an object with the following properties:
|
||||
|
||||
- `url` (required)
|
||||
- Type: `str`
|
||||
- Description: The URL of the SHTTP server.
|
||||
|
||||
- `api_key` (optional)
|
||||
- Type: `str`
|
||||
- Description: API key for authentication.
|
||||
|
||||
- `timeout` (optional)
|
||||
- Type: `int`
|
||||
- Default: `60`
|
||||
- Range: `1-3600` seconds (1 hour maximum)
|
||||
- Description: Timeout in seconds for tool execution. This prevents tool calls from hanging indefinitely.
|
||||
- **Use Cases:**
|
||||
- **Short timeout (1-30s)**: For lightweight operations like status checks or simple queries.
|
||||
- **Medium timeout (30-300s)**: For standard processing tasks like data analysis or API calls.
|
||||
- **Long timeout (300-3600s)**: For heavy operations like file processing, complex calculations, or batch operations.
|
||||
<Note>
|
||||
This timeout only applies to individual tool calls, not server connection establishment.
|
||||
</Note>
|
||||
|
||||
#### Stdio Servers
|
||||
|
||||
<Note>
|
||||
While stdio servers are supported, [we recommend using MCP proxies](/usage/settings/mcp-settings#configuration-examples) for
|
||||
better reliability and performance.
|
||||
</Note>
|
||||
|
||||
Stdio servers are configured using an object with the following properties:
|
||||
|
||||
- `name` (required)
|
||||
- Type: `str`
|
||||
- Description: A unique name for the server.
|
||||
|
||||
- `command` (required)
|
||||
- Type: `str`
|
||||
- Description: The command to run the server.
|
||||
|
||||
- `args` (optional)
|
||||
- Type: `list of str`
|
||||
- Default: `[]`
|
||||
- Description: Command-line arguments to pass to the server.
|
||||
|
||||
- `env` (optional)
|
||||
- Type: `dict of str to str`
|
||||
- Default: `{}`
|
||||
- Description: Environment variables to set for the server process.
|
||||
|
||||
##### When to Use Direct Stdio
|
||||
|
||||
Direct stdio connections may still be appropriate in these scenarios:
|
||||
- **Development and testing**: Quick prototyping of MCP servers.
|
||||
- **Simple, single-use tools**: Tools that don't require high reliability or concurrent access.
|
||||
- **Local-only environments**: When you don't want to manage additional proxy processes.
|
||||
|
||||
### Configuration Examples
|
||||
|
||||
#### Recommended: Using Proxy Servers (SSE/HTTP)
|
||||
|
||||
For stdio-based MCP servers, we recommend using MCP proxy tools like [`supergateway`](https://github.com/supercorp-ai/supergateway) instead of direct stdio connections.
|
||||
[SuperGateway](https://github.com/supercorp-ai/supergateway) is a popular MCP proxy that converts stdio MCP servers to HTTP/SSE endpoints:
|
||||
For stdio-based MCP servers, we recommend using MCP proxy tools like
|
||||
[`supergateway`](https://github.com/supercorp-ai/supergateway) instead of direct stdio connections.
|
||||
[SuperGateway](https://github.com/supercorp-ai/supergateway) is a popular MCP proxy that converts stdio MCP servers to
|
||||
HTTP/SSE endpoints.
|
||||
|
||||
Start the proxy servers separately:
|
||||
```bash
|
||||
@@ -67,10 +145,21 @@ sse_servers = [
|
||||
# External MCP service with authentication
|
||||
{url="https://api.example.com/mcp/sse", api_key="your-api-key"}
|
||||
]
|
||||
|
||||
# SHTTP Servers - Modern streamable HTTP transport (recommended)
|
||||
shttp_servers = [
|
||||
# Basic SHTTP server with default 60s timeout
|
||||
"https://api.example.com/mcp/shttp",
|
||||
|
||||
# Server with custom timeout for heavy operations
|
||||
{
|
||||
url = "https://files.example.com/mcp/shttp",
|
||||
api_key = "your-api-key",
|
||||
timeout = 1800 # 30 minutes for large file processing
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
|
||||
|
||||
#### Alternative: Direct Stdio Servers (Not Recommended for Production)
|
||||
|
||||
```toml
|
||||
@@ -92,105 +181,12 @@ stdio_servers = [
|
||||
]
|
||||
```
|
||||
|
||||
## Configuration Options
|
||||
|
||||
### SSE Servers
|
||||
|
||||
SSE servers are configured using either a string URL or an object with the following properties:
|
||||
|
||||
- `url` (required)
|
||||
- Type: `str`
|
||||
- Description: The URL of the SSE server
|
||||
|
||||
- `api_key` (optional)
|
||||
- Type: `str`
|
||||
- Description: API key for authentication
|
||||
|
||||
### SHTTP Servers
|
||||
|
||||
SHTTP (Streamable HTTP) servers are configured using either a string URL or an object with the following properties:
|
||||
|
||||
- `url` (required)
|
||||
- Type: `str`
|
||||
- Description: The URL of the SHTTP server
|
||||
|
||||
- `api_key` (optional)
|
||||
- Type: `str`
|
||||
- Description: API key for authentication
|
||||
|
||||
### Stdio Servers
|
||||
|
||||
**Note**: While stdio servers are supported, we recommend using MCP proxies (see above) for better reliability and performance.
|
||||
|
||||
Stdio servers are configured using an object with the following properties:
|
||||
|
||||
- `name` (required)
|
||||
- Type: `str`
|
||||
- Description: A unique name for the server
|
||||
|
||||
- `command` (required)
|
||||
- Type: `str`
|
||||
- Description: The command to run the server
|
||||
|
||||
- `args` (optional)
|
||||
- Type: `list of str`
|
||||
- Default: `[]`
|
||||
- Description: Command-line arguments to pass to the server
|
||||
|
||||
- `env` (optional)
|
||||
- Type: `dict of str to str`
|
||||
- Default: `{}`
|
||||
- Description: Environment variables to set for the server process
|
||||
|
||||
|
||||
#### When to Use Direct Stdio
|
||||
|
||||
Direct stdio connections may still be appropriate in these scenarios:
|
||||
- **Development and testing**: Quick prototyping of MCP servers
|
||||
- **Simple, single-use tools**: Tools that don't require high reliability or concurrent access
|
||||
- **Local-only environments**: When you don't want to manage additional proxy processes
|
||||
|
||||
For production use, we recommend using proxy tools like SuperGateway.
|
||||
|
||||
### Other Proxy Tools
|
||||
|
||||
Other options include:
|
||||
|
||||
- **Custom FastAPI/Express servers**: Build your own HTTP wrapper around stdio MCP servers
|
||||
- **Docker-based proxies**: Containerized solutions for better isolation
|
||||
- **Cloud-hosted MCP services**: Third-party services that provide MCP endpoints
|
||||
|
||||
### Troubleshooting MCP Connections
|
||||
|
||||
#### Common Issues with Stdio Servers
|
||||
- **Process crashes**: Stdio processes may crash without proper error handling
|
||||
- **Deadlocks**: Stdio communication can deadlock under high load
|
||||
- **Resource leaks**: Zombie processes if not properly managed
|
||||
- **Debugging difficulty**: Hard to inspect stdio communication
|
||||
|
||||
#### Benefits of Using Proxies
|
||||
- **HTTP status codes**: Clear error reporting via standard HTTP responses
|
||||
- **Request logging**: Easy to log and monitor HTTP requests
|
||||
- **Load balancing**: Can distribute requests across multiple server instances
|
||||
- **Health checks**: HTTP endpoints can provide health status
|
||||
- **CORS support**: Better integration with web-based tools
|
||||
|
||||
## Transport Protocols
|
||||
|
||||
OpenHands supports three different MCP transport protocols:
|
||||
|
||||
### Server-Sent Events (SSE)
|
||||
SSE is a legacy HTTP-based transport that uses Server-Sent Events for server-to-client communication and HTTP POST requests for client-to-server communication. This transport is suitable for basic streaming scenarios but has limitations in session management and connection resumability.
|
||||
|
||||
### Streamable HTTP (SHTTP)
|
||||
SHTTP is the modern HTTP-based transport protocol that provides enhanced features over SSE:
|
||||
|
||||
- **Improved Session Management**: Supports stateful sessions with session IDs for maintaining context across requests
|
||||
- **Connection Resumability**: Can resume broken connections and replay missed messages using event IDs
|
||||
- **Bidirectional Communication**: Uses HTTP POST for client-to-server and optional SSE streams for server-to-client communication
|
||||
- **Better Error Handling**: Enhanced error reporting and recovery mechanisms
|
||||
|
||||
SHTTP is the recommended transport for HTTP-based MCP servers as it provides better reliability and features compared to the legacy SSE transport.
|
||||
|
||||
### Standard Input/Output (stdio)
|
||||
Stdio transport enables communication through standard input and output streams, making it ideal for local integrations and command-line tools. This transport is used for locally executed MCP servers that run as separate processes.
|
||||
- **Custom FastAPI/Express servers**: Build your own HTTP wrapper around stdio MCP servers.
|
||||
- **Docker-based proxies**: Containerized solutions for better isolation.
|
||||
- **Cloud-hosted MCP services**: Third-party services that provide MCP endpoints.
|
||||
@@ -1,28 +1,19 @@
|
||||
---
|
||||
title: OpenHands Settings
|
||||
description: Overview of some of the settings available in OpenHands.
|
||||
title: Secrets Management
|
||||
description: How to manage secrets in OpenHands.
|
||||
---
|
||||
|
||||
## Openhands Cloud vs Running on Your Own
|
||||
|
||||
There are some differences between the settings available in OpenHands Cloud and those available when running OpenHands
|
||||
on your own:
|
||||
* [OpenHands Cloud settings](/usage/cloud/cloud-ui#settings)
|
||||
* [Settings available when running on your own](/usage/how-to/gui-mode#settings)
|
||||
|
||||
Refer to these pages for more detailed information.
|
||||
|
||||
## Secrets Management
|
||||
## Overview
|
||||
|
||||
OpenHands provides a secrets manager that allows you to securely store and manage sensitive information that can be
|
||||
accessed by the agent during runtime, such as API keys. These secrets are automatically exported as environment
|
||||
variables in the agent's runtime environment.
|
||||
|
||||
### Accessing the Secrets Manager
|
||||
## Accessing the Secrets Manager
|
||||
|
||||
In the Settings page, navigate to the `Secrets` tab. Here, you'll see a list of all your existing custom secrets.
|
||||
Navigate to the `Settings > Secrets` page. Here, you'll see a list of all your existing custom secrets.
|
||||
|
||||
### Adding a New Secret
|
||||
## Adding a New Secret
|
||||
1. Click `Add a new secret`.
|
||||
2. Fill in the following fields:
|
||||
- **Name**: A unique identifier for your secret (e.g., `AWS_ACCESS_KEY`). This will be the environment variable name.
|
||||
@@ -30,7 +21,7 @@ In the Settings page, navigate to the `Secrets` tab. Here, you'll see a list of
|
||||
- **Description** (optional): A brief description of what the secret is used for, which is also provided to the agent.
|
||||
3. Click `Add secret` to save.
|
||||
|
||||
### Editing a Secret
|
||||
## Editing a Secret
|
||||
|
||||
1. Click the `Edit` button next to the secret you want to modify.
|
||||
2. You can update the name and description of the secret.
|
||||
@@ -39,14 +30,13 @@ In the Settings page, navigate to the `Secrets` tab. Here, you'll see a list of
|
||||
value, delete the secret and create a new one.
|
||||
</Note>
|
||||
|
||||
### Deleting a Secret
|
||||
## Deleting a Secret
|
||||
|
||||
1. Click the `Delete` button next to the secret you want to remove.
|
||||
2. Select `Confirm` to delete the secret.
|
||||
|
||||
### Using Secrets in the Agent
|
||||
## Using Secrets in the Agent
|
||||
- All custom secrets are automatically exported as environment variables in the agent's runtime environment.
|
||||
- You can access them in your code using standard environment variable access methods
|
||||
(e.g., `os.environ['SECRET_NAME']` in Python).
|
||||
- Example: If you create a secret named `OPENAI_API_KEY`, you can access it in your code as
|
||||
`process.env.OPENAI_API_KEY` in JavaScript or `os.environ['OPENAI_API_KEY']` in Python.
|
||||
- You can access them in your code using standard environment variable access methods. For example, if you create a
|
||||
secret named `OPENAI_API_KEY`, you can access it in your code as `process.env.OPENAI_API_KEY` in JavaScript or
|
||||
`os.environ['OPENAI_API_KEY']` in Python.
|
||||
@@ -3,18 +3,16 @@ from logging.config import fileConfig
|
||||
|
||||
from alembic import context
|
||||
from google.cloud.sql.connector import Connector
|
||||
from sqlalchemy import create_engine, event
|
||||
|
||||
from sqlalchemy import create_engine
|
||||
from storage.base import Base
|
||||
|
||||
target_metadata = Base.metadata
|
||||
|
||||
DB_USER = os.getenv('DB_USER', 'postgres')
|
||||
DB_PASS = os.getenv('DB_PASS', 'postgres')
|
||||
DB_HOST = os.getenv('DB_HOST', 'localhost')
|
||||
DB_PORT = os.getenv('DB_PORT', '5432')
|
||||
DB_NAME = os.getenv('DB_NAME', 'openhands')
|
||||
DB_SCHEMA = os.getenv('DB_SCHEMA')
|
||||
DB_AUTH_TYPE = os.getenv('DB_AUTH_TYPE', 'password') # 'password' or 'rds-iam'
|
||||
AWS_REGION = os.getenv('AWS_REGION', 'us-east-1') # AWS region for RDS IAM auth
|
||||
|
||||
GCP_DB_INSTANCE = os.getenv('GCP_DB_INSTANCE')
|
||||
GCP_PROJECT = os.getenv('GCP_PROJECT')
|
||||
@@ -23,24 +21,6 @@ GCP_REGION = os.getenv('GCP_REGION')
|
||||
POOL_SIZE = int(os.getenv('DB_POOL_SIZE', '25'))
|
||||
MAX_OVERFLOW = int(os.getenv('DB_MAX_OVERFLOW', '10'))
|
||||
|
||||
target_metadata = Base.metadata
|
||||
# Set schema for target metadata if DB_SCHEMA is provided
|
||||
if DB_SCHEMA:
|
||||
target_metadata.schema = DB_SCHEMA
|
||||
|
||||
# RDS IAM authentication setup
|
||||
if DB_AUTH_TYPE == 'rds-iam':
|
||||
import boto3
|
||||
|
||||
# boto3 client (reused for token generation)
|
||||
rds = boto3.client('rds', region_name=AWS_REGION)
|
||||
|
||||
def get_auth_token():
|
||||
"""Generate a fresh IAM DB auth token."""
|
||||
return rds.generate_db_auth_token(
|
||||
DBHostname=DB_HOST, Port=DB_PORT, DBUsername=DB_USER
|
||||
)
|
||||
|
||||
|
||||
def get_engine(database_name=DB_NAME):
|
||||
"""Create SQLAlchemy engine with optional database name."""
|
||||
@@ -49,87 +29,29 @@ def get_engine(database_name=DB_NAME):
|
||||
def get_db_connection():
|
||||
connector = Connector()
|
||||
instance_string = f'{GCP_PROJECT}:{GCP_REGION}:{GCP_DB_INSTANCE}'
|
||||
connect_kwargs = {
|
||||
'user': DB_USER,
|
||||
'password': DB_PASS.strip(),
|
||||
'db': database_name,
|
||||
}
|
||||
# Note: pg8000 doesn't accept 'options' parameter, so we'll handle schema via SQL
|
||||
# Schema will be set after connection via event listener
|
||||
return connector.connect(instance_string, 'pg8000', **connect_kwargs)
|
||||
return connector.connect(
|
||||
instance_string,
|
||||
'pg8000',
|
||||
user=DB_USER,
|
||||
password=DB_PASS.strip(),
|
||||
db=database_name,
|
||||
)
|
||||
|
||||
engine = create_engine(
|
||||
return create_engine(
|
||||
'postgresql+pg8000://',
|
||||
creator=get_db_connection,
|
||||
pool_size=POOL_SIZE,
|
||||
max_overflow=MAX_OVERFLOW,
|
||||
pool_pre_ping=True,
|
||||
)
|
||||
|
||||
# Set schema via SQL after connection if specified
|
||||
if DB_SCHEMA:
|
||||
@event.listens_for(engine, 'connect')
|
||||
def set_search_path(dbapi_connection, connection_record):
|
||||
with dbapi_connection.cursor() as cursor:
|
||||
cursor.execute(f"SET search_path TO {DB_SCHEMA}")
|
||||
dbapi_connection.commit()
|
||||
|
||||
return engine
|
||||
else:
|
||||
if DB_AUTH_TYPE == 'rds-iam':
|
||||
# Build a SQLAlchemy connection URL with a dummy password — token will be injected dynamically
|
||||
# Note: SSL is enabled by default for pg8000 when connecting to RDS
|
||||
# For pg8000, we cannot use URL parameters like options, so schema must be handled differently
|
||||
base_url = (
|
||||
f'postgresql+pg8000://{DB_USER}:dummy-password'
|
||||
f'@{DB_HOST}:{DB_PORT}/{database_name}'
|
||||
)
|
||||
engine = create_engine(
|
||||
base_url,
|
||||
pool_size=POOL_SIZE,
|
||||
max_overflow=MAX_OVERFLOW,
|
||||
pool_pre_ping=True,
|
||||
)
|
||||
|
||||
# Hook: before a connection is made, inject a fresh token
|
||||
@event.listens_for(engine, 'do_connect')
|
||||
def provide_token(dialect, conn_rec, cargs, cparams):
|
||||
token = get_auth_token()
|
||||
# Replace password in connect arguments
|
||||
cparams['password'] = token
|
||||
return dialect.connect(*cargs, **cparams)
|
||||
|
||||
# Hook: after connection is established, set the schema if specified
|
||||
if DB_SCHEMA:
|
||||
@event.listens_for(engine, 'connect')
|
||||
def set_search_path(dbapi_connection, connection_record):
|
||||
with dbapi_connection.cursor() as cursor:
|
||||
cursor.execute(f"SET search_path TO {DB_SCHEMA}")
|
||||
dbapi_connection.commit()
|
||||
|
||||
return engine
|
||||
else:
|
||||
# Regular password authentication
|
||||
# Use postgresql:// (default driver) but handle schema via SQL to be safe
|
||||
url = (
|
||||
f'postgresql://{DB_USER}:{DB_PASS}@{DB_HOST}:{DB_PORT}/{database_name}'
|
||||
)
|
||||
engine = create_engine(
|
||||
url,
|
||||
pool_size=POOL_SIZE,
|
||||
max_overflow=MAX_OVERFLOW,
|
||||
pool_pre_ping=True,
|
||||
)
|
||||
|
||||
# Set schema via SQL after connection if specified
|
||||
if DB_SCHEMA:
|
||||
@event.listens_for(engine, 'connect')
|
||||
def set_search_path(dbapi_connection, connection_record):
|
||||
with dbapi_connection.cursor() as cursor:
|
||||
cursor.execute(f"SET search_path TO {DB_SCHEMA}")
|
||||
dbapi_connection.commit()
|
||||
|
||||
return engine
|
||||
url = f'postgresql://{DB_USER}:{DB_PASS}@{DB_HOST}:{DB_PORT}/{database_name}'
|
||||
return create_engine(
|
||||
url,
|
||||
pool_size=POOL_SIZE,
|
||||
max_overflow=MAX_OVERFLOW,
|
||||
pool_pre_ping=True,
|
||||
)
|
||||
|
||||
|
||||
engine = get_engine()
|
||||
@@ -161,7 +83,6 @@ def run_migrations_offline() -> None:
|
||||
target_metadata=target_metadata,
|
||||
literal_binds=True,
|
||||
dialect_opts={'paramstyle': 'named'},
|
||||
version_table_schema=target_metadata.schema,
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
|
||||
140
enterprise/poetry.lock
generated
@@ -766,7 +766,7 @@ version = "1.17.1"
|
||||
description = "Foreign Function Interface for Python calling C code."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
groups = ["main", "test"]
|
||||
files = [
|
||||
{file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"},
|
||||
{file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"},
|
||||
@@ -836,6 +836,7 @@ files = [
|
||||
{file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"},
|
||||
{file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"},
|
||||
]
|
||||
markers = {test = "platform_python_implementation == \"CPython\" and sys_platform == \"win32\""}
|
||||
|
||||
[package.dependencies]
|
||||
pycparser = "*"
|
||||
@@ -1901,25 +1902,25 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "fastapi"
|
||||
version = "0.116.1"
|
||||
version = "0.117.1"
|
||||
description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "fastapi-0.116.1-py3-none-any.whl", hash = "sha256:c46ac7c312df840f0c9e220f7964bada936781bc4e2e6eb71f1c4d7553786565"},
|
||||
{file = "fastapi-0.116.1.tar.gz", hash = "sha256:ed52cbf946abfd70c5a0dccb24673f0670deeb517a88b3544d03c2a6bf283143"},
|
||||
{file = "fastapi-0.117.1-py3-none-any.whl", hash = "sha256:33c51a0d21cab2b9722d4e56dbb9316f3687155be6b276191790d8da03507552"},
|
||||
{file = "fastapi-0.117.1.tar.gz", hash = "sha256:fb2d42082d22b185f904ca0ecad2e195b851030bd6c5e4c032d1c981240c631a"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0"
|
||||
starlette = ">=0.40.0,<0.48.0"
|
||||
starlette = ">=0.40.0,<0.49.0"
|
||||
typing-extensions = ">=4.8.0"
|
||||
|
||||
[package.extras]
|
||||
all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.8)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=3.1.5)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"]
|
||||
standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.8)", "httpx (>=0.23.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"]
|
||||
standard-no-fastapi-cloud-cli = ["email-validator (>=2.0.0)", "fastapi-cli[standard-no-fastapi-cloud-cli] (>=0.0.8)", "httpx (>=0.23.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"]
|
||||
all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.8)", "httpx (>=0.23.0,<1.0.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=3.1.5)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"]
|
||||
standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.8)", "httpx (>=0.23.0,<1.0.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"]
|
||||
standard-no-fastapi-cloud-cli = ["email-validator (>=2.0.0)", "fastapi-cli[standard-no-fastapi-cloud-cli] (>=0.0.8)", "httpx (>=0.23.0,<1.0.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "fastjsonschema"
|
||||
@@ -2291,6 +2292,72 @@ test-downstream = ["aiobotocore (>=2.5.4,<3.0.0)", "dask[dataframe,test]", "moto
|
||||
test-full = ["adlfs", "aiohttp (!=4.0.0a0,!=4.0.0a1)", "cloudpickle", "dask", "distributed", "dropbox", "dropboxdrivefs", "fastparquet", "fusepy", "gcsfs", "jinja2", "kerchunk", "libarchive-c", "lz4", "notebook", "numpy", "ocifs", "pandas", "panel", "paramiko", "pyarrow", "pyarrow (>=1)", "pyftpdlib", "pygit2", "pytest", "pytest-asyncio (!=0.22.0)", "pytest-benchmark", "pytest-cov", "pytest-mock", "pytest-recording", "pytest-rerunfailures", "python-snappy", "requests", "smbprotocol", "tqdm", "urllib3", "zarr", "zstandard ; python_version < \"3.14\""]
|
||||
tqdm = ["tqdm"]
|
||||
|
||||
[[package]]
|
||||
name = "gevent"
|
||||
version = "25.9.1"
|
||||
description = "Coroutine-based network library"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["test"]
|
||||
files = [
|
||||
{file = "gevent-25.9.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:856b990be5590e44c3a3dc6c8d48a40eaccbb42e99d2b791d11d1e7711a4297e"},
|
||||
{file = "gevent-25.9.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:fe1599d0b30e6093eb3213551751b24feeb43db79f07e89d98dd2f3330c9063e"},
|
||||
{file = "gevent-25.9.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:f0d8b64057b4bf1529b9ef9bd2259495747fba93d1f836c77bfeaacfec373fd0"},
|
||||
{file = "gevent-25.9.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b56cbc820e3136ba52cd690bdf77e47a4c239964d5f80dc657c1068e0fe9521c"},
|
||||
{file = "gevent-25.9.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c5fa9ce5122c085983e33e0dc058f81f5264cebe746de5c401654ab96dddfca8"},
|
||||
{file = "gevent-25.9.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:03c74fec58eda4b4edc043311fca8ba4f8744ad1632eb0a41d5ec25413581975"},
|
||||
{file = "gevent-25.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:a8ae9f895e8651d10b0a8328a61c9c53da11ea51b666388aa99b0ce90f9fdc27"},
|
||||
{file = "gevent-25.9.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:18e5aff9e8342dc954adb9c9c524db56c2f3557999463445ba3d9cbe3dada7b7"},
|
||||
{file = "gevent-25.9.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1cdf6db28f050ee103441caa8b0448ace545364f775059d5e2de089da975c457"},
|
||||
{file = "gevent-25.9.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:812debe235a8295be3b2a63b136c2474241fa5c58af55e6a0f8cfc29d4936235"},
|
||||
{file = "gevent-25.9.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b28b61ff9216a3d73fe8f35669eefcafa957f143ac534faf77e8a19eb9e6883a"},
|
||||
{file = "gevent-25.9.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5e4b6278b37373306fc6b1e5f0f1cf56339a1377f67c35972775143d8d7776ff"},
|
||||
{file = "gevent-25.9.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d99f0cb2ce43c2e8305bf75bee61a8bde06619d21b9d0316ea190fc7a0620a56"},
|
||||
{file = "gevent-25.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:72152517ecf548e2f838c61b4be76637d99279dbaa7e01b3924df040aa996586"},
|
||||
{file = "gevent-25.9.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:46b188248c84ffdec18a686fcac5dbb32365d76912e14fda350db5dc0bfd4f86"},
|
||||
{file = "gevent-25.9.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f2b54ea3ca6f0c763281cd3f96010ac7e98c2e267feb1221b5a26e2ca0b9a692"},
|
||||
{file = "gevent-25.9.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7a834804ac00ed8a92a69d3826342c677be651b1c3cd66cc35df8bc711057aa2"},
|
||||
{file = "gevent-25.9.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:323a27192ec4da6b22a9e51c3d9d896ff20bc53fdc9e45e56eaab76d1c39dd74"},
|
||||
{file = "gevent-25.9.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6ea78b39a2c51d47ff0f130f4c755a9a4bbb2dd9721149420ad4712743911a51"},
|
||||
{file = "gevent-25.9.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:dc45cd3e1cc07514a419960af932a62eb8515552ed004e56755e4bf20bad30c5"},
|
||||
{file = "gevent-25.9.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34e01e50c71eaf67e92c186ee0196a039d6e4f4b35670396baed4a2d8f1b347f"},
|
||||
{file = "gevent-25.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:4acd6bcd5feabf22c7c5174bd3b9535ee9f088d2bbce789f740ad8d6554b18f3"},
|
||||
{file = "gevent-25.9.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:4f84591d13845ee31c13f44bdf6bd6c3dbf385b5af98b2f25ec328213775f2ed"},
|
||||
{file = "gevent-25.9.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9cdbb24c276a2d0110ad5c978e49daf620b153719ac8a548ce1250a7eb1b9245"},
|
||||
{file = "gevent-25.9.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:88b6c07169468af631dcf0fdd3658f9246d6822cc51461d43f7c44f28b0abb82"},
|
||||
{file = "gevent-25.9.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b7bb0e29a7b3e6ca9bed2394aa820244069982c36dc30b70eb1004dd67851a48"},
|
||||
{file = "gevent-25.9.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2951bb070c0ee37b632ac9134e4fdaad70d2e660c931bb792983a0837fe5b7d7"},
|
||||
{file = "gevent-25.9.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e4e17c2d57e9a42e25f2a73d297b22b60b2470a74be5a515b36c984e1a246d47"},
|
||||
{file = "gevent-25.9.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8d94936f8f8b23d9de2251798fcb603b84f083fdf0d7f427183c1828fb64f117"},
|
||||
{file = "gevent-25.9.1-cp313-cp313-win_amd64.whl", hash = "sha256:eb51c5f9537b07da673258b4832f6635014fee31690c3f0944d34741b69f92fa"},
|
||||
{file = "gevent-25.9.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:1a3fe4ea1c312dbf6b375b416925036fe79a40054e6bf6248ee46526ea628be1"},
|
||||
{file = "gevent-25.9.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0adb937f13e5fb90cca2edf66d8d7e99d62a299687400ce2edee3f3504009356"},
|
||||
{file = "gevent-25.9.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:427f869a2050a4202d93cf7fd6ab5cffb06d3e9113c10c967b6e2a0d45237cb8"},
|
||||
{file = "gevent-25.9.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c049880175e8c93124188f9d926af0a62826a3b81aa6d3074928345f8238279e"},
|
||||
{file = "gevent-25.9.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b5a67a0974ad9f24721034d1e008856111e0535f1541499f72a733a73d658d1c"},
|
||||
{file = "gevent-25.9.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1d0f5d8d73f97e24ea8d24d8be0f51e0cf7c54b8021c1fddb580bf239474690f"},
|
||||
{file = "gevent-25.9.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ddd3ff26e5c4240d3fbf5516c2d9d5f2a998ef87cfb73e1429cfaeaaec860fa6"},
|
||||
{file = "gevent-25.9.1-cp314-cp314-win_amd64.whl", hash = "sha256:bb63c0d6cb9950cc94036a4995b9cc4667b8915366613449236970f4394f94d7"},
|
||||
{file = "gevent-25.9.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f18f80aef6b1f6907219affe15b36677904f7cfeed1f6a6bc198616e507ae2d7"},
|
||||
{file = "gevent-25.9.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b274a53e818124a281540ebb4e7a2c524778f745b7a99b01bdecf0ca3ac0ddb0"},
|
||||
{file = "gevent-25.9.1-cp39-cp39-win32.whl", hash = "sha256:c6c91f7e33c7f01237755884316110ee7ea076f5bdb9aa0982b6dc63243c0a38"},
|
||||
{file = "gevent-25.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:012a44b0121f3d7c800740ff80351c897e85e76a7e4764690f35c5ad9ec17de5"},
|
||||
{file = "gevent-25.9.1.tar.gz", hash = "sha256:adf9cd552de44a4e6754c51ff2e78d9193b7fa6eab123db9578a210e657235dd"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
cffi = {version = ">=1.17.1", markers = "platform_python_implementation == \"CPython\" and sys_platform == \"win32\""}
|
||||
greenlet = {version = ">=3.2.2", markers = "platform_python_implementation == \"CPython\""}
|
||||
"zope.event" = "*"
|
||||
"zope.interface" = "*"
|
||||
|
||||
[package.extras]
|
||||
dnspython = ["dnspython (>=1.16.0,<2.0) ; python_version < \"3.10\"", "idna ; python_version < \"3.10\""]
|
||||
docs = ["furo", "repoze.sphinx.autointerface", "sphinx", "sphinxcontrib-programoutput", "zope.schema"]
|
||||
monitor = ["psutil (>=5.7.0) ; sys_platform != \"win32\" or platform_python_implementation == \"CPython\""]
|
||||
recommended = ["cffi (>=1.17.1) ; platform_python_implementation == \"CPython\"", "dnspython (>=1.16.0,<2.0) ; python_version < \"3.10\"", "idna ; python_version < \"3.10\"", "psutil (>=5.7.0) ; sys_platform != \"win32\" or platform_python_implementation == \"CPython\""]
|
||||
test = ["cffi (>=1.17.1) ; platform_python_implementation == \"CPython\"", "coverage (>=5.0) ; sys_platform != \"win32\"", "dnspython (>=1.16.0,<2.0) ; python_version < \"3.10\"", "idna ; python_version < \"3.10\"", "objgraph", "psutil (>=5.7.0) ; sys_platform != \"win32\" or platform_python_implementation == \"CPython\"", "requests"]
|
||||
|
||||
[[package]]
|
||||
name = "gitdb"
|
||||
version = "4.0.12"
|
||||
@@ -2707,7 +2774,7 @@ version = "3.2.4"
|
||||
description = "Lightweight in-process concurrent programming"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
groups = ["main", "test"]
|
||||
files = [
|
||||
{file = "greenlet-3.2.4-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:8c68325b0d0acf8d91dde4e6f930967dd52a5302cd4062932a6b2e7c2969f47c"},
|
||||
{file = "greenlet-3.2.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:94385f101946790ae13da500603491f04a76b6e4c059dab271b3ce2e283b2590"},
|
||||
@@ -2764,6 +2831,7 @@ files = [
|
||||
{file = "greenlet-3.2.4-cp39-cp39-win_amd64.whl", hash = "sha256:d2e685ade4dafd447ede19c31277a224a239a0a1a4eca4e6390efedf20260cfb"},
|
||||
{file = "greenlet-3.2.4.tar.gz", hash = "sha256:0dca0d95ff849f9a364385f36ab49f50065d76964944638be9691e1832e9f86d"},
|
||||
]
|
||||
markers = {test = "platform_python_implementation == \"CPython\""}
|
||||
|
||||
[package.extras]
|
||||
docs = ["Sphinx", "furo"]
|
||||
@@ -5363,7 +5431,7 @@ llama = ["llama-index (>=0.12.29,<0.13.0)", "llama-index-core (>=0.12.29,<0.13.0
|
||||
|
||||
[[package]]
|
||||
name = "openhands-ai"
|
||||
version = "0.55.0"
|
||||
version = "0.57.0"
|
||||
description = "OpenHands: Code Less, Make More"
|
||||
optional = false
|
||||
python-versions = "^3.12,<3.14"
|
||||
@@ -5397,7 +5465,7 @@ json-repair = "*"
|
||||
jupyter_kernel_gateway = "*"
|
||||
kubernetes = "^33.1.0"
|
||||
libtmux = ">=0.37,<0.40"
|
||||
litellm = "^1.74.3, !=1.64.4, !=1.67.*"
|
||||
litellm = ">=1.74.3, <1.77.2, !=1.64.4, !=1.67.*"
|
||||
memory-profiler = "^0.61.0"
|
||||
numpy = "*"
|
||||
openai = "1.99.9"
|
||||
@@ -5406,6 +5474,7 @@ opentelemetry-api = "^1.33.1"
|
||||
opentelemetry-exporter-otlp-proto-grpc = "^1.33.1"
|
||||
pathspec = "^0.12.1"
|
||||
pexpect = "*"
|
||||
pillow = "^11.3.0"
|
||||
poetry = "^2.1.2"
|
||||
prompt-toolkit = "^3.0.50"
|
||||
protobuf = "^5.0.0,<6.0.0"
|
||||
@@ -5413,6 +5482,7 @@ psutil = "*"
|
||||
pygithub = "^2.5.0"
|
||||
pyjwt = "^2.9.0"
|
||||
pylatexenc = "*"
|
||||
pypdf = "^6.0.0"
|
||||
PyPDF2 = "*"
|
||||
python-docx = "*"
|
||||
python-dotenv = "*"
|
||||
@@ -5426,13 +5496,17 @@ pyyaml = "^6.0.2"
|
||||
qtconsole = "^5.6.1"
|
||||
rapidfuzz = "^3.9.0"
|
||||
redis = ">=5.2,<7.0"
|
||||
requests = "^2.32.5"
|
||||
setuptools = ">=78.1.1"
|
||||
shellingham = "^1.5.4"
|
||||
sse-starlette = "^2.1.3"
|
||||
sse-starlette = "^3.0.2"
|
||||
starlette = "^0.48.0"
|
||||
tenacity = ">=8.5,<10.0"
|
||||
termcolor = "*"
|
||||
toml = "*"
|
||||
tornado = "*"
|
||||
types-toml = "*"
|
||||
urllib3 = "^2.5.0"
|
||||
uvicorn = "*"
|
||||
whatthepatch = "^1.0.6"
|
||||
zope-interface = "7.2"
|
||||
@@ -6471,11 +6545,12 @@ version = "2.22"
|
||||
description = "C parser in Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
groups = ["main", "test"]
|
||||
files = [
|
||||
{file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"},
|
||||
{file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"},
|
||||
]
|
||||
markers = {test = "platform_python_implementation == \"CPython\" and sys_platform == \"win32\""}
|
||||
|
||||
[[package]]
|
||||
name = "pydantic"
|
||||
@@ -8265,7 +8340,7 @@ version = "80.9.0"
|
||||
description = "Easily download, build, install, upgrade, and uninstall Python packages"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
groups = ["main", "test"]
|
||||
files = [
|
||||
{file = "setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922"},
|
||||
{file = "setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c"},
|
||||
@@ -8631,14 +8706,14 @@ sqlcipher = ["sqlcipher3_binary"]
|
||||
|
||||
[[package]]
|
||||
name = "sse-starlette"
|
||||
version = "2.4.1"
|
||||
version = "3.0.2"
|
||||
description = "SSE plugin for Starlette"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "sse_starlette-2.4.1-py3-none-any.whl", hash = "sha256:08b77ea898ab1a13a428b2b6f73cfe6d0e607a7b4e15b9bb23e4a37b087fd39a"},
|
||||
{file = "sse_starlette-2.4.1.tar.gz", hash = "sha256:7c8a800a1ca343e9165fc06bbda45c78e4c6166320707ae30b416c42da070926"},
|
||||
{file = "sse_starlette-3.0.2-py3-none-any.whl", hash = "sha256:16b7cbfddbcd4eaca11f7b586f3b8a080f1afe952c15813455b162edea619e5a"},
|
||||
{file = "sse_starlette-3.0.2.tar.gz", hash = "sha256:ccd60b5765ebb3584d0de2d7a6e4f745672581de4f5005ab31c3a25d10b52b3a"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -8646,7 +8721,7 @@ anyio = ">=4.7.0"
|
||||
|
||||
[package.extras]
|
||||
daphne = ["daphne (>=4.2.0)"]
|
||||
examples = ["aiosqlite (>=0.21.0)", "fastapi (>=0.115.12)", "sqlalchemy[asyncio,examples] (>=2.0.41)", "starlette (>=0.41.3)", "uvicorn (>=0.34.0)"]
|
||||
examples = ["aiosqlite (>=0.21.0)", "fastapi (>=0.115.12)", "sqlalchemy[asyncio] (>=2.0.41)", "starlette (>=0.41.3)", "uvicorn (>=0.34.0)"]
|
||||
granian = ["granian (>=2.3.1)"]
|
||||
uvicorn = ["uvicorn (>=0.34.0)"]
|
||||
|
||||
@@ -8702,14 +8777,14 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "starlette"
|
||||
version = "0.47.3"
|
||||
version = "0.48.0"
|
||||
description = "The little ASGI library that shines."
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "starlette-0.47.3-py3-none-any.whl", hash = "sha256:89c0778ca62a76b826101e7c709e70680a1699ca7da6b44d38eb0a7e61fe4b51"},
|
||||
{file = "starlette-0.47.3.tar.gz", hash = "sha256:6bc94f839cc176c4858894f1f8908f0ab79dfec1a6b8402f6da9be26ebea52e9"},
|
||||
{file = "starlette-0.48.0-py3-none-any.whl", hash = "sha256:0764ca97b097582558ecb498132ed0c7d942f233f365b86ba37770e026510659"},
|
||||
{file = "starlette-0.48.0.tar.gz", hash = "sha256:7e8cee469a8ab2352911528110ce9088fdc6a37d9876926e73da7ce4aa4c7a46"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -9838,13 +9913,32 @@ enabler = ["pytest-enabler (>=2.2)"]
|
||||
test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more_itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"]
|
||||
type = ["pytest-mypy"]
|
||||
|
||||
[[package]]
|
||||
name = "zope-event"
|
||||
version = "6.0"
|
||||
description = "Very basic event publishing system"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["test"]
|
||||
files = [
|
||||
{file = "zope_event-6.0-py3-none-any.whl", hash = "sha256:6f0922593407cc673e7d8766b492c519f91bdc99f3080fe43dcec0a800d682a3"},
|
||||
{file = "zope_event-6.0.tar.gz", hash = "sha256:0ebac894fa7c5f8b7a89141c272133d8c1de6ddc75ea4b1f327f00d1f890df92"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
setuptools = ">=75.8.2"
|
||||
|
||||
[package.extras]
|
||||
docs = ["Sphinx"]
|
||||
test = ["zope.testrunner (>=6.4)"]
|
||||
|
||||
[[package]]
|
||||
name = "zope-interface"
|
||||
version = "7.2"
|
||||
description = "Interfaces for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
groups = ["main", "test"]
|
||||
files = [
|
||||
{file = "zope.interface-7.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ce290e62229964715f1011c3dbeab7a4a1e4971fd6f31324c4519464473ef9f2"},
|
||||
{file = "zope.interface-7.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:05b910a5afe03256b58ab2ba6288960a2892dfeef01336dc4be6f1b9ed02ab0a"},
|
||||
@@ -10008,4 +10102,4 @@ cffi = ["cffi (>=1.17) ; python_version >= \"3.13\" and platform_python_implemen
|
||||
[metadata]
|
||||
lock-version = "2.1"
|
||||
python-versions = "^3.12,<3.14"
|
||||
content-hash = "5771671ef2acc36e7b0931c73fa035ca1d329e8dac6827f7a349e1a569c3fd23"
|
||||
content-hash = "8c460070dce6bdec5ee0ee7bc0c2246fcf2602d1e64a0867b4f5e3a0e334fe93"
|
||||
|
||||
@@ -63,6 +63,7 @@ openai = "*"
|
||||
opencv-python = "*"
|
||||
pandas = "*"
|
||||
reportlab = "*"
|
||||
gevent = ">=24.2.1,<26.0.0"
|
||||
|
||||
[tool.poetry-dynamic-versioning]
|
||||
enable = true
|
||||
@@ -85,3 +86,7 @@ lint.pydocstyle.convention = "google"
|
||||
[tool.pytest.ini_options]
|
||||
asyncio_mode = "auto"
|
||||
asyncio_default_fixture_loop_scope = "function"
|
||||
|
||||
[tool.coverage.run]
|
||||
relative_files = true
|
||||
omit = [ "tests/*" ]
|
||||
|
||||
@@ -21,6 +21,7 @@ from openhands.events.event_store_abc import EventStoreABC
|
||||
from openhands.events.observation import AgentStateChangedObservation
|
||||
from openhands.events.stream import EventStreamSubscriber
|
||||
from openhands.llm.llm_registry import LLMRegistry
|
||||
from openhands.runtime.runtime_status import RuntimeStatus
|
||||
from openhands.server.config.server_config import ServerConfig
|
||||
from openhands.server.conversation_manager.conversation_manager import (
|
||||
ConversationManager,
|
||||
@@ -686,6 +687,7 @@ class ClusteredConversationManager(StandaloneConversationManager):
|
||||
url=self._get_conversation_url(conversation_id),
|
||||
session_api_key=None,
|
||||
event_store=EventStore(conversation_id, self.file_store, uid),
|
||||
runtime_status=RuntimeStatus.READY,
|
||||
)
|
||||
)
|
||||
return results
|
||||
|
||||
@@ -149,6 +149,13 @@ async def on_batch_write(
|
||||
background_tasks: BackgroundTasks,
|
||||
x_session_api_key: Annotated[str | None, Header()],
|
||||
):
|
||||
logger.info(
|
||||
'batch_write_webhook',
|
||||
extra={
|
||||
'batch_ops': batch_ops,
|
||||
'x_session_api_key': x_session_api_key,
|
||||
},
|
||||
)
|
||||
"""Handle batched webhook requests with multiple file operations in background"""
|
||||
# Add the batch processing to background tasks
|
||||
background_tasks.add_task(
|
||||
|
||||
@@ -138,6 +138,7 @@ async def saas_search_repositories(
|
||||
per_page: int = 5,
|
||||
sort: str = 'stars',
|
||||
order: str = 'desc',
|
||||
selected_provider: ProviderType | None = None,
|
||||
provider_tokens: PROVIDER_TOKEN_TYPE | None = Depends(get_provider_tokens),
|
||||
access_token: SecretStr | None = Depends(get_access_token),
|
||||
user_id: str | None = Depends(get_user_id),
|
||||
@@ -155,6 +156,7 @@ async def saas_search_repositories(
|
||||
per_page=per_page,
|
||||
sort=sort,
|
||||
order=order,
|
||||
selected_provider=selected_provider,
|
||||
provider_tokens=provider_tokens,
|
||||
access_token=access_token,
|
||||
user_id=user_id,
|
||||
|
||||
@@ -60,9 +60,14 @@ from openhands.utils.utils import create_registry_and_conversation_stats
|
||||
RUNTIME_URL_PATTERN = os.getenv(
|
||||
'RUNTIME_URL_PATTERN', 'https://{runtime_id}.prod-runtime.all-hands.dev'
|
||||
)
|
||||
RUNTIME_ROUTING_MODE = os.getenv('RUNTIME_ROUTING_MODE', 'subdomain').lower()
|
||||
|
||||
# Pattern for base URL for the runtime
|
||||
RUNTIME_CONVERSATION_URL = RUNTIME_URL_PATTERN + '/api/conversations/{conversation_id}'
|
||||
RUNTIME_CONVERSATION_URL = RUNTIME_URL_PATTERN + (
|
||||
'/runtime/api/conversations/{conversation_id}'
|
||||
if RUNTIME_ROUTING_MODE == 'path'
|
||||
else '/api/conversations/{conversation_id}'
|
||||
)
|
||||
|
||||
# Time in seconds before a Redis entry is considered expired if not refreshed
|
||||
_REDIS_ENTRY_TIMEOUT_SECONDS = 300
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import asyncio
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
from google.cloud.sql.connector import Connector
|
||||
from sqlalchemy import create_engine, event
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from sqlalchemy.pool import NullPool
|
||||
@@ -14,9 +13,6 @@ DB_PORT = os.environ.get('DB_PORT', '5432') # for non-GCP environments
|
||||
DB_USER = os.environ.get('DB_USER', 'postgres')
|
||||
DB_PASS = os.environ.get('DB_PASS', 'postgres').strip()
|
||||
DB_NAME = os.environ.get('DB_NAME', 'openhands')
|
||||
DB_SCHEMA = os.environ.get('DB_SCHEMA') # PostgreSQL schema name
|
||||
DB_AUTH_TYPE = os.environ.get('DB_AUTH_TYPE', 'password') # 'password' or 'rds-iam'
|
||||
AWS_REGION = os.environ.get('AWS_REGION', 'us-east-1') # AWS region for RDS IAM auth
|
||||
|
||||
GCP_DB_INSTANCE = os.environ.get('GCP_DB_INSTANCE') # for GCP environments
|
||||
GCP_PROJECT = os.environ.get('GCP_PROJECT')
|
||||
@@ -25,19 +21,6 @@ GCP_REGION = os.environ.get('GCP_REGION')
|
||||
POOL_SIZE = int(os.environ.get('DB_POOL_SIZE', '25'))
|
||||
MAX_OVERFLOW = int(os.environ.get('DB_MAX_OVERFLOW', '10'))
|
||||
|
||||
# RDS IAM authentication setup
|
||||
if DB_AUTH_TYPE == 'rds-iam':
|
||||
import boto3
|
||||
|
||||
# boto3 client (reused for token generation)
|
||||
rds = boto3.client('rds', region_name=AWS_REGION)
|
||||
|
||||
def get_auth_token():
|
||||
"""Generate a fresh IAM DB auth token."""
|
||||
return rds.generate_db_auth_token(
|
||||
DBHostname=DB_HOST, Port=DB_PORT, DBUsername=DB_USER
|
||||
)
|
||||
|
||||
|
||||
def _get_db_engine():
|
||||
if GCP_DB_INSTANCE: # GCP environments
|
||||
@@ -45,104 +28,38 @@ def _get_db_engine():
|
||||
def get_db_connection():
|
||||
connector = Connector()
|
||||
instance_string = f'{GCP_PROJECT}:{GCP_REGION}:{GCP_DB_INSTANCE}'
|
||||
connect_kwargs = {
|
||||
'user': DB_USER,
|
||||
'password': DB_PASS,
|
||||
'db': DB_NAME,
|
||||
}
|
||||
# Note: pg8000 doesn't accept 'options' parameter, so we'll handle schema via SQL
|
||||
# Schema will be set after connection via event listener
|
||||
return connector.connect(instance_string, 'pg8000', **connect_kwargs)
|
||||
return connector.connect(
|
||||
instance_string, 'pg8000', user=DB_USER, password=DB_PASS, db=DB_NAME
|
||||
)
|
||||
|
||||
engine = create_engine(
|
||||
return create_engine(
|
||||
'postgresql+pg8000://',
|
||||
creator=get_db_connection,
|
||||
pool_size=POOL_SIZE,
|
||||
max_overflow=MAX_OVERFLOW,
|
||||
pool_pre_ping=True,
|
||||
)
|
||||
|
||||
# Set schema via SQL after connection if specified
|
||||
if DB_SCHEMA:
|
||||
@event.listens_for(engine, 'connect')
|
||||
def set_search_path(dbapi_connection, connection_record):
|
||||
with dbapi_connection.cursor() as cursor:
|
||||
cursor.execute(f"SET search_path TO {DB_SCHEMA}")
|
||||
dbapi_connection.commit()
|
||||
|
||||
return engine
|
||||
else:
|
||||
if DB_AUTH_TYPE == 'rds-iam':
|
||||
# Build a SQLAlchemy connection URL with a dummy password — token will be injected dynamically
|
||||
# Note: SSL is enabled by default for pg8000 when connecting to RDS
|
||||
# For pg8000, we cannot use URL parameters like options, so schema must be handled differently
|
||||
base_url = (
|
||||
f'postgresql+pg8000://{DB_USER}:dummy-password'
|
||||
f'@{DB_HOST}:{DB_PORT}/{DB_NAME}'
|
||||
)
|
||||
engine = create_engine(
|
||||
base_url,
|
||||
pool_size=POOL_SIZE,
|
||||
max_overflow=MAX_OVERFLOW,
|
||||
pool_pre_ping=True,
|
||||
)
|
||||
|
||||
# Hook: before a connection is made, inject a fresh token
|
||||
@event.listens_for(engine, 'do_connect')
|
||||
def provide_token(dialect, conn_rec, cargs, cparams):
|
||||
token = get_auth_token()
|
||||
# Replace password in connect arguments
|
||||
cparams['password'] = token
|
||||
return dialect.connect(*cargs, **cparams)
|
||||
|
||||
# Hook: after connection is established, set the schema if specified
|
||||
if DB_SCHEMA:
|
||||
@event.listens_for(engine, 'connect')
|
||||
def set_search_path(dbapi_connection, connection_record):
|
||||
with dbapi_connection.cursor() as cursor:
|
||||
cursor.execute(f"SET search_path TO {DB_SCHEMA}")
|
||||
dbapi_connection.commit()
|
||||
|
||||
return engine
|
||||
else:
|
||||
# Regular password authentication with pg8000
|
||||
# pg8000 doesn't accept options as URL parameter, so handle schema via SQL
|
||||
host_string = (
|
||||
f'postgresql+pg8000://{DB_USER}:{DB_PASS}@{DB_HOST}:{DB_PORT}/{DB_NAME}'
|
||||
)
|
||||
engine = create_engine(
|
||||
host_string,
|
||||
pool_size=POOL_SIZE,
|
||||
max_overflow=MAX_OVERFLOW,
|
||||
pool_pre_ping=True,
|
||||
)
|
||||
|
||||
# Set schema via SQL after connection if specified
|
||||
if DB_SCHEMA:
|
||||
@event.listens_for(engine, 'connect')
|
||||
def set_search_path(dbapi_connection, connection_record):
|
||||
with dbapi_connection.cursor() as cursor:
|
||||
cursor.execute(f"SET search_path TO {DB_SCHEMA}")
|
||||
dbapi_connection.commit()
|
||||
|
||||
return engine
|
||||
host_string = (
|
||||
f'postgresql+pg8000://{DB_USER}:{DB_PASS}@{DB_HOST}:{DB_PORT}/{DB_NAME}'
|
||||
)
|
||||
return create_engine(
|
||||
host_string,
|
||||
pool_size=POOL_SIZE,
|
||||
max_overflow=MAX_OVERFLOW,
|
||||
pool_pre_ping=True,
|
||||
)
|
||||
|
||||
|
||||
async def async_creator():
|
||||
loop = asyncio.get_running_loop()
|
||||
async with Connector(loop=loop) as connector:
|
||||
connect_kwargs: dict[str, Any] = {
|
||||
'user': DB_USER,
|
||||
'password': DB_PASS,
|
||||
'db': DB_NAME,
|
||||
}
|
||||
# Add schema support for async GCP connections
|
||||
if DB_SCHEMA:
|
||||
connect_kwargs['server_settings'] = {'search_path': DB_SCHEMA}
|
||||
conn = await connector.connect_async(
|
||||
f'{GCP_PROJECT}:{GCP_REGION}:{GCP_DB_INSTANCE}', # Cloud SQL instance connection name"
|
||||
'asyncpg',
|
||||
**connect_kwargs,
|
||||
user=DB_USER,
|
||||
password=DB_PASS,
|
||||
db=DB_NAME,
|
||||
)
|
||||
return conn
|
||||
|
||||
@@ -170,49 +87,14 @@ def _get_async_db_engine():
|
||||
poolclass=NullPool,
|
||||
)
|
||||
else:
|
||||
if DB_AUTH_TYPE == 'rds-iam':
|
||||
# Build a SQLAlchemy connection URL with a dummy password — token will be injected dynamically
|
||||
# Note: SSL is enabled by default for asyncpg when connecting to RDS
|
||||
# For asyncpg, we cannot use URL parameters like options, so schema must be handled differently
|
||||
base_url = (
|
||||
f'postgresql+asyncpg://{DB_USER}:dummy-password'
|
||||
f'@{DB_HOST}:{DB_PORT}/{DB_NAME}'
|
||||
)
|
||||
engine = create_async_engine(
|
||||
base_url, echo=True, pool_pre_ping=True, poolclass=NullPool
|
||||
)
|
||||
|
||||
# Hook: before a connection is made, inject a fresh token and set schema
|
||||
@event.listens_for(engine.sync_engine, 'do_connect')
|
||||
def provide_token(dialect, conn_rec, cargs, cparams):
|
||||
token = get_auth_token()
|
||||
# Replace password in connect arguments
|
||||
cparams['password'] = token
|
||||
# Set schema via server_settings for asyncpg
|
||||
if DB_SCHEMA:
|
||||
cparams['server_settings'] = {'search_path': DB_SCHEMA}
|
||||
return dialect.connect(*cargs, **cparams)
|
||||
|
||||
return engine
|
||||
else:
|
||||
# Regular password authentication with asyncpg
|
||||
# asyncpg doesn't accept options as URL parameter, so handle schema via server_settings
|
||||
host_string = f'postgresql+asyncpg://{DB_USER}:{DB_PASS}@{DB_HOST}:{DB_PORT}/{DB_NAME}'
|
||||
engine = create_async_engine(
|
||||
host_string,
|
||||
# Use NullPool to disable connection pooling and avoid event loop issues
|
||||
poolclass=NullPool,
|
||||
)
|
||||
|
||||
# Set schema via server_settings for asyncpg
|
||||
if DB_SCHEMA:
|
||||
@event.listens_for(engine.sync_engine, 'do_connect')
|
||||
def set_schema(dialect, conn_rec, cargs, cparams):
|
||||
# Set schema via server_settings for asyncpg
|
||||
cparams['server_settings'] = {'search_path': DB_SCHEMA}
|
||||
return dialect.connect(*cargs, **cparams)
|
||||
|
||||
return engine
|
||||
host_string = (
|
||||
f'postgresql+asyncpg://{DB_USER}:{DB_PASS}@{DB_HOST}:{DB_PORT}/{DB_NAME}'
|
||||
)
|
||||
return create_async_engine(
|
||||
host_string,
|
||||
# Use NullPool to disable connection pooling and avoid event loop issues
|
||||
poolclass=NullPool,
|
||||
)
|
||||
|
||||
|
||||
engine = _get_db_engine()
|
||||
|
||||
@@ -13,7 +13,8 @@ vi.mock("react-router", async () => {
|
||||
|
||||
vi.mock("#/context/conversation-context", () => ({
|
||||
useConversation: () => ({ conversationId: "test-conversation-id" }),
|
||||
ConversationProvider: ({ children }: { children: React.ReactNode }) => children,
|
||||
ConversationProvider: ({ children }: { children: React.ReactNode }) =>
|
||||
children,
|
||||
}));
|
||||
|
||||
vi.mock("react-i18next", async () => {
|
||||
@@ -29,21 +30,18 @@ vi.mock("react-i18next", async () => {
|
||||
};
|
||||
});
|
||||
|
||||
// Mock redux
|
||||
const mockDispatch = vi.fn();
|
||||
// Mock Zustand browser store
|
||||
let mockBrowserState = {
|
||||
url: "https://example.com",
|
||||
screenshotSrc: "",
|
||||
setUrl: vi.fn(),
|
||||
setScreenshotSrc: vi.fn(),
|
||||
reset: vi.fn(),
|
||||
};
|
||||
|
||||
vi.mock("react-redux", async () => {
|
||||
const actual = await vi.importActual("react-redux");
|
||||
return {
|
||||
...actual,
|
||||
useDispatch: () => mockDispatch,
|
||||
useSelector: () => mockBrowserState,
|
||||
};
|
||||
});
|
||||
vi.mock("#/stores/browser-store", () => ({
|
||||
useBrowserStore: () => mockBrowserState,
|
||||
}));
|
||||
|
||||
// Import the component after all mocks are set up
|
||||
import { BrowserPanel } from "#/components/features/browser/browser";
|
||||
@@ -55,6 +53,9 @@ describe("Browser", () => {
|
||||
mockBrowserState = {
|
||||
url: "https://example.com",
|
||||
screenshotSrc: "",
|
||||
setUrl: vi.fn(),
|
||||
setScreenshotSrc: vi.fn(),
|
||||
reset: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -63,6 +64,9 @@ describe("Browser", () => {
|
||||
mockBrowserState = {
|
||||
url: "https://example.com",
|
||||
screenshotSrc: "",
|
||||
setUrl: vi.fn(),
|
||||
setScreenshotSrc: vi.fn(),
|
||||
reset: vi.fn(),
|
||||
};
|
||||
|
||||
render(<BrowserPanel />);
|
||||
@@ -75,7 +79,11 @@ describe("Browser", () => {
|
||||
// Set the mock state for this test
|
||||
mockBrowserState = {
|
||||
url: "https://example.com",
|
||||
screenshotSrc: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mN0uGvyHwAFCAJS091fQwAAAABJRU5ErkJggg==",
|
||||
screenshotSrc:
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mN0uGvyHwAFCAJS091fQwAAAABJRU5ErkJggg==",
|
||||
setUrl: vi.fn(),
|
||||
setScreenshotSrc: vi.fn(),
|
||||
reset: vi.fn(),
|
||||
};
|
||||
|
||||
render(<BrowserPanel />);
|
||||
|
||||
@@ -17,8 +17,8 @@ import type { Message } from "#/message";
|
||||
import { SUGGESTIONS } from "#/utils/suggestions";
|
||||
import { ChatInterface } from "#/components/features/chat/chat-interface";
|
||||
import { useWsClient } from "#/context/ws-client-provider";
|
||||
import { useOptimisticUserMessage } from "#/hooks/use-optimistic-user-message";
|
||||
import { useWSErrorMessage } from "#/hooks/use-ws-error-message";
|
||||
import { useErrorMessageStore } from "#/stores/error-message-store";
|
||||
import { useOptimisticUserMessageStore } from "#/stores/optimistic-user-message-store";
|
||||
import { useConfig } from "#/hooks/query/use-config";
|
||||
import { useGetTrajectory } from "#/hooks/mutation/use-get-trajectory";
|
||||
import { useUploadFiles } from "#/hooks/mutation/use-upload-files";
|
||||
@@ -26,8 +26,8 @@ import { OpenHandsAction } from "#/types/core/actions";
|
||||
|
||||
// Mock the hooks
|
||||
vi.mock("#/context/ws-client-provider");
|
||||
vi.mock("#/hooks/use-optimistic-user-message");
|
||||
vi.mock("#/hooks/use-ws-error-message");
|
||||
vi.mock("#/stores/error-message-store");
|
||||
vi.mock("#/stores/optimistic-user-message-store");
|
||||
vi.mock("#/hooks/query/use-config");
|
||||
vi.mock("#/hooks/mutation/use-get-trajectory");
|
||||
vi.mock("#/hooks/mutation/use-upload-files");
|
||||
@@ -61,39 +61,6 @@ vi.mock("#/hooks/use-conversation-name-context-menu", () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("react-redux", async () => {
|
||||
const actual = await vi.importActual("react-redux");
|
||||
return {
|
||||
...actual,
|
||||
useSelector: vi.fn((selector) => {
|
||||
// Create a mock state object
|
||||
const mockState = {
|
||||
agent: {
|
||||
curAgentState: "AWAITING_USER_INPUT",
|
||||
},
|
||||
initialQuery: {
|
||||
selectedRepository: null,
|
||||
replayJson: null,
|
||||
},
|
||||
conversation: {
|
||||
messageToSend: null,
|
||||
files: [],
|
||||
images: [],
|
||||
loadingFiles: [],
|
||||
loadingImages: [],
|
||||
},
|
||||
status: {
|
||||
curStatusMessage: null,
|
||||
},
|
||||
};
|
||||
|
||||
// Execute the selector function with our mock state
|
||||
return selector(mockState);
|
||||
}),
|
||||
useDispatch: vi.fn(() => vi.fn()),
|
||||
};
|
||||
});
|
||||
|
||||
// Helper function to render with Router context
|
||||
const renderChatInterfaceWithRouter = () =>
|
||||
renderWithProviders(
|
||||
@@ -141,13 +108,14 @@ describe("ChatInterface - Chat Suggestions", () => {
|
||||
parsedEvents: [],
|
||||
});
|
||||
(
|
||||
useOptimisticUserMessage as unknown as ReturnType<typeof vi.fn>
|
||||
useOptimisticUserMessageStore as unknown as ReturnType<typeof vi.fn>
|
||||
).mockReturnValue({
|
||||
setOptimisticUserMessage: vi.fn(),
|
||||
getOptimisticUserMessage: vi.fn(() => null),
|
||||
});
|
||||
(useWSErrorMessage as unknown as ReturnType<typeof vi.fn>).mockReturnValue({
|
||||
getErrorMessage: vi.fn(() => null),
|
||||
(
|
||||
useErrorMessageStore as unknown as ReturnType<typeof vi.fn>
|
||||
).mockReturnValue({
|
||||
setErrorMessage: vi.fn(),
|
||||
removeErrorMessage: vi.fn(),
|
||||
});
|
||||
@@ -235,7 +203,7 @@ describe("ChatInterface - Chat Suggestions", () => {
|
||||
|
||||
test("should hide chat suggestions when there is an optimistic user message", () => {
|
||||
(
|
||||
useOptimisticUserMessage as unknown as ReturnType<typeof vi.fn>
|
||||
useOptimisticUserMessageStore as unknown as ReturnType<typeof vi.fn>
|
||||
).mockReturnValue({
|
||||
setOptimisticUserMessage: vi.fn(),
|
||||
getOptimisticUserMessage: vi.fn(() => "Optimistic message"),
|
||||
@@ -278,13 +246,14 @@ describe("ChatInterface - Empty state", () => {
|
||||
parsedEvents: [],
|
||||
});
|
||||
(
|
||||
useOptimisticUserMessage as unknown as ReturnType<typeof vi.fn>
|
||||
useOptimisticUserMessageStore as unknown as ReturnType<typeof vi.fn>
|
||||
).mockReturnValue({
|
||||
setOptimisticUserMessage: vi.fn(),
|
||||
getOptimisticUserMessage: vi.fn(() => null),
|
||||
});
|
||||
(useWSErrorMessage as unknown as ReturnType<typeof vi.fn>).mockReturnValue({
|
||||
getErrorMessage: vi.fn(() => null),
|
||||
(
|
||||
useErrorMessageStore as unknown as ReturnType<typeof vi.fn>
|
||||
).mockReturnValue({
|
||||
setErrorMessage: vi.fn(),
|
||||
removeErrorMessage: vi.fn(),
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@ import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { createRoutesStub } from "react-router";
|
||||
import React from "react";
|
||||
import { renderWithQueryAndI18n } from "test-utils";
|
||||
import { renderWithProviders } from "test-utils";
|
||||
import { ConversationPanel } from "#/components/features/conversation-panel/conversation-panel";
|
||||
import ConversationService from "#/api/conversation-service/conversation-service.api";
|
||||
import { Conversation } from "#/api/open-hands.types";
|
||||
@@ -17,7 +17,7 @@ describe("ConversationPanel", () => {
|
||||
},
|
||||
]);
|
||||
|
||||
const renderConversationPanel = () => renderWithQueryAndI18n(<RouterStub />);
|
||||
const renderConversationPanel = () => renderWithProviders(<RouterStub />);
|
||||
|
||||
beforeAll(() => {
|
||||
vi.mock("react-router", async (importOriginal) => ({
|
||||
@@ -287,7 +287,7 @@ describe("ConversationPanel", () => {
|
||||
},
|
||||
]);
|
||||
|
||||
renderWithQueryAndI18n(<MyRouterStub />);
|
||||
renderWithProviders(<MyRouterStub />);
|
||||
|
||||
const toggleButton = screen.getByText("Toggle");
|
||||
|
||||
|
||||
@@ -6,31 +6,11 @@ import { ServerStatus } from "#/components/features/controls/server-status";
|
||||
import { ServerStatusContextMenu } from "#/components/features/controls/server-status-context-menu";
|
||||
import { ConversationStatus } from "#/types/conversation-status";
|
||||
import { AgentState } from "#/types/agent-state";
|
||||
import { useAgentStore } from "#/stores/agent-store";
|
||||
|
||||
// Mock the conversation slice actions
|
||||
vi.mock("#/state/conversation-slice", () => ({
|
||||
setShouldStopConversation: vi.fn(),
|
||||
setShouldStartConversation: vi.fn(),
|
||||
default: {
|
||||
name: "conversation",
|
||||
initialState: {
|
||||
isRightPanelShown: true,
|
||||
shouldStopConversation: false,
|
||||
shouldStartConversation: false,
|
||||
},
|
||||
reducers: {},
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock react-redux
|
||||
vi.mock("react-redux", () => ({
|
||||
useSelector: vi.fn((selector) => {
|
||||
// Mock the selector to return different agent states based on test needs
|
||||
return {
|
||||
curAgentState: AgentState.RUNNING,
|
||||
};
|
||||
}),
|
||||
Provider: ({ children }: { children: React.ReactNode }) => children,
|
||||
// Mock the agent store
|
||||
vi.mock("#/stores/agent-store", () => ({
|
||||
useAgentStore: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock the custom hooks
|
||||
@@ -86,11 +66,23 @@ vi.mock("react-i18next", async () => {
|
||||
});
|
||||
|
||||
describe("ServerStatus", () => {
|
||||
// Helper function to mock agent store with specific state
|
||||
const mockAgentStore = (agentState: AgentState) => {
|
||||
vi.mocked(useAgentStore).mockReturnValue({
|
||||
curAgentState: agentState,
|
||||
setCurrentAgentState: vi.fn(),
|
||||
reset: vi.fn(),
|
||||
});
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should render server status with different conversation statuses", () => {
|
||||
// Mock agent store to return RUNNING state
|
||||
mockAgentStore(AgentState.RUNNING);
|
||||
|
||||
// Test RUNNING status
|
||||
const { rerender } = renderWithProviders(
|
||||
<ServerStatus conversationStatus="RUNNING" />,
|
||||
@@ -112,6 +104,10 @@ describe("ServerStatus", () => {
|
||||
|
||||
it("should show context menu when clicked with RUNNING status", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
// Mock agent store to return RUNNING state
|
||||
mockAgentStore(AgentState.RUNNING);
|
||||
|
||||
renderWithProviders(<ServerStatus conversationStatus="RUNNING" />);
|
||||
|
||||
const statusContainer = screen.getByText("Running").closest("div");
|
||||
@@ -128,6 +124,10 @@ describe("ServerStatus", () => {
|
||||
|
||||
it("should show context menu when clicked with STOPPED status", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
// Mock agent store to return STOPPED state
|
||||
mockAgentStore(AgentState.STOPPED);
|
||||
|
||||
renderWithProviders(<ServerStatus conversationStatus="STOPPED" />);
|
||||
|
||||
const statusContainer = screen.getByText("Server Stopped").closest("div");
|
||||
@@ -144,6 +144,10 @@ describe("ServerStatus", () => {
|
||||
|
||||
it("should not show context menu when clicked with other statuses", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
// Mock agent store to return RUNNING state
|
||||
mockAgentStore(AgentState.RUNNING);
|
||||
|
||||
renderWithProviders(<ServerStatus conversationStatus="STARTING" />);
|
||||
|
||||
const statusContainer = screen.getByText("Running").closest("div");
|
||||
@@ -163,6 +167,9 @@ describe("ServerStatus", () => {
|
||||
// Clear previous calls
|
||||
mockStopConversationMutate.mockClear();
|
||||
|
||||
// Mock agent store to return RUNNING state
|
||||
mockAgentStore(AgentState.RUNNING);
|
||||
|
||||
renderWithProviders(<ServerStatus conversationStatus="RUNNING" />);
|
||||
|
||||
const statusContainer = screen.getByText("Running").closest("div");
|
||||
@@ -182,6 +189,9 @@ describe("ServerStatus", () => {
|
||||
// Clear previous calls
|
||||
mockStartConversationMutate.mockClear();
|
||||
|
||||
// Mock agent store to return STOPPED state
|
||||
mockAgentStore(AgentState.STOPPED);
|
||||
|
||||
renderWithProviders(<ServerStatus conversationStatus="STOPPED" />);
|
||||
|
||||
const statusContainer = screen.getByText("Server Stopped").closest("div");
|
||||
@@ -198,6 +208,10 @@ describe("ServerStatus", () => {
|
||||
|
||||
it("should close context menu after stop server action", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
// Mock agent store to return RUNNING state
|
||||
mockAgentStore(AgentState.RUNNING);
|
||||
|
||||
renderWithProviders(<ServerStatus conversationStatus="RUNNING" />);
|
||||
|
||||
const statusContainer = screen.getByText("Running").closest("div");
|
||||
@@ -214,6 +228,10 @@ describe("ServerStatus", () => {
|
||||
|
||||
it("should close context menu after start server action", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
// Mock agent store to return STOPPED state
|
||||
mockAgentStore(AgentState.STOPPED);
|
||||
|
||||
renderWithProviders(<ServerStatus conversationStatus="STOPPED" />);
|
||||
|
||||
const statusContainer = screen.getByText("Server Stopped").closest("div");
|
||||
@@ -229,6 +247,9 @@ describe("ServerStatus", () => {
|
||||
});
|
||||
|
||||
it("should handle null conversation status", () => {
|
||||
// Mock agent store to return RUNNING state
|
||||
mockAgentStore(AgentState.RUNNING);
|
||||
|
||||
renderWithProviders(<ServerStatus conversationStatus={null} />);
|
||||
|
||||
const statusText = screen.getByText("Running");
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { QueryClientProvider, QueryClient } from "@tanstack/react-query";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { Provider } from "react-redux";
|
||||
import { setupStore } from "test-utils";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { HomeHeader } from "#/components/features/home/home-header/home-header";
|
||||
|
||||
@@ -26,11 +24,9 @@ vi.mock("react-i18next", async () => {
|
||||
const renderHomeHeader = () => {
|
||||
return render(<HomeHeader />, {
|
||||
wrapper: ({ children }) => (
|
||||
<Provider store={setupStore()}>
|
||||
<QueryClientProvider client={new QueryClient()}>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
</Provider>
|
||||
<QueryClientProvider client={new QueryClient()}>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { QueryClientProvider, QueryClient } from "@tanstack/react-query";
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { Provider } from "react-redux";
|
||||
import { createRoutesStub } from "react-router";
|
||||
import { setupStore } from "test-utils";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import ConversationService from "#/api/conversation-service/conversation-service.api";
|
||||
@@ -43,11 +41,9 @@ const renderNewConversation = () => {
|
||||
|
||||
return render(<RouterStub />, {
|
||||
wrapper: ({ children }) => (
|
||||
<Provider store={setupStore()}>
|
||||
<QueryClientProvider client={new QueryClient()}>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
</Provider>
|
||||
<QueryClientProvider client={new QueryClient()}>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -2,8 +2,6 @@ import { render, screen, waitFor, within } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { QueryClientProvider, QueryClient } from "@tanstack/react-query";
|
||||
import { setupStore } from "test-utils";
|
||||
import { Provider } from "react-redux";
|
||||
import { createRoutesStub, Outlet } from "react-router";
|
||||
import SettingsService from "#/settings-service/settings-service.api";
|
||||
import ConversationService from "#/api/conversation-service/conversation-service.api";
|
||||
@@ -42,11 +40,9 @@ const renderRepoConnector = () => {
|
||||
|
||||
return render(<RouterStub />, {
|
||||
wrapper: ({ children }) => (
|
||||
<Provider store={setupStore()}>
|
||||
<QueryClientProvider client={new QueryClient()}>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
</Provider>
|
||||
<QueryClientProvider client={new QueryClient()}>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -2,9 +2,7 @@ import { render, screen } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { Provider } from "react-redux";
|
||||
import { createRoutesStub } from "react-router";
|
||||
import { setupStore } from "test-utils";
|
||||
import ConversationService from "#/api/conversation-service/conversation-service.api";
|
||||
import UserService from "#/api/user-service/user-service.api";
|
||||
import GitService from "#/api/git-service/git-service.api";
|
||||
@@ -41,11 +39,9 @@ const renderTaskCard = (task = MOCK_TASK_1) => {
|
||||
|
||||
return render(<RouterStub />, {
|
||||
wrapper: ({ children }) => (
|
||||
<Provider store={setupStore()}>
|
||||
<QueryClientProvider client={new QueryClient()}>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
</Provider>
|
||||
<QueryClientProvider client={new QueryClient()}>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { Provider } from "react-redux";
|
||||
import { createRoutesStub } from "react-router";
|
||||
import { setupStore } from "test-utils";
|
||||
import { TaskSuggestions } from "#/components/features/home/tasks/task-suggestions";
|
||||
import { SuggestionsService } from "#/api/suggestions-service/suggestions-service.api";
|
||||
import { MOCK_TASKS } from "#/mocks/task-suggestions-handlers";
|
||||
@@ -62,11 +60,9 @@ const renderTaskSuggestions = () => {
|
||||
|
||||
return render(<RouterStub />, {
|
||||
wrapper: ({ children }) => (
|
||||
<Provider store={setupStore()}>
|
||||
<QueryClientProvider client={new QueryClient()}>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
</Provider>
|
||||
<QueryClientProvider client={new QueryClient()}>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -12,6 +12,7 @@ import GitService from "#/api/git-service/git-service.api";
|
||||
import { GitRepository } from "#/types/git";
|
||||
import { RepositoryMicroagent } from "#/types/microagent-management";
|
||||
import { Conversation } from "#/api/open-hands.types";
|
||||
import { useMicroagentManagementStore } from "#/state/microagent-management-store";
|
||||
|
||||
// Mock hooks
|
||||
const mockUseUserProviders = vi.fn();
|
||||
@@ -55,20 +56,47 @@ describe("MicroagentManagement", () => {
|
||||
]);
|
||||
|
||||
const renderMicroagentManagement = (config?: QueryClientConfig) =>
|
||||
renderWithProviders(<RouterStub />, {
|
||||
preloadedState: {
|
||||
microagentManagement: {
|
||||
addMicroagentModalVisible: false,
|
||||
updateMicroagentModalVisible: false,
|
||||
selectedRepository: null,
|
||||
personalRepositories: [],
|
||||
organizationRepositories: [],
|
||||
repositories: [],
|
||||
selectedMicroagentItem: null,
|
||||
learnThisRepoModalVisible: false,
|
||||
},
|
||||
},
|
||||
renderWithProviders(<RouterStub />);
|
||||
|
||||
// Common test data
|
||||
const testRepository = {
|
||||
id: "1",
|
||||
full_name: "user/test-repo",
|
||||
git_provider: "github" as const,
|
||||
is_public: true,
|
||||
owner_type: "user" as const,
|
||||
pushed_at: "2021-10-01T12:00:00Z",
|
||||
};
|
||||
|
||||
// Helper function to render with custom Zustand store state
|
||||
const renderWithCustomStore = (storeOverrides: Partial<any>) => {
|
||||
useMicroagentManagementStore.setState(storeOverrides);
|
||||
return renderWithProviders(<RouterStub />);
|
||||
};
|
||||
|
||||
// Helper function to render with update modal visible
|
||||
const renderWithUpdateModal = (additionalState: Partial<any> = {}) => {
|
||||
return renderWithCustomStore({
|
||||
updateMicroagentModalVisible: true,
|
||||
selectedRepository: testRepository,
|
||||
...additionalState,
|
||||
});
|
||||
};
|
||||
|
||||
// Helper function to render with selected microagent
|
||||
const renderWithSelectedMicroagent = (
|
||||
microagent: any,
|
||||
additionalState: Partial<any> = {},
|
||||
) => {
|
||||
return renderWithCustomStore({
|
||||
selectedRepository: testRepository,
|
||||
selectedMicroagentItem: {
|
||||
microagent,
|
||||
conversation: null,
|
||||
},
|
||||
...additionalState,
|
||||
});
|
||||
};
|
||||
|
||||
beforeAll(() => {
|
||||
vi.mock("react-router", async (importOriginal) => ({
|
||||
@@ -129,8 +157,52 @@ describe("MicroagentManagement", () => {
|
||||
owner_type: "organization",
|
||||
pushed_at: "2021-10-06T12:00:00Z",
|
||||
},
|
||||
{
|
||||
id: "7",
|
||||
full_name: "user/gitlab-repo/openhands-config",
|
||||
git_provider: "gitlab",
|
||||
is_public: true,
|
||||
owner_type: "user",
|
||||
pushed_at: "2021-10-07T12:00:00Z",
|
||||
},
|
||||
{
|
||||
id: "8",
|
||||
full_name: "org/gitlab-org-repo/openhands-config",
|
||||
git_provider: "gitlab",
|
||||
is_public: true,
|
||||
owner_type: "organization",
|
||||
pushed_at: "2021-10-08T12:00:00Z",
|
||||
},
|
||||
];
|
||||
|
||||
// Helper function to filter repositories with OpenHands suffixes
|
||||
const getRepositoriesWithOpenHandsSuffix = (
|
||||
repositories: GitRepository[],
|
||||
) => {
|
||||
return repositories.filter(
|
||||
(repo) =>
|
||||
repo.full_name.endsWith("/.openhands") ||
|
||||
repo.full_name.endsWith("/openhands-config"),
|
||||
);
|
||||
};
|
||||
|
||||
// Helper functions for mocking search repositories
|
||||
const mockSearchRepositoriesWithData = (data: GitRepository[]) => {
|
||||
mockUseSearchRepositories.mockReturnValue({
|
||||
data,
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
});
|
||||
};
|
||||
|
||||
const mockSearchRepositoriesEmpty = () => {
|
||||
mockUseSearchRepositories.mockReturnValue({
|
||||
data: [],
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
});
|
||||
};
|
||||
|
||||
const mockMicroagents: RepositoryMicroagent[] = [
|
||||
{
|
||||
name: "test-microagent-1",
|
||||
@@ -181,6 +253,23 @@ describe("MicroagentManagement", () => {
|
||||
vi.clearAllMocks();
|
||||
vi.restoreAllMocks();
|
||||
|
||||
// Reset Zustand store to default state
|
||||
useMicroagentManagementStore.setState({
|
||||
// Modal visibility states
|
||||
addMicroagentModalVisible: false,
|
||||
updateMicroagentModalVisible: false,
|
||||
learnThisRepoModalVisible: false,
|
||||
|
||||
// Repository states
|
||||
selectedRepository: null,
|
||||
personalRepositories: [],
|
||||
organizationRepositories: [],
|
||||
repositories: [],
|
||||
|
||||
// Microagent states
|
||||
selectedMicroagentItem: null,
|
||||
});
|
||||
|
||||
// Setup default hook mocks
|
||||
mockUseUserProviders.mockReturnValue({
|
||||
providers: ["github"],
|
||||
@@ -220,11 +309,11 @@ describe("MicroagentManagement", () => {
|
||||
isError: false,
|
||||
});
|
||||
|
||||
mockUseSearchRepositories.mockReturnValue({
|
||||
data: [],
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
});
|
||||
// Mock the search repositories hook to return repositories with OpenHands suffixes
|
||||
const mockSearchResults =
|
||||
getRepositoriesWithOpenHandsSuffix(mockRepositories);
|
||||
|
||||
mockSearchRepositoriesWithData(mockSearchResults);
|
||||
|
||||
// Setup default mock for retrieveUserGitRepositories
|
||||
vi.spyOn(GitService, "retrieveUserGitRepositories").mockResolvedValue({
|
||||
@@ -549,6 +638,9 @@ describe("MicroagentManagement", () => {
|
||||
onLoadMore: vi.fn(),
|
||||
});
|
||||
|
||||
// Mock empty search results
|
||||
mockSearchRepositoriesEmpty();
|
||||
|
||||
renderMicroagentManagement();
|
||||
|
||||
// Wait for repositories to be loaded
|
||||
@@ -737,6 +829,10 @@ describe("MicroagentManagement", () => {
|
||||
|
||||
it("should handle empty search results", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
// Mock empty search results for this test
|
||||
mockSearchRepositoriesEmpty();
|
||||
|
||||
renderMicroagentManagement();
|
||||
|
||||
// Wait for repositories to be loaded
|
||||
@@ -1342,28 +1438,10 @@ describe("MicroagentManagement", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should render modal when Redux state is set to visible", async () => {
|
||||
// Render with modal already visible in Redux state
|
||||
renderWithProviders(<RouterStub />, {
|
||||
preloadedState: {
|
||||
microagentManagement: {
|
||||
selectedMicroagentItem: null,
|
||||
addMicroagentModalVisible: true, // Start with modal visible
|
||||
selectedRepository: {
|
||||
id: "1",
|
||||
full_name: "user/test-repo",
|
||||
git_provider: "github",
|
||||
is_public: true,
|
||||
owner_type: "user",
|
||||
pushed_at: "2021-10-01T12:00:00Z",
|
||||
},
|
||||
personalRepositories: [],
|
||||
organizationRepositories: [],
|
||||
repositories: [],
|
||||
updateMicroagentModalVisible: false,
|
||||
learnThisRepoModalVisible: false,
|
||||
},
|
||||
},
|
||||
it("should render modal when Zustand state is set to visible", async () => {
|
||||
// Render with modal already visible in Zustand state
|
||||
renderWithCustomStore({
|
||||
addMicroagentModalVisible: true,
|
||||
});
|
||||
|
||||
// Check that modal is rendered
|
||||
@@ -1633,29 +1711,16 @@ describe("MicroagentManagement", () => {
|
||||
pr_number: null,
|
||||
};
|
||||
|
||||
const renderMicroagentManagementMain = (selectedMicroagentItem: any) =>
|
||||
renderWithProviders(<MicroagentManagementMain />, {
|
||||
preloadedState: {
|
||||
microagentManagement: {
|
||||
addMicroagentModalVisible: false,
|
||||
selectedRepository: {
|
||||
id: "1",
|
||||
full_name: "user/test-repo",
|
||||
git_provider: "github",
|
||||
is_public: true,
|
||||
owner_type: "user",
|
||||
pushed_at: "2021-10-01T12:00:00Z",
|
||||
},
|
||||
personalRepositories: [],
|
||||
organizationRepositories: [],
|
||||
repositories: [],
|
||||
selectedMicroagentItem,
|
||||
updateMicroagentModalVisible: false,
|
||||
learnThisRepoModalVisible: false,
|
||||
},
|
||||
},
|
||||
const renderMicroagentManagementMain = (selectedMicroagentItem: any) => {
|
||||
// Set the store with the selected microagent item and a repository
|
||||
useMicroagentManagementStore.setState({
|
||||
selectedMicroagentItem,
|
||||
selectedRepository: testRepository,
|
||||
});
|
||||
|
||||
return renderWithProviders(<MicroagentManagementMain />);
|
||||
};
|
||||
|
||||
it("should render MicroagentManagementDefault when no microagent or conversation is selected", async () => {
|
||||
renderMicroagentManagementMain(null);
|
||||
|
||||
@@ -1980,31 +2045,8 @@ describe("MicroagentManagement", () => {
|
||||
});
|
||||
|
||||
it("should render update microagent modal when updateMicroagentModalVisible is true", async () => {
|
||||
// Render with update modal visible in Redux state
|
||||
renderWithProviders(<RouterStub />, {
|
||||
preloadedState: {
|
||||
microagentManagement: {
|
||||
selectedMicroagentItem: {
|
||||
microagent: mockMicroagentForUpdate,
|
||||
conversation: undefined,
|
||||
},
|
||||
addMicroagentModalVisible: false,
|
||||
updateMicroagentModalVisible: true, // Start with update modal visible
|
||||
selectedRepository: {
|
||||
id: "1",
|
||||
full_name: "user/test-repo",
|
||||
git_provider: "github",
|
||||
is_public: true,
|
||||
owner_type: "user",
|
||||
pushed_at: "2021-10-01T12:00:00Z",
|
||||
},
|
||||
personalRepositories: [],
|
||||
organizationRepositories: [],
|
||||
repositories: [],
|
||||
learnThisRepoModalVisible: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
// Render with update modal visible in Zustand state
|
||||
renderWithUpdateModal();
|
||||
|
||||
// Check that update modal is rendered
|
||||
expect(screen.getByTestId("add-microagent-modal")).toBeInTheDocument();
|
||||
@@ -2015,30 +2057,7 @@ describe("MicroagentManagement", () => {
|
||||
|
||||
it("should display update microagent title when isUpdate is true", async () => {
|
||||
// Render with update modal visible and selected microagent
|
||||
renderWithProviders(<RouterStub />, {
|
||||
preloadedState: {
|
||||
microagentManagement: {
|
||||
selectedMicroagentItem: {
|
||||
microagent: mockMicroagentForUpdate,
|
||||
conversation: undefined,
|
||||
},
|
||||
addMicroagentModalVisible: false,
|
||||
updateMicroagentModalVisible: true,
|
||||
selectedRepository: {
|
||||
id: "1",
|
||||
full_name: "user/test-repo",
|
||||
git_provider: "github",
|
||||
is_public: true,
|
||||
owner_type: "user",
|
||||
pushed_at: "2021-10-01T12:00:00Z",
|
||||
},
|
||||
personalRepositories: [],
|
||||
organizationRepositories: [],
|
||||
repositories: [],
|
||||
learnThisRepoModalVisible: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
renderWithUpdateModal();
|
||||
|
||||
// Check that the update title is displayed
|
||||
expect(
|
||||
@@ -2048,28 +2067,10 @@ describe("MicroagentManagement", () => {
|
||||
|
||||
it("should populate form fields with existing microagent data when updating", async () => {
|
||||
// Render with update modal visible and selected microagent
|
||||
renderWithProviders(<RouterStub />, {
|
||||
preloadedState: {
|
||||
microagentManagement: {
|
||||
selectedMicroagentItem: {
|
||||
microagent: mockMicroagentForUpdate,
|
||||
conversation: undefined,
|
||||
},
|
||||
addMicroagentModalVisible: false,
|
||||
updateMicroagentModalVisible: true,
|
||||
selectedRepository: {
|
||||
id: "1",
|
||||
full_name: "user/test-repo",
|
||||
git_provider: "github",
|
||||
is_public: true,
|
||||
owner_type: "user",
|
||||
pushed_at: "2021-10-01T12:00:00Z",
|
||||
},
|
||||
personalRepositories: [],
|
||||
organizationRepositories: [],
|
||||
repositories: [],
|
||||
learnThisRepoModalVisible: false,
|
||||
},
|
||||
renderWithUpdateModal({
|
||||
selectedMicroagentItem: {
|
||||
microagent: mockMicroagentForUpdate,
|
||||
conversation: null,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -2086,30 +2087,7 @@ describe("MicroagentManagement", () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
// Render with update modal visible and selected microagent
|
||||
renderWithProviders(<RouterStub />, {
|
||||
preloadedState: {
|
||||
microagentManagement: {
|
||||
selectedMicroagentItem: {
|
||||
microagent: mockMicroagentForUpdate,
|
||||
conversation: undefined,
|
||||
},
|
||||
addMicroagentModalVisible: false,
|
||||
updateMicroagentModalVisible: true,
|
||||
selectedRepository: {
|
||||
id: "1",
|
||||
full_name: "user/test-repo",
|
||||
git_provider: "github",
|
||||
is_public: true,
|
||||
owner_type: "user",
|
||||
pushed_at: "2021-10-01T12:00:00Z",
|
||||
},
|
||||
personalRepositories: [],
|
||||
organizationRepositories: [],
|
||||
repositories: [],
|
||||
learnThisRepoModalVisible: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
renderWithUpdateModal();
|
||||
|
||||
// Wait for modal to be rendered
|
||||
await waitFor(() => {
|
||||
@@ -2137,30 +2115,7 @@ describe("MicroagentManagement", () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
// Render with update modal visible
|
||||
renderWithProviders(<RouterStub />, {
|
||||
preloadedState: {
|
||||
microagentManagement: {
|
||||
selectedMicroagentItem: {
|
||||
microagent: mockMicroagentForUpdate,
|
||||
conversation: undefined,
|
||||
},
|
||||
addMicroagentModalVisible: false,
|
||||
updateMicroagentModalVisible: true,
|
||||
selectedRepository: {
|
||||
id: "1",
|
||||
full_name: "user/test-repo",
|
||||
git_provider: "github",
|
||||
is_public: true,
|
||||
owner_type: "user",
|
||||
pushed_at: "2021-10-01T12:00:00Z",
|
||||
},
|
||||
personalRepositories: [],
|
||||
organizationRepositories: [],
|
||||
repositories: [],
|
||||
learnThisRepoModalVisible: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
renderWithUpdateModal();
|
||||
|
||||
// Wait for modal to be rendered
|
||||
await waitFor(() => {
|
||||
@@ -2183,30 +2138,7 @@ describe("MicroagentManagement", () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
// Render with update modal visible
|
||||
renderWithProviders(<RouterStub />, {
|
||||
preloadedState: {
|
||||
microagentManagement: {
|
||||
selectedMicroagentItem: {
|
||||
microagent: mockMicroagentForUpdate,
|
||||
conversation: undefined,
|
||||
},
|
||||
addMicroagentModalVisible: false,
|
||||
updateMicroagentModalVisible: true,
|
||||
selectedRepository: {
|
||||
id: "1",
|
||||
full_name: "user/test-repo",
|
||||
git_provider: "github",
|
||||
is_public: true,
|
||||
owner_type: "user",
|
||||
pushed_at: "2021-10-01T12:00:00Z",
|
||||
},
|
||||
personalRepositories: [],
|
||||
organizationRepositories: [],
|
||||
repositories: [],
|
||||
learnThisRepoModalVisible: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
renderWithUpdateModal();
|
||||
|
||||
// Wait for modal to be rendered
|
||||
await waitFor(() => {
|
||||
@@ -2232,27 +2164,7 @@ describe("MicroagentManagement", () => {
|
||||
|
||||
it("should handle update modal with empty microagent data", async () => {
|
||||
// Render with update modal visible but no microagent data
|
||||
renderWithProviders(<RouterStub />, {
|
||||
preloadedState: {
|
||||
microagentManagement: {
|
||||
selectedMicroagentItem: null,
|
||||
addMicroagentModalVisible: false,
|
||||
updateMicroagentModalVisible: true,
|
||||
selectedRepository: {
|
||||
id: "1",
|
||||
full_name: "user/test-repo",
|
||||
git_provider: "github",
|
||||
is_public: true,
|
||||
owner_type: "user",
|
||||
pushed_at: "2021-10-01T12:00:00Z",
|
||||
},
|
||||
personalRepositories: [],
|
||||
organizationRepositories: [],
|
||||
repositories: [],
|
||||
learnThisRepoModalVisible: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
renderWithUpdateModal();
|
||||
|
||||
// Check that update modal is still rendered
|
||||
expect(screen.getByTestId("add-microagent-modal")).toBeInTheDocument();
|
||||
@@ -2273,30 +2185,7 @@ describe("MicroagentManagement", () => {
|
||||
});
|
||||
|
||||
// Render with update modal visible and microagent
|
||||
renderWithProviders(<RouterStub />, {
|
||||
preloadedState: {
|
||||
microagentManagement: {
|
||||
selectedMicroagentItem: {
|
||||
microagent: mockMicroagentForUpdate,
|
||||
conversation: undefined,
|
||||
},
|
||||
addMicroagentModalVisible: false,
|
||||
updateMicroagentModalVisible: true,
|
||||
selectedRepository: {
|
||||
id: "1",
|
||||
full_name: "user/test-repo",
|
||||
git_provider: "github",
|
||||
is_public: true,
|
||||
owner_type: "user",
|
||||
pushed_at: "2021-10-01T12:00:00Z",
|
||||
},
|
||||
personalRepositories: [],
|
||||
organizationRepositories: [],
|
||||
repositories: [],
|
||||
learnThisRepoModalVisible: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
renderWithUpdateModal();
|
||||
|
||||
// Wait for the content to be loaded and check that the form field is empty
|
||||
await waitFor(() => {
|
||||
@@ -2317,30 +2206,7 @@ describe("MicroagentManagement", () => {
|
||||
});
|
||||
|
||||
// Render with update modal visible and microagent
|
||||
renderWithProviders(<RouterStub />, {
|
||||
preloadedState: {
|
||||
microagentManagement: {
|
||||
selectedMicroagentItem: {
|
||||
microagent: mockMicroagentForUpdate,
|
||||
conversation: undefined,
|
||||
},
|
||||
addMicroagentModalVisible: false,
|
||||
updateMicroagentModalVisible: true,
|
||||
selectedRepository: {
|
||||
id: "1",
|
||||
full_name: "user/test-repo",
|
||||
git_provider: "github",
|
||||
is_public: true,
|
||||
owner_type: "user",
|
||||
pushed_at: "2021-10-01T12:00:00Z",
|
||||
},
|
||||
personalRepositories: [],
|
||||
organizationRepositories: [],
|
||||
repositories: [],
|
||||
learnThisRepoModalVisible: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
renderWithUpdateModal();
|
||||
|
||||
// Check that the modal is rendered correctly
|
||||
expect(screen.getByTestId("add-microagent-modal")).toBeInTheDocument();
|
||||
@@ -2499,30 +2365,7 @@ describe("MicroagentManagement", () => {
|
||||
|
||||
it("should render learn something new button in microagent view", async () => {
|
||||
// Render with selected microagent
|
||||
renderWithProviders(<RouterStub />, {
|
||||
preloadedState: {
|
||||
microagentManagement: {
|
||||
selectedMicroagentItem: {
|
||||
microagent: mockMicroagentForLearn,
|
||||
conversation: undefined,
|
||||
},
|
||||
addMicroagentModalVisible: false,
|
||||
updateMicroagentModalVisible: false,
|
||||
selectedRepository: {
|
||||
id: "1",
|
||||
full_name: "user/test-repo",
|
||||
git_provider: "github",
|
||||
is_public: true,
|
||||
owner_type: "user",
|
||||
pushed_at: "2021-10-01T12:00:00Z",
|
||||
},
|
||||
personalRepositories: [],
|
||||
organizationRepositories: [],
|
||||
repositories: [],
|
||||
learnThisRepoModalVisible: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
renderWithSelectedMicroagent(mockMicroagentForLearn);
|
||||
|
||||
// Check that the learn something new button is displayed
|
||||
expect(
|
||||
@@ -2534,30 +2377,7 @@ describe("MicroagentManagement", () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
// Render with selected microagent
|
||||
renderWithProviders(<RouterStub />, {
|
||||
preloadedState: {
|
||||
microagentManagement: {
|
||||
selectedMicroagentItem: {
|
||||
microagent: mockMicroagentForLearn,
|
||||
conversation: undefined,
|
||||
},
|
||||
addMicroagentModalVisible: false,
|
||||
updateMicroagentModalVisible: false,
|
||||
selectedRepository: {
|
||||
id: "1",
|
||||
full_name: "user/test-repo",
|
||||
git_provider: "github",
|
||||
is_public: true,
|
||||
owner_type: "user",
|
||||
pushed_at: "2021-10-01T12:00:00Z",
|
||||
},
|
||||
personalRepositories: [],
|
||||
organizationRepositories: [],
|
||||
repositories: [],
|
||||
learnThisRepoModalVisible: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
renderWithSelectedMicroagent(mockMicroagentForLearn);
|
||||
|
||||
// Find and click the learn something new button
|
||||
const learnButton = screen.getByText("COMMON$LEARN_SOMETHING_NEW");
|
||||
@@ -2586,30 +2406,7 @@ describe("MicroagentManagement", () => {
|
||||
});
|
||||
|
||||
// Render with selected microagent
|
||||
renderWithProviders(<RouterStub />, {
|
||||
preloadedState: {
|
||||
microagentManagement: {
|
||||
selectedMicroagentItem: {
|
||||
microagent: mockMicroagentForLearn,
|
||||
conversation: undefined,
|
||||
},
|
||||
addMicroagentModalVisible: false,
|
||||
updateMicroagentModalVisible: false,
|
||||
selectedRepository: {
|
||||
id: "1",
|
||||
full_name: "user/test-repo",
|
||||
git_provider: "github",
|
||||
is_public: true,
|
||||
owner_type: "user",
|
||||
pushed_at: "2021-10-01T12:00:00Z",
|
||||
},
|
||||
personalRepositories: [],
|
||||
organizationRepositories: [],
|
||||
repositories: [],
|
||||
learnThisRepoModalVisible: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
renderWithSelectedMicroagent(mockMicroagentForLearn);
|
||||
|
||||
// Find and click the learn something new button
|
||||
const learnButton = screen.getByText("COMMON$LEARN_SOMETHING_NEW");
|
||||
@@ -2641,30 +2438,7 @@ describe("MicroagentManagement", () => {
|
||||
});
|
||||
|
||||
// Render with selected microagent
|
||||
renderWithProviders(<RouterStub />, {
|
||||
preloadedState: {
|
||||
microagentManagement: {
|
||||
selectedMicroagentItem: {
|
||||
microagent: mockMicroagentForLearn,
|
||||
conversation: undefined,
|
||||
},
|
||||
addMicroagentModalVisible: false,
|
||||
updateMicroagentModalVisible: false,
|
||||
selectedRepository: {
|
||||
id: "1",
|
||||
full_name: "user/test-repo",
|
||||
git_provider: "github",
|
||||
is_public: true,
|
||||
owner_type: "user",
|
||||
pushed_at: "2021-10-01T12:00:00Z",
|
||||
},
|
||||
personalRepositories: [],
|
||||
organizationRepositories: [],
|
||||
repositories: [],
|
||||
learnThisRepoModalVisible: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
renderWithSelectedMicroagent(mockMicroagentForLearn);
|
||||
|
||||
// Find and click the learn something new button
|
||||
const learnButton = screen.getByText("COMMON$LEARN_SOMETHING_NEW");
|
||||
@@ -2694,30 +2468,7 @@ describe("MicroagentManagement", () => {
|
||||
});
|
||||
|
||||
// Render with selected microagent
|
||||
renderWithProviders(<RouterStub />, {
|
||||
preloadedState: {
|
||||
microagentManagement: {
|
||||
selectedMicroagentItem: {
|
||||
microagent: mockMicroagentForLearn,
|
||||
conversation: undefined,
|
||||
},
|
||||
addMicroagentModalVisible: false,
|
||||
updateMicroagentModalVisible: false,
|
||||
selectedRepository: {
|
||||
id: "1",
|
||||
full_name: "user/test-repo",
|
||||
git_provider: "github",
|
||||
is_public: true,
|
||||
owner_type: "user",
|
||||
pushed_at: "2021-10-01T12:00:00Z",
|
||||
},
|
||||
personalRepositories: [],
|
||||
organizationRepositories: [],
|
||||
repositories: [],
|
||||
learnThisRepoModalVisible: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
renderWithSelectedMicroagent(mockMicroagentForLearn);
|
||||
|
||||
// Find and click the learn something new button
|
||||
const learnButton = screen.getByText("COMMON$LEARN_SOMETHING_NEW");
|
||||
|
||||
@@ -5,6 +5,18 @@ import { MemoryRouter } from "react-router";
|
||||
import { InteractiveChatBox } from "#/components/features/chat/interactive-chat-box";
|
||||
import { renderWithProviders } from "../../test-utils";
|
||||
import { AgentState } from "#/types/agent-state";
|
||||
import { useAgentStore } from "#/stores/agent-store";
|
||||
import { useConversationStore } from "#/state/conversation-store";
|
||||
|
||||
// Mock the agent store
|
||||
vi.mock("#/stores/agent-store", () => ({
|
||||
useAgentStore: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock the conversation store
|
||||
vi.mock("#/state/conversation-store", () => ({
|
||||
useConversationStore: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock React Router hooks
|
||||
vi.mock("react-router", async () => {
|
||||
@@ -47,6 +59,49 @@ describe("InteractiveChatBox", () => {
|
||||
const onSubmitMock = vi.fn();
|
||||
const onStopMock = vi.fn();
|
||||
|
||||
// Helper function to mock stores
|
||||
const mockStores = (agentState: AgentState = AgentState.INIT) => {
|
||||
vi.mocked(useAgentStore).mockReturnValue({
|
||||
curAgentState: agentState,
|
||||
setCurrentAgentState: vi.fn(),
|
||||
reset: vi.fn(),
|
||||
});
|
||||
|
||||
vi.mocked(useConversationStore).mockReturnValue({
|
||||
images: [],
|
||||
files: [],
|
||||
addImages: vi.fn(),
|
||||
addFiles: vi.fn(),
|
||||
clearAllFiles: vi.fn(),
|
||||
addFileLoading: vi.fn(),
|
||||
removeFileLoading: vi.fn(),
|
||||
addImageLoading: vi.fn(),
|
||||
removeImageLoading: vi.fn(),
|
||||
submittedMessage: null,
|
||||
setShouldHideSuggestions: vi.fn(),
|
||||
setSubmittedMessage: vi.fn(),
|
||||
isRightPanelShown: true,
|
||||
selectedTab: "editor" as const,
|
||||
loadingFiles: [],
|
||||
loadingImages: [],
|
||||
messageToSend: null,
|
||||
shouldShownAgentLoading: false,
|
||||
shouldHideSuggestions: false,
|
||||
hasRightPanelToggled: true,
|
||||
setIsRightPanelShown: vi.fn(),
|
||||
setSelectedTab: vi.fn(),
|
||||
setShouldShownAgentLoading: vi.fn(),
|
||||
removeImage: vi.fn(),
|
||||
removeFile: vi.fn(),
|
||||
clearImages: vi.fn(),
|
||||
clearFiles: vi.fn(),
|
||||
clearAllLoading: vi.fn(),
|
||||
setMessageToSend: vi.fn(),
|
||||
resetConversationState: vi.fn(),
|
||||
setHasRightPanelToggled: vi.fn(),
|
||||
});
|
||||
};
|
||||
|
||||
// Helper function to render with Router context
|
||||
const renderInteractiveChatBox = (props: any, options: any = {}) => {
|
||||
return renderWithProviders(
|
||||
@@ -68,22 +123,12 @@ describe("InteractiveChatBox", () => {
|
||||
});
|
||||
|
||||
it("should render", () => {
|
||||
renderInteractiveChatBox(
|
||||
{
|
||||
onSubmit: onSubmitMock,
|
||||
onStop: onStopMock,
|
||||
isWaitingForUserInput: false,
|
||||
hasSubstantiveAgentActions: false,
|
||||
optimisticUserMessage: false,
|
||||
},
|
||||
{
|
||||
preloadedState: {
|
||||
agent: {
|
||||
curAgentState: AgentState.INIT,
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
mockStores(AgentState.INIT);
|
||||
|
||||
renderInteractiveChatBox({
|
||||
onSubmit: onSubmitMock,
|
||||
onStop: onStopMock,
|
||||
});
|
||||
|
||||
const chatBox = screen.getByTestId("interactive-chat-box");
|
||||
expect(chatBox).toBeInTheDocument();
|
||||
@@ -91,33 +136,12 @@ describe("InteractiveChatBox", () => {
|
||||
|
||||
it("should set custom values", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderInteractiveChatBox(
|
||||
{
|
||||
onSubmit: onSubmitMock,
|
||||
onStop: onStopMock,
|
||||
isWaitingForUserInput: true,
|
||||
hasSubstantiveAgentActions: true,
|
||||
optimisticUserMessage: false,
|
||||
},
|
||||
{
|
||||
preloadedState: {
|
||||
agent: {
|
||||
curAgentState: AgentState.AWAITING_USER_INPUT,
|
||||
},
|
||||
conversation: {
|
||||
isRightPanelShown: true,
|
||||
shouldStopConversation: false,
|
||||
shouldStartConversation: false,
|
||||
images: [],
|
||||
files: [],
|
||||
loadingFiles: [],
|
||||
loadingImages: [],
|
||||
messageToSend: null,
|
||||
shouldShownAgentLoading: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
mockStores(AgentState.AWAITING_USER_INPUT);
|
||||
|
||||
renderInteractiveChatBox({
|
||||
onSubmit: onSubmitMock,
|
||||
onStop: onStopMock,
|
||||
});
|
||||
|
||||
const textbox = screen.getByTestId("chat-input");
|
||||
|
||||
@@ -129,22 +153,12 @@ describe("InteractiveChatBox", () => {
|
||||
|
||||
it("should display the image previews when images are uploaded", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderInteractiveChatBox(
|
||||
{
|
||||
onSubmit: onSubmitMock,
|
||||
onStop: onStopMock,
|
||||
isWaitingForUserInput: false,
|
||||
hasSubstantiveAgentActions: false,
|
||||
optimisticUserMessage: false,
|
||||
},
|
||||
{
|
||||
preloadedState: {
|
||||
agent: {
|
||||
curAgentState: AgentState.INIT,
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
mockStores(AgentState.INIT);
|
||||
|
||||
renderInteractiveChatBox({
|
||||
onSubmit: onSubmitMock,
|
||||
onStop: onStopMock,
|
||||
});
|
||||
|
||||
// Create a larger file to ensure it passes validation
|
||||
const fileContent = new Array(1024).fill("a").join(""); // 1KB file
|
||||
@@ -166,22 +180,12 @@ describe("InteractiveChatBox", () => {
|
||||
|
||||
it("should remove the image preview when the close button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderInteractiveChatBox(
|
||||
{
|
||||
onSubmit: onSubmitMock,
|
||||
onStop: onStopMock,
|
||||
isWaitingForUserInput: false,
|
||||
hasSubstantiveAgentActions: false,
|
||||
optimisticUserMessage: false,
|
||||
},
|
||||
{
|
||||
preloadedState: {
|
||||
agent: {
|
||||
curAgentState: AgentState.INIT,
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
mockStores(AgentState.INIT);
|
||||
|
||||
renderInteractiveChatBox({
|
||||
onSubmit: onSubmitMock,
|
||||
onStop: onStopMock,
|
||||
});
|
||||
|
||||
const fileContent = new Array(1024).fill("a").join(""); // 1KB file
|
||||
const file = new File([fileContent], "chucknorris.png", {
|
||||
@@ -201,22 +205,12 @@ describe("InteractiveChatBox", () => {
|
||||
|
||||
it("should call onSubmit with the message and images", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderInteractiveChatBox(
|
||||
{
|
||||
onSubmit: onSubmitMock,
|
||||
onStop: onStopMock,
|
||||
isWaitingForUserInput: false,
|
||||
hasSubstantiveAgentActions: false,
|
||||
optimisticUserMessage: false,
|
||||
},
|
||||
{
|
||||
preloadedState: {
|
||||
agent: {
|
||||
curAgentState: AgentState.INIT,
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
mockStores(AgentState.INIT);
|
||||
|
||||
renderInteractiveChatBox({
|
||||
onSubmit: onSubmitMock,
|
||||
onStop: onStopMock,
|
||||
});
|
||||
|
||||
const textarea = screen.getByTestId("chat-input");
|
||||
|
||||
@@ -242,22 +236,12 @@ describe("InteractiveChatBox", () => {
|
||||
|
||||
it("should disable the submit button when agent is loading", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderInteractiveChatBox(
|
||||
{
|
||||
onSubmit: onSubmitMock,
|
||||
onStop: onStopMock,
|
||||
isWaitingForUserInput: false,
|
||||
hasSubstantiveAgentActions: false,
|
||||
optimisticUserMessage: false,
|
||||
},
|
||||
{
|
||||
preloadedState: {
|
||||
agent: {
|
||||
curAgentState: AgentState.LOADING,
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
mockStores(AgentState.LOADING);
|
||||
|
||||
renderInteractiveChatBox({
|
||||
onSubmit: onSubmitMock,
|
||||
onStop: onStopMock,
|
||||
});
|
||||
|
||||
const button = screen.getByTestId("submit-button");
|
||||
expect(button).toBeDisabled();
|
||||
@@ -268,23 +252,14 @@ describe("InteractiveChatBox", () => {
|
||||
|
||||
it("should display the stop button when agent is running and call onStop when clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderInteractiveChatBox(
|
||||
{
|
||||
onSubmit: onSubmitMock,
|
||||
onStop: onStopMock,
|
||||
isWaitingForUserInput: false,
|
||||
hasSubstantiveAgentActions: true,
|
||||
optimisticUserMessage: false,
|
||||
},
|
||||
{
|
||||
preloadedState: {
|
||||
agent: {
|
||||
curAgentState: AgentState.RUNNING,
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
mockStores(AgentState.RUNNING);
|
||||
|
||||
renderInteractiveChatBox({
|
||||
onSubmit: onSubmitMock,
|
||||
onStop: onStopMock,
|
||||
});
|
||||
|
||||
// The stop button should be available when agent is running
|
||||
const stopButton = screen.getByTestId("stop-button");
|
||||
expect(stopButton).toBeInTheDocument();
|
||||
|
||||
@@ -297,33 +272,12 @@ describe("InteractiveChatBox", () => {
|
||||
const onSubmit = vi.fn();
|
||||
const onStop = vi.fn();
|
||||
|
||||
const { rerender } = renderInteractiveChatBox(
|
||||
{
|
||||
onSubmit: onSubmit,
|
||||
onStop: onStop,
|
||||
isWaitingForUserInput: true,
|
||||
hasSubstantiveAgentActions: true,
|
||||
optimisticUserMessage: false,
|
||||
},
|
||||
{
|
||||
preloadedState: {
|
||||
agent: {
|
||||
curAgentState: AgentState.AWAITING_USER_INPUT,
|
||||
},
|
||||
conversation: {
|
||||
isRightPanelShown: true,
|
||||
shouldStopConversation: false,
|
||||
shouldStartConversation: false,
|
||||
images: [],
|
||||
files: [],
|
||||
loadingFiles: [],
|
||||
loadingImages: [],
|
||||
messageToSend: null,
|
||||
shouldShownAgentLoading: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
mockStores(AgentState.AWAITING_USER_INPUT);
|
||||
|
||||
const { rerender } = renderInteractiveChatBox({
|
||||
onSubmit: onSubmit,
|
||||
onStop: onStop,
|
||||
});
|
||||
|
||||
// Verify text input has the initial value
|
||||
const textarea = screen.getByTestId("chat-input");
|
||||
@@ -342,13 +296,7 @@ describe("InteractiveChatBox", () => {
|
||||
// Simulate parent component updating the value prop
|
||||
rerender(
|
||||
<MemoryRouter>
|
||||
<InteractiveChatBox
|
||||
onSubmit={onSubmit}
|
||||
onStop={onStop}
|
||||
isWaitingForUserInput={true}
|
||||
hasSubstantiveAgentActions={true}
|
||||
optimisticUserMessage={false}
|
||||
/>
|
||||
<InteractiveChatBox onSubmit={onSubmit} onStop={onStop} />
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
|
||||
@@ -1,42 +1,46 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { Provider } from "react-redux";
|
||||
import { configureStore } from "@reduxjs/toolkit";
|
||||
import { JupyterEditor } from "#/components/features/jupyter/jupyter";
|
||||
import { jupyterReducer } from "#/state/jupyter-slice";
|
||||
import { vi, describe, it, expect } from "vitest";
|
||||
import { vi, describe, it, expect, beforeEach } from "vitest";
|
||||
import { AgentState } from "#/types/agent-state";
|
||||
import { useAgentStore } from "#/stores/agent-store";
|
||||
import { useJupyterStore } from "#/state/jupyter-store";
|
||||
|
||||
// Mock the agent store
|
||||
vi.mock("#/stores/agent-store", () => ({
|
||||
useAgentStore: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock react-i18next
|
||||
vi.mock("react-i18next", () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}));
|
||||
|
||||
describe("JupyterEditor", () => {
|
||||
const mockStore = configureStore({
|
||||
reducer: {
|
||||
fileState: () => ({}),
|
||||
initalQuery: () => ({}),
|
||||
browser: () => ({}),
|
||||
chat: () => ({}),
|
||||
code: () => ({}),
|
||||
cmd: () => ({}),
|
||||
agent: () => ({}),
|
||||
jupyter: jupyterReducer,
|
||||
securityAnalyzer: () => ({}),
|
||||
status: () => ({}),
|
||||
},
|
||||
preloadedState: {
|
||||
jupyter: {
|
||||
cells: Array(20).fill({
|
||||
content: "Test cell content",
|
||||
type: "input",
|
||||
output: "Test output",
|
||||
}),
|
||||
},
|
||||
},
|
||||
beforeEach(() => {
|
||||
// Reset the Zustand store before each test
|
||||
useJupyterStore.setState({
|
||||
cells: Array(20).fill({
|
||||
content: "Test cell content",
|
||||
type: "input",
|
||||
imageUrls: undefined,
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
it("should have a scrollable container", () => {
|
||||
// Mock agent store to return RUNNING state (not in RUNTIME_INACTIVE_STATES)
|
||||
vi.mocked(useAgentStore).mockReturnValue({
|
||||
curAgentState: AgentState.RUNNING,
|
||||
setCurrentAgentState: vi.fn(),
|
||||
reset: vi.fn(),
|
||||
});
|
||||
|
||||
render(
|
||||
<Provider store={mockStore}>
|
||||
<div style={{ height: "100vh" }}>
|
||||
<JupyterEditor maxWidth={800} />
|
||||
</div>
|
||||
</Provider>
|
||||
<div style={{ height: "100vh" }}>
|
||||
<JupyterEditor maxWidth={800} />
|
||||
</div>,
|
||||
);
|
||||
|
||||
const container = screen.getByTestId("jupyter-container");
|
||||
|
||||
@@ -5,19 +5,17 @@ import { renderWithProviders } from "test-utils";
|
||||
import { MicroagentsModal } from "#/components/features/conversation-panel/microagents-modal";
|
||||
import ConversationService from "#/api/conversation-service/conversation-service.api";
|
||||
import { AgentState } from "#/types/agent-state";
|
||||
import { useAgentStore } from "#/stores/agent-store";
|
||||
|
||||
vi.mock("react-redux", async () => {
|
||||
const actual = await vi.importActual("react-redux");
|
||||
return {
|
||||
...actual,
|
||||
useDispatch: () => vi.fn(),
|
||||
useSelector: () => ({
|
||||
agent: {
|
||||
curAgentState: AgentState.AWAITING_USER_INPUT,
|
||||
},
|
||||
}),
|
||||
};
|
||||
});
|
||||
// Mock the agent store
|
||||
vi.mock("#/stores/agent-store", () => ({
|
||||
useAgentStore: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock the conversation ID hook
|
||||
vi.mock("#/hooks/use-conversation-id", () => ({
|
||||
useConversationId: () => ({ conversationId: "test-conversation-id" }),
|
||||
}));
|
||||
|
||||
describe("MicroagentsModal - Refresh Button", () => {
|
||||
const mockOnClose = vi.fn();
|
||||
@@ -47,10 +45,17 @@ describe("MicroagentsModal - Refresh Button", () => {
|
||||
// Reset all mocks before each test
|
||||
vi.clearAllMocks();
|
||||
|
||||
// Setup default mock for getUserConversations
|
||||
// Setup default mock for getMicroagents
|
||||
vi.spyOn(ConversationService, "getMicroagents").mockResolvedValue({
|
||||
microagents: mockMicroagents,
|
||||
});
|
||||
|
||||
// Mock the agent store to return a ready state
|
||||
vi.mocked(useAgentStore).mockReturnValue({
|
||||
curAgentState: AgentState.AWAITING_USER_INPUT,
|
||||
setCurrentAgentState: vi.fn(),
|
||||
reset: vi.fn(),
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -58,10 +63,11 @@ describe("MicroagentsModal - Refresh Button", () => {
|
||||
});
|
||||
|
||||
describe("Refresh Button Rendering", () => {
|
||||
it("should render the refresh button with correct text and test ID", () => {
|
||||
it("should render the refresh button with correct text and test ID", async () => {
|
||||
renderWithProviders(<MicroagentsModal {...defaultProps} />);
|
||||
|
||||
const refreshButton = screen.getByTestId("refresh-microagents");
|
||||
// Wait for the component to load and render the refresh button
|
||||
const refreshButton = await screen.findByTestId("refresh-microagents");
|
||||
expect(refreshButton).toBeInTheDocument();
|
||||
expect(refreshButton).toHaveTextContent("BUTTON$REFRESH");
|
||||
});
|
||||
@@ -75,7 +81,8 @@ describe("MicroagentsModal - Refresh Button", () => {
|
||||
|
||||
const refreshSpy = vi.spyOn(ConversationService, "getMicroagents");
|
||||
|
||||
const refreshButton = screen.getByTestId("refresh-microagents");
|
||||
// Wait for the component to load and render the refresh button
|
||||
const refreshButton = await screen.findByTestId("refresh-microagents");
|
||||
await user.click(refreshButton);
|
||||
|
||||
expect(refreshSpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useTerminal } from "#/hooks/use-terminal";
|
||||
import { Command, useCommandStore } from "#/state/command-store";
|
||||
import { AgentState } from "#/types/agent-state";
|
||||
import { renderWithProviders } from "../../test-utils";
|
||||
import { useAgentStore } from "#/stores/agent-store";
|
||||
|
||||
// Mock the WsClient context
|
||||
vi.mock("#/context/ws-client-provider", () => ({
|
||||
@@ -22,6 +23,8 @@ interface TestTerminalComponentProps {
|
||||
function TestTerminalComponent({ commands }: TestTerminalComponentProps) {
|
||||
// Set commands in Zustand store
|
||||
useCommandStore.setState({ commands });
|
||||
// Set agent state in Zustand store
|
||||
useAgentStore.setState({ curAgentState: AgentState.RUNNING });
|
||||
const ref = useTerminal();
|
||||
return <div ref={ref} />;
|
||||
}
|
||||
@@ -57,11 +60,7 @@ describe("useTerminal", () => {
|
||||
});
|
||||
|
||||
it("should render", () => {
|
||||
renderWithProviders(<TestTerminalComponent commands={[]} />, {
|
||||
preloadedState: {
|
||||
agent: { curAgentState: AgentState.RUNNING },
|
||||
},
|
||||
});
|
||||
renderWithProviders(<TestTerminalComponent commands={[]} />);
|
||||
});
|
||||
|
||||
it("should render the commands in the terminal", () => {
|
||||
@@ -70,11 +69,7 @@ describe("useTerminal", () => {
|
||||
{ content: "hello", type: "output" },
|
||||
];
|
||||
|
||||
renderWithProviders(<TestTerminalComponent commands={commands} />, {
|
||||
preloadedState: {
|
||||
agent: { curAgentState: AgentState.RUNNING },
|
||||
},
|
||||
});
|
||||
renderWithProviders(<TestTerminalComponent commands={commands} />);
|
||||
|
||||
expect(mockTerminal.writeln).toHaveBeenNthCalledWith(1, "echo hello");
|
||||
expect(mockTerminal.writeln).toHaveBeenNthCalledWith(2, "hello");
|
||||
@@ -92,11 +87,7 @@ describe("useTerminal", () => {
|
||||
{ content: secret, type: "output" },
|
||||
];
|
||||
|
||||
renderWithProviders(<TestTerminalComponent commands={commands} />, {
|
||||
preloadedState: {
|
||||
agent: { curAgentState: AgentState.RUNNING },
|
||||
},
|
||||
});
|
||||
renderWithProviders(<TestTerminalComponent commands={commands} />);
|
||||
|
||||
// This test is no longer relevant as secrets filtering has been removed
|
||||
});
|
||||
|
||||
@@ -3,8 +3,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { QueryClientProvider, QueryClient } from "@tanstack/react-query";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { createRoutesStub } from "react-router";
|
||||
import { Provider } from "react-redux";
|
||||
import { createAxiosNotFoundErrorObject, setupStore } from "test-utils";
|
||||
import { createAxiosNotFoundErrorObject } from "test-utils";
|
||||
import HomeScreen from "#/routes/home";
|
||||
import { GitRepository } from "#/types/git";
|
||||
import SettingsService from "#/settings-service/settings-service.api";
|
||||
@@ -66,11 +65,9 @@ const selectRepository = async (repoName: string) => {
|
||||
const renderHomeScreen = () =>
|
||||
render(<RouterStub />, {
|
||||
wrapper: ({ children }) => (
|
||||
<Provider store={setupStore()}>
|
||||
<QueryClientProvider client={new QueryClient()}>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
</Provider>
|
||||
<QueryClientProvider client={new QueryClient()}>
|
||||
{children}
|
||||
</QueryClientProvider>
|
||||
),
|
||||
});
|
||||
|
||||
|
||||
@@ -133,7 +133,7 @@ describe("Settings Screen", () => {
|
||||
"user",
|
||||
"integrations",
|
||||
"application",
|
||||
"billing", // The nav item shows "billing" text and routes to /billing
|
||||
"billing", // The nav item shows "Billing" text and routes to /billing
|
||||
"secrets",
|
||||
"api keys",
|
||||
];
|
||||
|
||||
@@ -21,8 +21,12 @@ vi.mock("#/state/command-store", () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("#/state/jupyter-slice", () => ({
|
||||
appendJupyterInput: mockAppendJupyterInput,
|
||||
vi.mock("#/state/jupyter-store", () => ({
|
||||
useJupyterStore: {
|
||||
getState: () => ({
|
||||
appendJupyterInput: mockAppendJupyterInput,
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("#/state/metrics-slice", () => ({
|
||||
@@ -81,8 +85,8 @@ describe("handleActionMessage", () => {
|
||||
handleActionMessage(ipythonAction);
|
||||
|
||||
// Check that appendJupyterInput was called with the code
|
||||
expect(mockDispatch).toHaveBeenCalledWith(
|
||||
mockAppendJupyterInput("print('Hello from Jupyter!')"),
|
||||
expect(mockAppendJupyterInput).toHaveBeenCalledWith(
|
||||
"print('Hello from Jupyter!')",
|
||||
);
|
||||
expect(mockAppendInput).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -60,13 +60,7 @@ describe("Check for hardcoded English strings", () => {
|
||||
test("InteractiveChatBox should not have hardcoded English strings", () => {
|
||||
const { container } = renderWithProviders(
|
||||
<MemoryRouter>
|
||||
<InteractiveChatBox
|
||||
onSubmit={() => {}}
|
||||
onStop={() => {}}
|
||||
isWaitingForUserInput={false}
|
||||
hasSubstantiveAgentActions={false}
|
||||
optimisticUserMessage={false}
|
||||
/>
|
||||
<InteractiveChatBox onSubmit={() => {}} onStop={() => {}} />
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
|
||||
1853
frontend/package-lock.json
generated
@@ -1,54 +1,51 @@
|
||||
{
|
||||
"name": "openhands-frontend",
|
||||
"version": "0.57.0",
|
||||
"version": "0.58.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": ">=22.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@heroui/react": "^2.8.3",
|
||||
"@heroui/react": "^2.8.4",
|
||||
"@heroui/use-infinite-scroll": "^2.2.11",
|
||||
"@microlink/react-json-view": "^1.26.2",
|
||||
"@monaco-editor/react": "^4.7.0-rc.0",
|
||||
"@react-router/node": "^7.8.2",
|
||||
"@react-router/serve": "^7.8.2",
|
||||
"@react-router/node": "^7.9.3",
|
||||
"@react-router/serve": "^7.9.3",
|
||||
"@react-types/shared": "^3.32.0",
|
||||
"@reduxjs/toolkit": "^2.9.0",
|
||||
"@stripe/react-stripe-js": "^4.0.0",
|
||||
"@stripe/react-stripe-js": "^4.0.2",
|
||||
"@stripe/stripe-js": "^7.9.0",
|
||||
"@tailwindcss/postcss": "^4.1.13",
|
||||
"@tailwindcss/vite": "^4.1.13",
|
||||
"@tanstack/react-query": "^5.87.0",
|
||||
"@tanstack/react-query": "^5.90.2",
|
||||
"@uidotdev/usehooks": "^2.4.1",
|
||||
"@vitejs/plugin-react": "^5.0.2",
|
||||
"@vitejs/plugin-react": "^5.0.4",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/xterm": "^5.4.0",
|
||||
"axios": "^1.11.0",
|
||||
"axios": "^1.12.2",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"downshift": "^9.0.10",
|
||||
"eslint-config-airbnb-typescript": "^18.0.0",
|
||||
"framer-motion": "^12.23.12",
|
||||
"framer-motion": "^12.23.22",
|
||||
"i18next": "^25.5.2",
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"i18next-http-backend": "^3.0.2",
|
||||
"isbot": "^5.1.30",
|
||||
"isbot": "^5.1.31",
|
||||
"jose": "^6.1.0",
|
||||
"lucide-react": "^0.542.0",
|
||||
"monaco-editor": "^0.52.2",
|
||||
"posthog-js": "^1.261.7",
|
||||
"lucide-react": "^0.544.0",
|
||||
"monaco-editor": "^0.53.0",
|
||||
"posthog-js": "^1.268.8",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-highlight": "^0.15.0",
|
||||
"react-hot-toast": "^2.6.0",
|
||||
"react-i18next": "^15.7.2",
|
||||
"react-i18next": "^16.0.0",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-redux": "^9.2.0",
|
||||
"react-resizable-panels": "^3.0.5",
|
||||
"react-router": "^7.8.2",
|
||||
"react-router": "^7.9.3",
|
||||
"react-syntax-highlighter": "^15.6.6",
|
||||
"remark-breaks": "^4.0.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
@@ -56,7 +53,7 @@
|
||||
"socket.io-client": "^4.8.1",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tailwind-scrollbar": "^4.0.2",
|
||||
"vite": "^7.1.4",
|
||||
"vite": "^7.1.7",
|
||||
"web-vitals": "^5.1.0",
|
||||
"ws": "^8.18.2",
|
||||
"zustand": "^5.0.8"
|
||||
@@ -98,16 +95,16 @@
|
||||
"@babel/traverse": "^7.28.3",
|
||||
"@babel/types": "^7.28.2",
|
||||
"@mswjs/socket.io-binding": "^0.2.0",
|
||||
"@playwright/test": "^1.55.0",
|
||||
"@react-router/dev": "^7.8.2",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@tanstack/eslint-plugin-query": "^5.86.0",
|
||||
"@playwright/test": "^1.55.1",
|
||||
"@react-router/dev": "^7.9.3",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"@tanstack/eslint-plugin-query": "^5.90.1",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/jest-dom": "^6.8.0",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/node": "^24.3.1",
|
||||
"@types/react": "^19.1.12",
|
||||
"@types/node": "^24.5.2",
|
||||
"@types/react": "^19.1.15",
|
||||
"@types/react-dom": "^19.1.9",
|
||||
"@types/react-highlight": "^0.12.8",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
@@ -129,8 +126,8 @@
|
||||
"eslint-plugin-react-hooks": "^4.6.2",
|
||||
"eslint-plugin-unused-imports": "^4.2.0",
|
||||
"husky": "^9.1.7",
|
||||
"jsdom": "^26.1.0",
|
||||
"lint-staged": "^16.1.6",
|
||||
"jsdom": "^27.0.0",
|
||||
"lint-staged": "^16.2.3",
|
||||
"msw": "^2.6.6",
|
||||
"prettier": "^3.6.2",
|
||||
"stripe": "^18.5.0",
|
||||
@@ -142,7 +139,7 @@
|
||||
},
|
||||
"packageManager": "npm@10.5.0",
|
||||
"volta": {
|
||||
"node": "18.20.1"
|
||||
"node": "22.0.0"
|
||||
},
|
||||
"msw": {
|
||||
"workerDirectory": [
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* - Please do NOT modify this file.
|
||||
*/
|
||||
|
||||
const PACKAGE_VERSION = '2.10.5'
|
||||
const PACKAGE_VERSION = '2.11.1'
|
||||
const INTEGRITY_CHECKSUM = 'f5825c521429caf22a4dd13b66e243af'
|
||||
const IS_MOCKED_RESPONSE = Symbol('isMockedResponse')
|
||||
const activeClientIds = new Set()
|
||||
|
||||
@@ -23,7 +23,7 @@ class GitService {
|
||||
*/
|
||||
static async searchGitRepositories(
|
||||
query: string,
|
||||
per_page = 5,
|
||||
per_page = 100,
|
||||
selected_provider?: Provider,
|
||||
): Promise<GitRepository[]> {
|
||||
const response = await openHands.get<GitRepository[]>(
|
||||
|
||||
@@ -1,26 +1,16 @@
|
||||
import { useEffect } from "react";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import { RootState } from "#/store";
|
||||
import { BrowserSnapshot } from "./browser-snapshot";
|
||||
import { EmptyBrowserMessage } from "./empty-browser-message";
|
||||
import { useConversationId } from "#/hooks/use-conversation-id";
|
||||
import {
|
||||
initialState as browserInitialState,
|
||||
setUrl,
|
||||
setScreenshotSrc,
|
||||
} from "#/state/browser-slice";
|
||||
import { useBrowserStore } from "#/stores/browser-store";
|
||||
|
||||
export function BrowserPanel() {
|
||||
const { url, screenshotSrc } = useSelector(
|
||||
(state: RootState) => state.browser,
|
||||
);
|
||||
const { url, screenshotSrc, reset } = useBrowserStore();
|
||||
const { conversationId } = useConversationId();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(setUrl(browserInitialState.url));
|
||||
dispatch(setScreenshotSrc(browserInitialState.screenshotSrc));
|
||||
}, [conversationId]);
|
||||
reset();
|
||||
}, [conversationId, reset]);
|
||||
|
||||
const imgSrc =
|
||||
screenshotSrc && screenshotSrc.startsWith("data:image/png;base64,")
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import React from "react";
|
||||
import posthog from "posthog-js";
|
||||
import { useParams } from "react-router";
|
||||
@@ -7,7 +6,6 @@ import { convertImageToBase64 } from "#/utils/convert-image-to-base-64";
|
||||
import { TrajectoryActions } from "../trajectory/trajectory-actions";
|
||||
import { createChatMessage } from "#/services/chat-service";
|
||||
import { InteractiveChatBox } from "./interactive-chat-box";
|
||||
import { RootState } from "#/store";
|
||||
import { AgentState } from "#/types/agent-state";
|
||||
import { isOpenHandsAction } from "#/types/core/guards";
|
||||
import { generateAgentStateChangeEvent } from "#/services/agent-state-service";
|
||||
@@ -19,12 +17,13 @@ import { Messages } from "./messages";
|
||||
import { ChatSuggestions } from "./chat-suggestions";
|
||||
import { ScrollProvider } from "#/context/scroll-context";
|
||||
import { useInitialQueryStore } from "#/stores/initial-query-store";
|
||||
import { useAgentStore } from "#/stores/agent-store";
|
||||
|
||||
import { ScrollToBottomButton } from "#/components/shared/buttons/scroll-to-bottom-button";
|
||||
import { LoadingSpinner } from "#/components/shared/loading-spinner";
|
||||
import { displayErrorToast } from "#/utils/custom-toast-handlers";
|
||||
import { useOptimisticUserMessage } from "#/hooks/use-optimistic-user-message";
|
||||
import { useWSErrorMessage } from "#/hooks/use-ws-error-message";
|
||||
import { useErrorMessageStore } from "#/stores/error-message-store";
|
||||
import { useOptimisticUserMessageStore } from "#/stores/optimistic-user-message-store";
|
||||
import { ErrorMessageBanner } from "./error-message-banner";
|
||||
import {
|
||||
hasUserEvent,
|
||||
@@ -33,7 +32,7 @@ import {
|
||||
import { useUploadFiles } from "#/hooks/mutation/use-upload-files";
|
||||
import { useConfig } from "#/hooks/query/use-config";
|
||||
import { validateFiles } from "#/utils/file-validation";
|
||||
import { setMessageToSend } from "#/state/conversation-slice";
|
||||
import { useConversationStore } from "#/state/conversation-store";
|
||||
import ConfirmationModeEnabled from "./confirmation-mode-enabled";
|
||||
|
||||
function getEntryPoint(
|
||||
@@ -46,11 +45,11 @@ function getEntryPoint(
|
||||
}
|
||||
|
||||
export function ChatInterface() {
|
||||
const dispatch = useDispatch();
|
||||
const { getErrorMessage } = useWSErrorMessage();
|
||||
const { setMessageToSend } = useConversationStore();
|
||||
const { errorMessage } = useErrorMessageStore();
|
||||
const { send, isLoadingMessages, parsedEvents } = useWsClient();
|
||||
const { setOptimisticUserMessage, getOptimisticUserMessage } =
|
||||
useOptimisticUserMessage();
|
||||
useOptimisticUserMessageStore();
|
||||
const { t } = useTranslation();
|
||||
const scrollRef = React.useRef<HTMLDivElement>(null);
|
||||
const {
|
||||
@@ -63,7 +62,7 @@ export function ChatInterface() {
|
||||
} = useScrollToBottom(scrollRef);
|
||||
const { data: config } = useConfig();
|
||||
|
||||
const { curAgentState } = useSelector((state: RootState) => state.agent);
|
||||
const { curAgentState } = useAgentStore();
|
||||
|
||||
const [feedbackPolarity, setFeedbackPolarity] = React.useState<
|
||||
"positive" | "negative"
|
||||
@@ -74,7 +73,6 @@ export function ChatInterface() {
|
||||
const { mutateAsync: uploadFiles } = useUploadFiles();
|
||||
|
||||
const optimisticUserMessage = getOptimisticUserMessage();
|
||||
const errorMessage = getErrorMessage();
|
||||
|
||||
const events = parsedEvents.filter(shouldRenderEvent);
|
||||
|
||||
@@ -141,7 +139,7 @@ export function ChatInterface() {
|
||||
|
||||
send(createChatMessage(prompt, imageUrls, uploadedFiles, timestamp));
|
||||
setOptimisticUserMessage(content);
|
||||
dispatch(setMessageToSend(null));
|
||||
setMessageToSend("");
|
||||
};
|
||||
|
||||
const handleStop = () => {
|
||||
@@ -156,10 +154,6 @@ export function ChatInterface() {
|
||||
setFeedbackPolarity(polarity);
|
||||
};
|
||||
|
||||
const isWaitingForUserInput =
|
||||
curAgentState === AgentState.AWAITING_USER_INPUT ||
|
||||
curAgentState === AgentState.FINISHED;
|
||||
|
||||
// Create a ScrollProvider with the scroll hook values
|
||||
const scrollProviderValue = {
|
||||
scrollRef,
|
||||
@@ -180,9 +174,7 @@ export function ChatInterface() {
|
||||
!optimisticUserMessage &&
|
||||
!userEventsExist && (
|
||||
<ChatSuggestions
|
||||
onSuggestionsClick={(message) =>
|
||||
dispatch(setMessageToSend(message))
|
||||
}
|
||||
onSuggestionsClick={(message) => setMessageToSend(message)}
|
||||
/>
|
||||
)}
|
||||
{/* Note: We only hide chat suggestions when there's a user message */}
|
||||
@@ -237,9 +229,6 @@ export function ChatInterface() {
|
||||
<InteractiveChatBox
|
||||
onSubmit={handleSendMessage}
|
||||
onStop={handleStop}
|
||||
isWaitingForUserInput={isWaitingForUserInput}
|
||||
hasSubstantiveAgentActions={hasSubstantiveAgentActions}
|
||||
optimisticUserMessage={!!optimisticUserMessage}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -55,10 +55,10 @@ export function ChatMessage({
|
||||
onMouseEnter={() => setIsHovering(true)}
|
||||
onMouseLeave={() => setIsHovering(false)}
|
||||
className={cn(
|
||||
"rounded-xl relative w-fit max-w-full",
|
||||
"rounded-xl relative w-fit max-w-full last:mb-4",
|
||||
"flex flex-col gap-2",
|
||||
type === "user" && " p-4 bg-tertiary self-end",
|
||||
type === "agent" && "mt-6 max-w-full bg-transparent",
|
||||
type === "agent" && "mt-6 w-full max-w-full bg-transparent",
|
||||
)}
|
||||
>
|
||||
<div
|
||||
|
||||
@@ -6,7 +6,12 @@ export interface ChatStopButtonProps {
|
||||
|
||||
export function ChatStopButton({ handleStop }: ChatStopButtonProps) {
|
||||
return (
|
||||
<button type="button" onClick={handleStop} data-testid="stop-button">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleStop}
|
||||
data-testid="stop-button"
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<PauseIcon className="block max-w-none w-4 h-4" />
|
||||
</button>
|
||||
);
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSelector } from "react-redux";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { Suggestions } from "#/components/features/suggestions/suggestions";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import BuildIt from "#/icons/build-it.svg?react";
|
||||
import { SUGGESTIONS } from "#/utils/suggestions";
|
||||
import { RootState } from "#/store";
|
||||
import { useConversationStore } from "#/state/conversation-store";
|
||||
|
||||
interface ChatSuggestionsProps {
|
||||
onSuggestionsClick: (value: string) => void;
|
||||
@@ -13,9 +12,7 @@ interface ChatSuggestionsProps {
|
||||
|
||||
export function ChatSuggestions({ onSuggestionsClick }: ChatSuggestionsProps) {
|
||||
const { t } = useTranslation();
|
||||
const shouldHideSuggestions = useSelector(
|
||||
(state: RootState) => state.conversation.shouldHideSuggestions,
|
||||
);
|
||||
const { shouldHideSuggestions } = useConversationStore();
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { ConversationStatus } from "#/types/conversation-status";
|
||||
import {
|
||||
clearAllFiles,
|
||||
setShouldHideSuggestions,
|
||||
setSubmittedMessage,
|
||||
} from "#/state/conversation-slice";
|
||||
import { RootState } from "#/store";
|
||||
import { useChatInputLogic } from "#/hooks/chat/use-chat-input-logic";
|
||||
import { useFileHandling } from "#/hooks/chat/use-file-handling";
|
||||
import { useGripResize } from "#/hooks/chat/use-grip-resize";
|
||||
@@ -15,6 +8,7 @@ import { useChatSubmission } from "#/hooks/chat/use-chat-submission";
|
||||
import { ChatInputGrip } from "./components/chat-input-grip";
|
||||
import { ChatInputContainer } from "./components/chat-input-container";
|
||||
import { HiddenFileInput } from "./components/hidden-file-input";
|
||||
import { useConversationStore } from "#/state/conversation-store";
|
||||
|
||||
export interface CustomChatInputProps {
|
||||
disabled?: boolean;
|
||||
@@ -41,10 +35,12 @@ export function CustomChatInput({
|
||||
className = "",
|
||||
buttonClassName = "",
|
||||
}: CustomChatInputProps) {
|
||||
const { submittedMessage } = useSelector(
|
||||
(state: RootState) => state.conversation,
|
||||
);
|
||||
const dispatch = useDispatch();
|
||||
const {
|
||||
submittedMessage,
|
||||
clearAllFiles,
|
||||
setShouldHideSuggestions,
|
||||
setSubmittedMessage,
|
||||
} = useConversationStore();
|
||||
|
||||
// Disable input when conversation is stopped
|
||||
const isConversationStopped = conversationStatus === "STOPPED";
|
||||
@@ -56,8 +52,8 @@ export function CustomChatInput({
|
||||
return;
|
||||
}
|
||||
onSubmit(submittedMessage);
|
||||
dispatch(setSubmittedMessage(null));
|
||||
}, [submittedMessage, disabled, onSubmit, dispatch]);
|
||||
setSubmittedMessage(null);
|
||||
}, [submittedMessage, disabled, onSubmit, setSubmittedMessage]);
|
||||
|
||||
// Custom hooks
|
||||
const {
|
||||
@@ -112,10 +108,10 @@ export function CustomChatInput({
|
||||
// Cleanup: reset suggestions visibility when component unmounts
|
||||
useEffect(
|
||||
() => () => {
|
||||
dispatch(setShouldHideSuggestions(false));
|
||||
dispatch(clearAllFiles());
|
||||
setShouldHideSuggestions(false);
|
||||
clearAllFiles();
|
||||
},
|
||||
[dispatch],
|
||||
[setShouldHideSuggestions, clearAllFiles],
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ActionSecurityRisk } from "#/state/security-analyzer-slice";
|
||||
import { ActionSecurityRisk } from "#/stores/security-analyzer-store";
|
||||
import {
|
||||
FileWriteAction,
|
||||
CommandAction,
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import React from "react";
|
||||
import { OpenHandsObservation } from "#/types/core/observations";
|
||||
import { isErrorObservation } from "#/types/core/guards";
|
||||
import { ErrorMessage } from "../error-message";
|
||||
import { MicroagentStatusWrapper } from "./microagent-status-wrapper";
|
||||
import { LikertScaleWrapper } from "./likert-scale-wrapper";
|
||||
import { MicroagentStatus } from "#/types/microagent-status";
|
||||
|
||||
interface ErrorEventMessageProps {
|
||||
event: OpenHandsObservation;
|
||||
microagentStatus?: MicroagentStatus | null;
|
||||
microagentConversationId?: string;
|
||||
microagentPRUrl?: string;
|
||||
actions?: Array<{
|
||||
icon: React.ReactNode;
|
||||
onClick: () => void;
|
||||
tooltip?: string;
|
||||
}>;
|
||||
isLastMessage: boolean;
|
||||
isInLast10Actions: boolean;
|
||||
config?: { APP_MODE?: string } | null;
|
||||
isCheckingFeedback: boolean;
|
||||
feedbackData: {
|
||||
exists: boolean;
|
||||
rating?: number;
|
||||
reason?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export function ErrorEventMessage({
|
||||
event,
|
||||
microagentStatus,
|
||||
microagentConversationId,
|
||||
microagentPRUrl,
|
||||
actions,
|
||||
isLastMessage,
|
||||
isInLast10Actions,
|
||||
config,
|
||||
isCheckingFeedback,
|
||||
feedbackData,
|
||||
}: ErrorEventMessageProps) {
|
||||
if (!isErrorObservation(event)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ErrorMessage
|
||||
errorId={event.extras.error_id}
|
||||
defaultMessage={event.message}
|
||||
/>
|
||||
<MicroagentStatusWrapper
|
||||
microagentStatus={microagentStatus}
|
||||
microagentConversationId={microagentConversationId}
|
||||
microagentPRUrl={microagentPRUrl}
|
||||
actions={actions}
|
||||
/>
|
||||
<LikertScaleWrapper
|
||||
event={event}
|
||||
isLastMessage={isLastMessage}
|
||||
isInLast10Actions={isInLast10Actions}
|
||||
config={config}
|
||||
isCheckingFeedback={isCheckingFeedback}
|
||||
feedbackData={feedbackData}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import React from "react";
|
||||
import { OpenHandsAction } from "#/types/core/actions";
|
||||
import { isFinishAction } from "#/types/core/guards";
|
||||
import { ChatMessage } from "../chat-message";
|
||||
import { MicroagentStatusWrapper } from "./microagent-status-wrapper";
|
||||
import { LikertScaleWrapper } from "./likert-scale-wrapper";
|
||||
import { getEventContent } from "../event-content-helpers/get-event-content";
|
||||
import { MicroagentStatus } from "#/types/microagent-status";
|
||||
|
||||
interface FinishEventMessageProps {
|
||||
event: OpenHandsAction;
|
||||
microagentStatus?: MicroagentStatus | null;
|
||||
microagentConversationId?: string;
|
||||
microagentPRUrl?: string;
|
||||
actions?: Array<{
|
||||
icon: React.ReactNode;
|
||||
onClick: () => void;
|
||||
tooltip?: string;
|
||||
}>;
|
||||
isLastMessage: boolean;
|
||||
isInLast10Actions: boolean;
|
||||
config?: { APP_MODE?: string } | null;
|
||||
isCheckingFeedback: boolean;
|
||||
feedbackData: {
|
||||
exists: boolean;
|
||||
rating?: number;
|
||||
reason?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export function FinishEventMessage({
|
||||
event,
|
||||
microagentStatus,
|
||||
microagentConversationId,
|
||||
microagentPRUrl,
|
||||
actions,
|
||||
isLastMessage,
|
||||
isInLast10Actions,
|
||||
config,
|
||||
isCheckingFeedback,
|
||||
feedbackData,
|
||||
}: FinishEventMessageProps) {
|
||||
if (!isFinishAction(event)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ChatMessage
|
||||
type="agent"
|
||||
message={getEventContent(event).details}
|
||||
actions={actions}
|
||||
/>
|
||||
<MicroagentStatusWrapper
|
||||
microagentStatus={microagentStatus}
|
||||
microagentConversationId={microagentConversationId}
|
||||
microagentPRUrl={microagentPRUrl}
|
||||
actions={actions}
|
||||
/>
|
||||
<LikertScaleWrapper
|
||||
event={event}
|
||||
isLastMessage={isLastMessage}
|
||||
isInLast10Actions={isInLast10Actions}
|
||||
config={config}
|
||||
isCheckingFeedback={isCheckingFeedback}
|
||||
feedbackData={feedbackData}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import React from "react";
|
||||
import { OpenHandsAction } from "#/types/core/actions";
|
||||
import { OpenHandsObservation } from "#/types/core/observations";
|
||||
import { isOpenHandsAction, isOpenHandsObservation } from "#/types/core/guards";
|
||||
import { ChatMessage } from "../chat-message";
|
||||
import { GenericEventMessage } from "../generic-event-message";
|
||||
import { ConfirmationButtons } from "#/components/shared/buttons/confirmation-buttons";
|
||||
import { getEventContent } from "../event-content-helpers/get-event-content";
|
||||
import { getObservationResult } from "../event-content-helpers/get-observation-result";
|
||||
|
||||
const hasThoughtProperty = (
|
||||
obj: Record<string, unknown>,
|
||||
): obj is { thought: string } => "thought" in obj && !!obj.thought;
|
||||
|
||||
interface GenericEventMessageWrapperProps {
|
||||
event: OpenHandsAction | OpenHandsObservation;
|
||||
shouldShowConfirmationButtons: boolean;
|
||||
}
|
||||
|
||||
export function GenericEventMessageWrapper({
|
||||
event,
|
||||
shouldShowConfirmationButtons,
|
||||
}: GenericEventMessageWrapperProps) {
|
||||
return (
|
||||
<div>
|
||||
{isOpenHandsAction(event) &&
|
||||
hasThoughtProperty(event.args) &&
|
||||
event.action !== "think" && (
|
||||
<ChatMessage type="agent" message={event.args.thought} />
|
||||
)}
|
||||
|
||||
<GenericEventMessage
|
||||
title={getEventContent(event).title}
|
||||
details={getEventContent(event).details}
|
||||
success={
|
||||
isOpenHandsObservation(event)
|
||||
? getObservationResult(event)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
|
||||
{shouldShowConfirmationButtons && <ConfirmationButtons />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export { ErrorEventMessage } from "./error-event-message";
|
||||
export { UserAssistantEventMessage } from "./user-assistant-event-message";
|
||||
export { FinishEventMessage } from "./finish-event-message";
|
||||
export { RejectEventMessage } from "./reject-event-message";
|
||||
export { McpEventMessage } from "./mcp-event-message";
|
||||
export { TaskTrackingEventMessage } from "./task-tracking-event-message";
|
||||
export { ObservationPairEventMessage } from "./observation-pair-event-message";
|
||||
export { GenericEventMessageWrapper } from "./generic-event-message-wrapper";
|
||||
export { MicroagentStatusWrapper } from "./microagent-status-wrapper";
|
||||
export { LikertScaleWrapper } from "./likert-scale-wrapper";
|
||||
@@ -0,0 +1,50 @@
|
||||
import React from "react";
|
||||
import { OpenHandsAction } from "#/types/core/actions";
|
||||
import { OpenHandsObservation } from "#/types/core/observations";
|
||||
import { isErrorObservation } from "#/types/core/guards";
|
||||
import { LikertScale } from "../../feedback/likert-scale";
|
||||
|
||||
interface LikertScaleWrapperProps {
|
||||
event: OpenHandsAction | OpenHandsObservation;
|
||||
isLastMessage: boolean;
|
||||
isInLast10Actions: boolean;
|
||||
config?: { APP_MODE?: string } | null;
|
||||
isCheckingFeedback: boolean;
|
||||
feedbackData: {
|
||||
exists: boolean;
|
||||
rating?: number;
|
||||
reason?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export function LikertScaleWrapper({
|
||||
event,
|
||||
isLastMessage,
|
||||
isInLast10Actions,
|
||||
config,
|
||||
isCheckingFeedback,
|
||||
feedbackData,
|
||||
}: LikertScaleWrapperProps) {
|
||||
if (config?.APP_MODE !== "saas" || isCheckingFeedback) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// For error observations, show if in last 10 actions
|
||||
// For other events, show only if it's the last message
|
||||
const shouldShow = isErrorObservation(event)
|
||||
? isInLast10Actions
|
||||
: isLastMessage;
|
||||
|
||||
if (!shouldShow) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<LikertScale
|
||||
eventId={event.id}
|
||||
initiallySubmitted={feedbackData.exists}
|
||||
initialRating={feedbackData.rating}
|
||||
initialReason={feedbackData.reason}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import React from "react";
|
||||
import { OpenHandsObservation } from "#/types/core/observations";
|
||||
import { isMcpObservation } from "#/types/core/guards";
|
||||
import { GenericEventMessage } from "../generic-event-message";
|
||||
import { MCPObservationContent } from "../mcp-observation-content";
|
||||
import { ConfirmationButtons } from "#/components/shared/buttons/confirmation-buttons";
|
||||
import { getEventContent } from "../event-content-helpers/get-event-content";
|
||||
import { getObservationResult } from "../event-content-helpers/get-observation-result";
|
||||
|
||||
interface McpEventMessageProps {
|
||||
event: OpenHandsObservation;
|
||||
shouldShowConfirmationButtons: boolean;
|
||||
}
|
||||
|
||||
export function McpEventMessage({
|
||||
event,
|
||||
shouldShowConfirmationButtons,
|
||||
}: McpEventMessageProps) {
|
||||
if (!isMcpObservation(event)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<GenericEventMessage
|
||||
title={getEventContent(event).title}
|
||||
details={<MCPObservationContent event={event} />}
|
||||
success={getObservationResult(event)}
|
||||
/>
|
||||
{shouldShowConfirmationButtons && <ConfirmationButtons />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import React from "react";
|
||||
import { MicroagentStatus } from "#/types/microagent-status";
|
||||
import { MicroagentStatusIndicator } from "../microagent/microagent-status-indicator";
|
||||
|
||||
interface MicroagentStatusWrapperProps {
|
||||
microagentStatus?: MicroagentStatus | null;
|
||||
microagentConversationId?: string;
|
||||
microagentPRUrl?: string;
|
||||
actions?: Array<{
|
||||
icon: React.ReactNode;
|
||||
onClick: () => void;
|
||||
tooltip?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export function MicroagentStatusWrapper({
|
||||
microagentStatus,
|
||||
microagentConversationId,
|
||||
microagentPRUrl,
|
||||
actions,
|
||||
}: MicroagentStatusWrapperProps) {
|
||||
if (!microagentStatus || !actions) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<MicroagentStatusIndicator
|
||||
status={microagentStatus}
|
||||
conversationId={microagentConversationId}
|
||||
prUrl={microagentPRUrl}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import React from "react";
|
||||
import { OpenHandsAction } from "#/types/core/actions";
|
||||
import { isOpenHandsAction } from "#/types/core/guards";
|
||||
import { ChatMessage } from "../chat-message";
|
||||
import { MicroagentStatusWrapper } from "./microagent-status-wrapper";
|
||||
import { MicroagentStatus } from "#/types/microagent-status";
|
||||
|
||||
const hasThoughtProperty = (
|
||||
obj: Record<string, unknown>,
|
||||
): obj is { thought: string } => "thought" in obj && !!obj.thought;
|
||||
|
||||
interface ObservationPairEventMessageProps {
|
||||
event: OpenHandsAction;
|
||||
microagentStatus?: MicroagentStatus | null;
|
||||
microagentConversationId?: string;
|
||||
microagentPRUrl?: string;
|
||||
actions?: Array<{
|
||||
icon: React.ReactNode;
|
||||
onClick: () => void;
|
||||
tooltip?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export function ObservationPairEventMessage({
|
||||
event,
|
||||
microagentStatus,
|
||||
microagentConversationId,
|
||||
microagentPRUrl,
|
||||
actions,
|
||||
}: ObservationPairEventMessageProps) {
|
||||
if (!isOpenHandsAction(event)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (hasThoughtProperty(event.args) && event.action !== "think") {
|
||||
return (
|
||||
<div>
|
||||
<ChatMessage
|
||||
type="agent"
|
||||
message={event.args.thought}
|
||||
actions={actions}
|
||||
/>
|
||||
<MicroagentStatusWrapper
|
||||
microagentStatus={microagentStatus}
|
||||
microagentConversationId={microagentConversationId}
|
||||
microagentPRUrl={microagentPRUrl}
|
||||
actions={actions}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<MicroagentStatusWrapper
|
||||
microagentStatus={microagentStatus}
|
||||
microagentConversationId={microagentConversationId}
|
||||
microagentPRUrl={microagentPRUrl}
|
||||
actions={actions}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import React from "react";
|
||||
import { OpenHandsObservation } from "#/types/core/observations";
|
||||
import { isRejectObservation } from "#/types/core/guards";
|
||||
import { ChatMessage } from "../chat-message";
|
||||
|
||||
interface RejectEventMessageProps {
|
||||
event: OpenHandsObservation;
|
||||
}
|
||||
|
||||
export function RejectEventMessage({ event }: RejectEventMessageProps) {
|
||||
if (!isRejectObservation(event)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ChatMessage type="agent" message={event.content} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { OpenHandsObservation } from "#/types/core/observations";
|
||||
import { isTaskTrackingObservation } from "#/types/core/guards";
|
||||
import { GenericEventMessage } from "../generic-event-message";
|
||||
import { TaskTrackingObservationContent } from "../task-tracking-observation-content";
|
||||
import { ConfirmationButtons } from "#/components/shared/buttons/confirmation-buttons";
|
||||
import { getObservationResult } from "../event-content-helpers/get-observation-result";
|
||||
|
||||
interface TaskTrackingEventMessageProps {
|
||||
event: OpenHandsObservation;
|
||||
shouldShowConfirmationButtons: boolean;
|
||||
}
|
||||
|
||||
export function TaskTrackingEventMessage({
|
||||
event,
|
||||
shouldShowConfirmationButtons,
|
||||
}: TaskTrackingEventMessageProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (!isTaskTrackingObservation(event)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { command } = event.extras;
|
||||
let title: React.ReactNode;
|
||||
let initiallyExpanded = false;
|
||||
|
||||
// Determine title and expansion state based on command
|
||||
if (command === "plan") {
|
||||
title = t("OBSERVATION_MESSAGE$TASK_TRACKING_PLAN");
|
||||
initiallyExpanded = true;
|
||||
} else {
|
||||
// command === "view"
|
||||
title = t("OBSERVATION_MESSAGE$TASK_TRACKING_VIEW");
|
||||
initiallyExpanded = false;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<GenericEventMessage
|
||||
title={title}
|
||||
details={<TaskTrackingObservationContent event={event} />}
|
||||
success={getObservationResult(event)}
|
||||
initiallyExpanded={initiallyExpanded}
|
||||
/>
|
||||
{shouldShowConfirmationButtons && <ConfirmationButtons />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import React from "react";
|
||||
import { OpenHandsAction } from "#/types/core/actions";
|
||||
import { isUserMessage, isAssistantMessage } from "#/types/core/guards";
|
||||
import { ChatMessage } from "../chat-message";
|
||||
import { ImageCarousel } from "../../images/image-carousel";
|
||||
import { FileList } from "../../files/file-list";
|
||||
import { ConfirmationButtons } from "#/components/shared/buttons/confirmation-buttons";
|
||||
import { MicroagentStatusWrapper } from "./microagent-status-wrapper";
|
||||
import { LikertScaleWrapper } from "./likert-scale-wrapper";
|
||||
import { parseMessageFromEvent } from "../event-content-helpers/parse-message-from-event";
|
||||
import { MicroagentStatus } from "#/types/microagent-status";
|
||||
|
||||
interface UserAssistantEventMessageProps {
|
||||
event: OpenHandsAction;
|
||||
shouldShowConfirmationButtons: boolean;
|
||||
microagentStatus?: MicroagentStatus | null;
|
||||
microagentConversationId?: string;
|
||||
microagentPRUrl?: string;
|
||||
actions?: Array<{
|
||||
icon: React.ReactNode;
|
||||
onClick: () => void;
|
||||
tooltip?: string;
|
||||
}>;
|
||||
isLastMessage: boolean;
|
||||
isInLast10Actions: boolean;
|
||||
config?: { APP_MODE?: string } | null;
|
||||
isCheckingFeedback: boolean;
|
||||
feedbackData: {
|
||||
exists: boolean;
|
||||
rating?: number;
|
||||
reason?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export function UserAssistantEventMessage({
|
||||
event,
|
||||
shouldShowConfirmationButtons,
|
||||
microagentStatus,
|
||||
microagentConversationId,
|
||||
microagentPRUrl,
|
||||
actions,
|
||||
isLastMessage,
|
||||
isInLast10Actions,
|
||||
config,
|
||||
isCheckingFeedback,
|
||||
feedbackData,
|
||||
}: UserAssistantEventMessageProps) {
|
||||
if (!isUserMessage(event) && !isAssistantMessage(event)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const message = parseMessageFromEvent(event);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ChatMessage type={event.source} message={message} actions={actions}>
|
||||
{event.args.image_urls && event.args.image_urls.length > 0 && (
|
||||
<ImageCarousel size="small" images={event.args.image_urls} />
|
||||
)}
|
||||
{event.args.file_urls && event.args.file_urls.length > 0 && (
|
||||
<FileList files={event.args.file_urls} />
|
||||
)}
|
||||
{shouldShowConfirmationButtons && <ConfirmationButtons />}
|
||||
</ChatMessage>
|
||||
<MicroagentStatusWrapper
|
||||
microagentStatus={microagentStatus}
|
||||
microagentConversationId={microagentConversationId}
|
||||
microagentPRUrl={microagentPRUrl}
|
||||
actions={actions}
|
||||
/>
|
||||
{isAssistantMessage(event) && event.action === "message" && (
|
||||
<LikertScaleWrapper
|
||||
event={event}
|
||||
isLastMessage={isLastMessage}
|
||||
isInLast10Actions={isInLast10Actions}
|
||||
config={config}
|
||||
isCheckingFeedback={isCheckingFeedback}
|
||||
feedbackData={feedbackData}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,39 +1,29 @@
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ConfirmationButtons } from "#/components/shared/buttons/confirmation-buttons";
|
||||
import { OpenHandsAction } from "#/types/core/actions";
|
||||
import {
|
||||
isUserMessage,
|
||||
isErrorObservation,
|
||||
isAssistantMessage,
|
||||
isOpenHandsAction,
|
||||
isOpenHandsObservation,
|
||||
isFinishAction,
|
||||
isRejectObservation,
|
||||
isMcpObservation,
|
||||
isTaskTrackingObservation,
|
||||
} from "#/types/core/guards";
|
||||
import { OpenHandsObservation } from "#/types/core/observations";
|
||||
import { ImageCarousel } from "../images/image-carousel";
|
||||
import { ChatMessage } from "./chat-message";
|
||||
import { ErrorMessage } from "./error-message";
|
||||
import { MCPObservationContent } from "./mcp-observation-content";
|
||||
import { TaskTrackingObservationContent } from "./task-tracking-observation-content";
|
||||
import { getObservationResult } from "./event-content-helpers/get-observation-result";
|
||||
import { getEventContent } from "./event-content-helpers/get-event-content";
|
||||
import { GenericEventMessage } from "./generic-event-message";
|
||||
import { MicroagentStatus } from "#/types/microagent-status";
|
||||
import { MicroagentStatusIndicator } from "./microagent/microagent-status-indicator";
|
||||
import { FileList } from "../files/file-list";
|
||||
import { parseMessageFromEvent } from "./event-content-helpers/parse-message-from-event";
|
||||
import { LikertScale } from "../feedback/likert-scale";
|
||||
|
||||
import { useConfig } from "#/hooks/query/use-config";
|
||||
import { useFeedbackExists } from "#/hooks/query/use-feedback-exists";
|
||||
|
||||
const hasThoughtProperty = (
|
||||
obj: Record<string, unknown>,
|
||||
): obj is { thought: string } => "thought" in obj && !!obj.thought;
|
||||
import {
|
||||
ErrorEventMessage,
|
||||
UserAssistantEventMessage,
|
||||
FinishEventMessage,
|
||||
RejectEventMessage,
|
||||
McpEventMessage,
|
||||
TaskTrackingEventMessage,
|
||||
ObservationPairEventMessage,
|
||||
GenericEventMessageWrapper,
|
||||
} from "./event-message-components";
|
||||
|
||||
interface EventMessageProps {
|
||||
event: OpenHandsAction | OpenHandsObservation;
|
||||
@@ -51,6 +41,7 @@ interface EventMessageProps {
|
||||
isInLast10Actions: boolean;
|
||||
}
|
||||
|
||||
/* eslint-disable react/jsx-props-no-spreading */
|
||||
export function EventMessage({
|
||||
event,
|
||||
hasObservationPair,
|
||||
@@ -62,7 +53,6 @@ export function EventMessage({
|
||||
actions,
|
||||
isInLast10Actions,
|
||||
}: EventMessageProps) {
|
||||
const { t } = useTranslation();
|
||||
const shouldShowConfirmationButtons =
|
||||
isLastMessage && event.source === "agent" && isAwaitingUserConfirmation;
|
||||
|
||||
@@ -73,194 +63,83 @@ export function EventMessage({
|
||||
isLoading: isCheckingFeedback,
|
||||
} = useFeedbackExists(event.id);
|
||||
|
||||
const renderLikertScale = () => {
|
||||
if (config?.APP_MODE !== "saas" || isCheckingFeedback) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// For error observations, show if in last 10 actions
|
||||
// For other events, show only if it's the last message
|
||||
const shouldShow = isErrorObservation(event)
|
||||
? isInLast10Actions
|
||||
: isLastMessage;
|
||||
|
||||
if (!shouldShow) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<LikertScale
|
||||
eventId={event.id}
|
||||
initiallySubmitted={feedbackData.exists}
|
||||
initialRating={feedbackData.rating}
|
||||
initialReason={feedbackData.reason}
|
||||
/>
|
||||
);
|
||||
// Common props for components that need them
|
||||
const commonProps = {
|
||||
microagentStatus,
|
||||
microagentConversationId,
|
||||
microagentPRUrl,
|
||||
actions,
|
||||
isLastMessage,
|
||||
isInLast10Actions,
|
||||
config,
|
||||
isCheckingFeedback,
|
||||
feedbackData,
|
||||
};
|
||||
|
||||
// Error observations
|
||||
if (isErrorObservation(event)) {
|
||||
return (
|
||||
<div>
|
||||
<ErrorMessage
|
||||
errorId={event.extras.error_id}
|
||||
defaultMessage={event.message}
|
||||
/>
|
||||
{microagentStatus && actions && (
|
||||
<MicroagentStatusIndicator
|
||||
status={microagentStatus}
|
||||
conversationId={microagentConversationId}
|
||||
prUrl={microagentPRUrl}
|
||||
/>
|
||||
)}
|
||||
{renderLikertScale()}
|
||||
</div>
|
||||
);
|
||||
return <ErrorEventMessage event={event} {...commonProps} />;
|
||||
}
|
||||
|
||||
// Observation pairs with OpenHands actions
|
||||
if (hasObservationPair && isOpenHandsAction(event)) {
|
||||
if (hasThoughtProperty(event.args) && event.action !== "think") {
|
||||
return (
|
||||
<div>
|
||||
<ChatMessage
|
||||
type="agent"
|
||||
message={event.args.thought}
|
||||
actions={actions}
|
||||
/>
|
||||
{microagentStatus && actions && (
|
||||
<MicroagentStatusIndicator
|
||||
status={microagentStatus}
|
||||
conversationId={microagentConversationId}
|
||||
prUrl={microagentPRUrl}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return microagentStatus && actions ? (
|
||||
<MicroagentStatusIndicator
|
||||
status={microagentStatus}
|
||||
conversationId={microagentConversationId}
|
||||
prUrl={microagentPRUrl}
|
||||
return (
|
||||
<ObservationPairEventMessage
|
||||
event={event}
|
||||
microagentStatus={microagentStatus}
|
||||
microagentConversationId={microagentConversationId}
|
||||
microagentPRUrl={microagentPRUrl}
|
||||
actions={actions}
|
||||
/>
|
||||
) : null;
|
||||
);
|
||||
}
|
||||
|
||||
// Finish actions
|
||||
if (isFinishAction(event)) {
|
||||
return (
|
||||
<>
|
||||
<ChatMessage
|
||||
type="agent"
|
||||
message={getEventContent(event).details}
|
||||
actions={actions}
|
||||
/>
|
||||
{microagentStatus && actions && (
|
||||
<MicroagentStatusIndicator
|
||||
status={microagentStatus}
|
||||
conversationId={microagentConversationId}
|
||||
prUrl={microagentPRUrl}
|
||||
/>
|
||||
)}
|
||||
{renderLikertScale()}
|
||||
</>
|
||||
);
|
||||
return <FinishEventMessage event={event} {...commonProps} />;
|
||||
}
|
||||
|
||||
// User and assistant messages
|
||||
if (isUserMessage(event) || isAssistantMessage(event)) {
|
||||
const message = parseMessageFromEvent(event);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ChatMessage type={event.source} message={message} actions={actions}>
|
||||
{event.args.image_urls && event.args.image_urls.length > 0 && (
|
||||
<ImageCarousel size="small" images={event.args.image_urls} />
|
||||
)}
|
||||
{event.args.file_urls && event.args.file_urls.length > 0 && (
|
||||
<FileList files={event.args.file_urls} />
|
||||
)}
|
||||
{shouldShowConfirmationButtons && <ConfirmationButtons />}
|
||||
</ChatMessage>
|
||||
{microagentStatus && actions && (
|
||||
<MicroagentStatusIndicator
|
||||
status={microagentStatus}
|
||||
conversationId={microagentConversationId}
|
||||
prUrl={microagentPRUrl}
|
||||
/>
|
||||
)}
|
||||
{isAssistantMessage(event) &&
|
||||
event.action === "message" &&
|
||||
renderLikertScale()}
|
||||
</>
|
||||
<UserAssistantEventMessage
|
||||
event={event}
|
||||
shouldShowConfirmationButtons={shouldShowConfirmationButtons}
|
||||
{...commonProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Reject observations
|
||||
if (isRejectObservation(event)) {
|
||||
return (
|
||||
<div>
|
||||
<ChatMessage type="agent" message={event.content} />
|
||||
</div>
|
||||
);
|
||||
return <RejectEventMessage event={event} />;
|
||||
}
|
||||
|
||||
// MCP observations
|
||||
if (isMcpObservation(event)) {
|
||||
return (
|
||||
<div>
|
||||
<GenericEventMessage
|
||||
title={getEventContent(event).title}
|
||||
details={<MCPObservationContent event={event} />}
|
||||
success={getObservationResult(event)}
|
||||
/>
|
||||
{shouldShowConfirmationButtons && <ConfirmationButtons />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isTaskTrackingObservation(event)) {
|
||||
const { command } = event.extras;
|
||||
let title: React.ReactNode;
|
||||
let initiallyExpanded = false;
|
||||
|
||||
// Determine title and expansion state based on command
|
||||
if (command === "plan") {
|
||||
title = t("OBSERVATION_MESSAGE$TASK_TRACKING_PLAN");
|
||||
initiallyExpanded = true;
|
||||
} else {
|
||||
// command === "view"
|
||||
title = t("OBSERVATION_MESSAGE$TASK_TRACKING_VIEW");
|
||||
initiallyExpanded = false;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<GenericEventMessage
|
||||
title={title}
|
||||
details={<TaskTrackingObservationContent event={event} />}
|
||||
success={getObservationResult(event)}
|
||||
initiallyExpanded={initiallyExpanded}
|
||||
/>
|
||||
{shouldShowConfirmationButtons && <ConfirmationButtons />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{isOpenHandsAction(event) &&
|
||||
hasThoughtProperty(event.args) &&
|
||||
event.action !== "think" && (
|
||||
<ChatMessage type="agent" message={event.args.thought} />
|
||||
)}
|
||||
|
||||
<GenericEventMessage
|
||||
title={getEventContent(event).title}
|
||||
details={getEventContent(event).details}
|
||||
success={
|
||||
isOpenHandsObservation(event)
|
||||
? getObservationResult(event)
|
||||
: undefined
|
||||
}
|
||||
<McpEventMessage
|
||||
event={event}
|
||||
shouldShowConfirmationButtons={shouldShowConfirmationButtons}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
{shouldShowConfirmationButtons && <ConfirmationButtons />}
|
||||
</div>
|
||||
// Task tracking observations
|
||||
if (isTaskTrackingObservation(event)) {
|
||||
return (
|
||||
<TaskTrackingEventMessage
|
||||
event={event}
|
||||
shouldShowConfirmationButtons={shouldShowConfirmationButtons}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Generic fallback
|
||||
return (
|
||||
<GenericEventMessageWrapper
|
||||
event={event}
|
||||
shouldShowConfirmationButtons={shouldShowConfirmationButtons}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { PayloadAction } from "@reduxjs/toolkit";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import Markdown from "react-markdown";
|
||||
@@ -30,8 +29,8 @@ interface ExpandableMessageProps {
|
||||
message: string;
|
||||
type: string;
|
||||
success?: boolean;
|
||||
observation?: PayloadAction<OpenHandsObservation>;
|
||||
action?: PayloadAction<OpenHandsAction>;
|
||||
observation?: { payload: OpenHandsObservation };
|
||||
action?: { payload: OpenHandsAction };
|
||||
}
|
||||
|
||||
export function ExpandableMessage({
|
||||
|
||||
@@ -8,14 +8,12 @@ import { Provider } from "#/types/settings";
|
||||
|
||||
interface GitControlBarPrButtonProps {
|
||||
onSuggestionsClick: (value: string) => void;
|
||||
isEnabled: boolean;
|
||||
hasRepository: boolean;
|
||||
currentGitProvider: Provider;
|
||||
}
|
||||
|
||||
export function GitControlBarPrButton({
|
||||
onSuggestionsClick,
|
||||
isEnabled,
|
||||
hasRepository,
|
||||
currentGitProvider,
|
||||
}: GitControlBarPrButtonProps) {
|
||||
@@ -24,7 +22,7 @@ export function GitControlBarPrButton({
|
||||
const { providers } = useUserProviders();
|
||||
|
||||
const providersAreSet = providers.length > 0;
|
||||
const isButtonEnabled = isEnabled && providersAreSet && hasRepository;
|
||||
const isButtonEnabled = providersAreSet && hasRepository;
|
||||
|
||||
const handlePrClick = () => {
|
||||
posthog.capture("create_pr_button_clicked");
|
||||
|
||||
@@ -8,12 +8,10 @@ import { I18nKey } from "#/i18n/declaration";
|
||||
|
||||
interface GitControlBarPullButtonProps {
|
||||
onSuggestionsClick: (value: string) => void;
|
||||
isEnabled: boolean;
|
||||
}
|
||||
|
||||
export function GitControlBarPullButton({
|
||||
onSuggestionsClick,
|
||||
isEnabled,
|
||||
}: GitControlBarPullButtonProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -22,7 +20,7 @@ export function GitControlBarPullButton({
|
||||
|
||||
const providersAreSet = providers.length > 0;
|
||||
const hasRepository = conversation?.selected_repository;
|
||||
const isButtonEnabled = isEnabled && providersAreSet && hasRepository;
|
||||
const isButtonEnabled = providersAreSet && hasRepository;
|
||||
|
||||
const handlePullClick = () => {
|
||||
posthog.capture("pull_button_clicked");
|
||||
|
||||
@@ -8,14 +8,12 @@ import { Provider } from "#/types/settings";
|
||||
|
||||
interface GitControlBarPushButtonProps {
|
||||
onSuggestionsClick: (value: string) => void;
|
||||
isEnabled: boolean;
|
||||
hasRepository: boolean;
|
||||
currentGitProvider: Provider;
|
||||
}
|
||||
|
||||
export function GitControlBarPushButton({
|
||||
onSuggestionsClick,
|
||||
isEnabled,
|
||||
hasRepository,
|
||||
currentGitProvider,
|
||||
}: GitControlBarPushButtonProps) {
|
||||
@@ -24,7 +22,7 @@ export function GitControlBarPushButton({
|
||||
const { providers } = useUserProviders();
|
||||
|
||||
const providersAreSet = providers.length > 0;
|
||||
const isButtonEnabled = isEnabled && providersAreSet && hasRepository;
|
||||
const isButtonEnabled = providersAreSet && hasRepository;
|
||||
|
||||
const handlePushClick = () => {
|
||||
posthog.capture("push_button_clicked");
|
||||
|
||||
@@ -11,17 +11,9 @@ import { GitControlBarTooltipWrapper } from "./git-control-bar-tooltip-wrapper";
|
||||
|
||||
interface GitControlBarProps {
|
||||
onSuggestionsClick: (value: string) => void;
|
||||
isWaitingForUserInput: boolean;
|
||||
hasSubstantiveAgentActions: boolean;
|
||||
optimisticUserMessage: boolean;
|
||||
}
|
||||
|
||||
export function GitControlBar({
|
||||
onSuggestionsClick,
|
||||
isWaitingForUserInput,
|
||||
hasSubstantiveAgentActions,
|
||||
optimisticUserMessage,
|
||||
}: GitControlBarProps) {
|
||||
export function GitControlBar({ onSuggestionsClick }: GitControlBarProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { data: conversation } = useActiveConversation();
|
||||
@@ -30,12 +22,6 @@ export function GitControlBar({
|
||||
const gitProvider = conversation?.git_provider as Provider;
|
||||
const selectedBranch = conversation?.selected_branch;
|
||||
|
||||
// Button is enabled when the agent is waiting for user input, has substantive actions, and no optimistic message
|
||||
const isButtonEnabled =
|
||||
isWaitingForUserInput &&
|
||||
hasSubstantiveAgentActions &&
|
||||
!optimisticUserMessage;
|
||||
|
||||
const hasRepository = !!selectedRepository;
|
||||
|
||||
return (
|
||||
@@ -73,7 +59,6 @@ export function GitControlBar({
|
||||
>
|
||||
<GitControlBarPullButton
|
||||
onSuggestionsClick={onSuggestionsClick}
|
||||
isEnabled={isButtonEnabled}
|
||||
/>
|
||||
</GitControlBarTooltipWrapper>
|
||||
|
||||
@@ -84,7 +69,6 @@ export function GitControlBar({
|
||||
>
|
||||
<GitControlBarPushButton
|
||||
onSuggestionsClick={onSuggestionsClick}
|
||||
isEnabled={isButtonEnabled}
|
||||
hasRepository={hasRepository}
|
||||
currentGitProvider={gitProvider}
|
||||
/>
|
||||
@@ -97,7 +81,6 @@ export function GitControlBar({
|
||||
>
|
||||
<GitControlBarPrButton
|
||||
onSuggestionsClick={onSuggestionsClick}
|
||||
isEnabled={isButtonEnabled}
|
||||
hasRepository={hasRepository}
|
||||
currentGitProvider={gitProvider}
|
||||
/>
|
||||
|
||||
@@ -1,44 +1,35 @@
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import { isFileImage } from "#/utils/is-file-image";
|
||||
import { displayErrorToast } from "#/utils/custom-toast-handlers";
|
||||
import { validateFiles } from "#/utils/file-validation";
|
||||
import { CustomChatInput } from "./custom-chat-input";
|
||||
import { RootState } from "#/store";
|
||||
import { AgentState } from "#/types/agent-state";
|
||||
import { useActiveConversation } from "#/hooks/query/use-active-conversation";
|
||||
import { GitControlBar } from "./git-control-bar";
|
||||
import {
|
||||
addImages,
|
||||
addFiles,
|
||||
clearAllFiles,
|
||||
addFileLoading,
|
||||
removeFileLoading,
|
||||
addImageLoading,
|
||||
removeImageLoading,
|
||||
} from "#/state/conversation-slice";
|
||||
import { useConversationStore } from "#/state/conversation-store";
|
||||
import { useAgentStore } from "#/stores/agent-store";
|
||||
import { processFiles, processImages } from "#/utils/file-processing";
|
||||
|
||||
interface InteractiveChatBoxProps {
|
||||
onSubmit: (message: string, images: File[], files: File[]) => void;
|
||||
onStop: () => void;
|
||||
isWaitingForUserInput: boolean;
|
||||
hasSubstantiveAgentActions: boolean;
|
||||
optimisticUserMessage: boolean;
|
||||
}
|
||||
|
||||
export function InteractiveChatBox({
|
||||
onSubmit,
|
||||
onStop,
|
||||
isWaitingForUserInput,
|
||||
hasSubstantiveAgentActions,
|
||||
optimisticUserMessage,
|
||||
}: InteractiveChatBoxProps) {
|
||||
const dispatch = useDispatch();
|
||||
const curAgentState = useSelector(
|
||||
(state: RootState) => state.agent.curAgentState,
|
||||
);
|
||||
const images = useSelector((state: RootState) => state.conversation.images);
|
||||
const files = useSelector((state: RootState) => state.conversation.files);
|
||||
const {
|
||||
images,
|
||||
files,
|
||||
addImages,
|
||||
addFiles,
|
||||
clearAllFiles,
|
||||
addFileLoading,
|
||||
removeFileLoading,
|
||||
addImageLoading,
|
||||
removeImageLoading,
|
||||
} = useConversationStore();
|
||||
const { curAgentState } = useAgentStore();
|
||||
const { data: conversation } = useActiveConversation();
|
||||
|
||||
// Helper function to validate and filter files
|
||||
@@ -58,26 +49,24 @@ export function InteractiveChatBox({
|
||||
|
||||
// Helper function to show loading indicators for files
|
||||
const showLoadingIndicators = (validFiles: File[], validImages: File[]) => {
|
||||
validFiles.forEach((file) => dispatch(addFileLoading(file.name)));
|
||||
validImages.forEach((image) => dispatch(addImageLoading(image.name)));
|
||||
validFiles.forEach((file) => addFileLoading(file.name));
|
||||
validImages.forEach((image) => addImageLoading(image.name));
|
||||
};
|
||||
|
||||
// Helper function to handle successful file processing results
|
||||
const handleSuccessfulFiles = (fileResults: { successful: File[] }) => {
|
||||
if (fileResults.successful.length > 0) {
|
||||
dispatch(addFiles(fileResults.successful));
|
||||
fileResults.successful.forEach((file) =>
|
||||
dispatch(removeFileLoading(file.name)),
|
||||
);
|
||||
addFiles(fileResults.successful);
|
||||
fileResults.successful.forEach((file) => removeFileLoading(file.name));
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to handle successful image processing results
|
||||
const handleSuccessfulImages = (imageResults: { successful: File[] }) => {
|
||||
if (imageResults.successful.length > 0) {
|
||||
dispatch(addImages(imageResults.successful));
|
||||
addImages(imageResults.successful);
|
||||
imageResults.successful.forEach((image) =>
|
||||
dispatch(removeImageLoading(image.name)),
|
||||
removeImageLoading(image.name),
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -88,14 +77,14 @@ export function InteractiveChatBox({
|
||||
imageResults: { failed: { file: File; error: Error }[] },
|
||||
) => {
|
||||
fileResults.failed.forEach(({ file, error }) => {
|
||||
dispatch(removeFileLoading(file.name));
|
||||
removeFileLoading(file.name);
|
||||
displayErrorToast(
|
||||
`Failed to process file ${file.name}: ${error.message}`,
|
||||
);
|
||||
});
|
||||
|
||||
imageResults.failed.forEach(({ file, error }) => {
|
||||
dispatch(removeImageLoading(file.name));
|
||||
removeImageLoading(file.name);
|
||||
displayErrorToast(
|
||||
`Failed to process image ${file.name}: ${error.message}`,
|
||||
);
|
||||
@@ -104,8 +93,8 @@ export function InteractiveChatBox({
|
||||
|
||||
// Helper function to clear loading states on error
|
||||
const clearLoadingStates = (validFiles: File[], validImages: File[]) => {
|
||||
validFiles.forEach((file) => dispatch(removeFileLoading(file.name)));
|
||||
validImages.forEach((image) => dispatch(removeImageLoading(image.name)));
|
||||
validFiles.forEach((file) => removeFileLoading(file.name));
|
||||
validImages.forEach((image) => removeImageLoading(image.name));
|
||||
};
|
||||
|
||||
const handleUpload = async (selectedFiles: File[]) => {
|
||||
@@ -140,7 +129,7 @@ export function InteractiveChatBox({
|
||||
|
||||
const handleSubmit = (message: string) => {
|
||||
onSubmit(message, images, files);
|
||||
dispatch(clearAllFiles());
|
||||
clearAllFiles();
|
||||
};
|
||||
|
||||
const handleSuggestionsClick = (suggestion: string) => {
|
||||
@@ -161,12 +150,7 @@ export function InteractiveChatBox({
|
||||
conversationStatus={conversation?.status || null}
|
||||
/>
|
||||
<div className="mt-4">
|
||||
<GitControlBar
|
||||
onSuggestionsClick={handleSuggestionsClick}
|
||||
isWaitingForUserInput={isWaitingForUserInput}
|
||||
hasSubstantiveAgentActions={hasSubstantiveAgentActions}
|
||||
optimisticUserMessage={optimisticUserMessage}
|
||||
/>
|
||||
<GitControlBar onSuggestionsClick={handleSuggestionsClick} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
} from "#/types/core/guards";
|
||||
import { EventMessage } from "./event-message";
|
||||
import { ChatMessage } from "./chat-message";
|
||||
import { useOptimisticUserMessage } from "#/hooks/use-optimistic-user-message";
|
||||
import { useOptimisticUserMessageStore } from "#/stores/optimistic-user-message-store";
|
||||
import { LaunchMicroagentModal } from "./microagent/launch-microagent-modal";
|
||||
import { useUserConversation } from "#/hooks/query/use-user-conversation";
|
||||
import { useConversationId } from "#/hooks/use-conversation-id";
|
||||
@@ -48,7 +48,7 @@ export const Messages: React.FC<MessagesProps> = React.memo(
|
||||
isPending,
|
||||
unsubscribeFromConversation,
|
||||
} = useCreateConversationAndSubscribeMultiple();
|
||||
const { getOptimisticUserMessage } = useOptimisticUserMessage();
|
||||
const { getOptimisticUserMessage } = useOptimisticUserMessageStore();
|
||||
const { conversationId } = useConversationId();
|
||||
const { data: conversation } = useUserConversation(conversationId);
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import { useHandleRuntimeActive } from "#/hooks/use-handle-runtime-active";
|
||||
import { LoadingMicroagentBody } from "./loading-microagent-body";
|
||||
import { LoadingMicroagentTextarea } from "./loading-microagent-textarea";
|
||||
import { useGetMicroagents } from "#/hooks/query/use-get-microagents";
|
||||
import { Typography } from "#/ui/typography";
|
||||
|
||||
interface LaunchMicroagentModalProps {
|
||||
onClose: () => void;
|
||||
@@ -76,9 +77,9 @@ export function LaunchMicroagentModal({
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<span className="text-sm text-[#A3A3A3] font-normal leading-5">
|
||||
<Typography.Text className="text-sm text-[#A3A3A3] font-normal leading-5">
|
||||
{t("MICROAGENT$DEFINITION")}
|
||||
</span>
|
||||
</Typography.Text>
|
||||
|
||||
<form
|
||||
data-testid="launch-microagent-modal"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Spinner } from "@heroui/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ModalBody } from "#/components/shared/modals/modal-body";
|
||||
import { Typography } from "#/ui/typography";
|
||||
|
||||
export function LoadingMicroagentBody() {
|
||||
const { t } = useTranslation();
|
||||
@@ -10,7 +11,7 @@ export function LoadingMicroagentBody() {
|
||||
{t("MICROAGENT$ADD_TO_MICROAGENT")}
|
||||
</h2>
|
||||
<Spinner size="lg" />
|
||||
<p>{t("MICROAGENT$WAIT_FOR_RUNTIME")}</p>
|
||||
<Typography.Text>{t("MICROAGENT$WAIT_FOR_RUNTIME")}</Typography.Text>
|
||||
</ModalBody>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { Spinner } from "@heroui/react";
|
||||
import { MicroagentStatus } from "#/types/microagent-status";
|
||||
import { SuccessIndicator } from "../success-indicator";
|
||||
import { Typography } from "#/ui/typography";
|
||||
|
||||
interface MicroagentStatusIndicatorProps {
|
||||
status: MicroagentStatus;
|
||||
@@ -81,7 +82,9 @@ export function MicroagentStatusIndicator({
|
||||
);
|
||||
}
|
||||
|
||||
return <span className="underline">{statusText}</span>;
|
||||
return (
|
||||
<Typography.Text className="underline">{statusText}</Typography.Text>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,26 +1,23 @@
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import { RootState } from "#/store";
|
||||
import { UploadedFile } from "./uploaded-file";
|
||||
import { UploadedImage } from "./uploaded-image";
|
||||
import { removeFile, removeImage } from "#/state/conversation-slice";
|
||||
import { useConversationStore } from "#/state/conversation-store";
|
||||
|
||||
export function UploadedFiles() {
|
||||
const dispatch = useDispatch();
|
||||
const images = useSelector((state: RootState) => state.conversation.images);
|
||||
const files = useSelector((state: RootState) => state.conversation.files);
|
||||
const loadingFiles = useSelector(
|
||||
(state: RootState) => state.conversation.loadingFiles,
|
||||
);
|
||||
const loadingImages = useSelector(
|
||||
(state: RootState) => state.conversation.loadingImages,
|
||||
);
|
||||
const {
|
||||
images,
|
||||
files,
|
||||
loadingFiles,
|
||||
loadingImages,
|
||||
removeFile,
|
||||
removeImage,
|
||||
} = useConversationStore();
|
||||
|
||||
const handleRemoveFile = (index: number) => {
|
||||
dispatch(removeFile(index));
|
||||
removeFile(index);
|
||||
};
|
||||
|
||||
const handleRemoveImage = (index: number) => {
|
||||
dispatch(removeImage(index));
|
||||
removeImage(index);
|
||||
};
|
||||
|
||||
// Don't render anything if there are no files, images, or loading items
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router";
|
||||
import { ContextMenu } from "#/ui/context-menu";
|
||||
@@ -6,91 +7,14 @@ import { Divider } from "#/ui/divider";
|
||||
import { useClickOutsideElement } from "#/hooks/use-click-outside-element";
|
||||
import { useConfig } from "#/hooks/query/use-config";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import CreditCardIcon from "#/icons/credit-card.svg?react";
|
||||
import KeyIcon from "#/icons/key.svg?react";
|
||||
import LogOutIcon from "#/icons/log-out.svg?react";
|
||||
import ServerProcessIcon from "#/icons/server-process.svg?react";
|
||||
import SettingsGearIcon from "#/icons/settings-gear.svg?react";
|
||||
import CircuitIcon from "#/icons/u-circuit.svg?react";
|
||||
import PuzzlePieceIcon from "#/icons/u-puzzle-piece.svg?react";
|
||||
import UserIcon from "#/icons/user.svg?react";
|
||||
import { SAAS_NAV_ITEMS, OSS_NAV_ITEMS } from "#/constants/settings-nav";
|
||||
|
||||
interface AccountSettingsContextMenuProps {
|
||||
onLogout: () => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const SAAS_NAV_ITEMS = [
|
||||
{
|
||||
icon: <UserIcon width={16} height={16} />,
|
||||
to: "/settings/user",
|
||||
text: "COMMON$USER_SETTINGS",
|
||||
},
|
||||
{
|
||||
icon: <PuzzlePieceIcon width={16} height={16} />,
|
||||
to: "/settings/integrations",
|
||||
text: "SETTINGS$NAV_INTEGRATIONS",
|
||||
},
|
||||
{
|
||||
icon: <SettingsGearIcon width={16} height={16} />,
|
||||
to: "/settings/app",
|
||||
text: "COMMON$APPLICATION_SETTINGS",
|
||||
},
|
||||
{
|
||||
icon: <CircuitIcon width={16} height={16} />,
|
||||
to: "/settings",
|
||||
text: "COMMON$LANGUAGE_MODEL_LLM",
|
||||
},
|
||||
{
|
||||
icon: <CreditCardIcon width={16} height={16} />,
|
||||
to: "/settings/billing",
|
||||
text: "SETTINGS$NAV_BILLING",
|
||||
},
|
||||
{
|
||||
icon: <KeyIcon width={16} height={16} />,
|
||||
to: "/settings/secrets",
|
||||
text: "SETTINGS$NAV_SECRETS",
|
||||
},
|
||||
{
|
||||
icon: <KeyIcon width={16} height={16} />,
|
||||
to: "/settings/api-keys",
|
||||
text: "SETTINGS$NAV_API_KEYS",
|
||||
},
|
||||
{
|
||||
icon: <ServerProcessIcon width={16} height={16} />,
|
||||
to: "/settings/mcp",
|
||||
text: "SETTINGS$NAV_MCP",
|
||||
},
|
||||
];
|
||||
|
||||
const OSS_NAV_ITEMS = [
|
||||
{
|
||||
icon: <CircuitIcon width={16} height={16} />,
|
||||
to: "/settings",
|
||||
text: "COMMON$LANGUAGE_MODEL_LLM",
|
||||
},
|
||||
{
|
||||
icon: <ServerProcessIcon width={16} height={16} />,
|
||||
to: "/settings/mcp",
|
||||
text: "COMMON$MODEL_CONTEXT_PROTOCOL_MCP",
|
||||
},
|
||||
{
|
||||
icon: <PuzzlePieceIcon width={16} height={16} />,
|
||||
to: "/settings/integrations",
|
||||
text: "SETTINGS$NAV_INTEGRATIONS",
|
||||
},
|
||||
{
|
||||
icon: <SettingsGearIcon width={16} height={16} />,
|
||||
to: "/settings/app",
|
||||
text: "COMMON$APPLICATION_SETTINGS",
|
||||
},
|
||||
{
|
||||
icon: <KeyIcon width={16} height={16} />,
|
||||
to: "/settings/secrets",
|
||||
text: "SETTINGS$NAV_SECRETS",
|
||||
},
|
||||
];
|
||||
|
||||
export function AccountSettingsContextMenu({
|
||||
onLogout,
|
||||
onClose,
|
||||
@@ -100,7 +24,13 @@ export function AccountSettingsContextMenu({
|
||||
const { data: config } = useConfig();
|
||||
|
||||
const isSaas = config?.APP_MODE === "saas";
|
||||
const navItems = isSaas ? SAAS_NAV_ITEMS : OSS_NAV_ITEMS;
|
||||
const navItems = (isSaas ? SAAS_NAV_ITEMS : OSS_NAV_ITEMS).map((item) => ({
|
||||
...item,
|
||||
icon: React.cloneElement(item.icon, {
|
||||
width: 16,
|
||||
height: 16,
|
||||
} as React.SVGProps<SVGSVGElement>),
|
||||
}));
|
||||
|
||||
const handleNavigationClick = () => {
|
||||
onClose();
|
||||
@@ -112,7 +42,7 @@ export function AccountSettingsContextMenu({
|
||||
testId="account-settings-context-menu"
|
||||
ref={ref}
|
||||
alignment="right"
|
||||
className="mt-0 md:right-full md:left-full md:bottom-0 ml-0 z-10 w-fit z-[9999]"
|
||||
className="mt-0 md:right-full md:left-full md:bottom-0 ml-0 w-fit z-[9999]"
|
||||
>
|
||||
{navItems.map(({ to, text, icon }) => (
|
||||
<Link key={to} to={to} className="text-decoration-none">
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import { useEffect } from "react";
|
||||
import { RootState } from "#/store";
|
||||
import { useStatusStore } from "#/state/status-store";
|
||||
import { useWsClient } from "#/context/ws-client-provider";
|
||||
import { useActiveConversation } from "#/hooks/query/use-active-conversation";
|
||||
@@ -12,8 +10,9 @@ import ClockIcon from "#/icons/u-clock-three.svg?react";
|
||||
import { ChatResumeAgentButton } from "../chat/chat-play-button";
|
||||
import { cn } from "#/utils/utils";
|
||||
import { AgentLoading } from "./agent-loading";
|
||||
import { setShouldShownAgentLoading } from "#/state/conversation-slice";
|
||||
import { useConversationStore } from "#/state/conversation-store";
|
||||
import CircleErrorIcon from "#/icons/circle-error.svg?react";
|
||||
import { useAgentStore } from "#/stores/agent-store";
|
||||
|
||||
export interface AgentStatusProps {
|
||||
className?: string;
|
||||
@@ -29,8 +28,8 @@ export function AgentStatus({
|
||||
disabled = false,
|
||||
}: AgentStatusProps) {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
const { curAgentState } = useSelector((state: RootState) => state.agent);
|
||||
const { setShouldShownAgentLoading } = useConversationStore();
|
||||
const { curAgentState } = useAgentStore();
|
||||
const { curStatusMessage } = useStatusStore();
|
||||
const { webSocketStatus } = useWsClient();
|
||||
const { data: conversation } = useActiveConversation();
|
||||
@@ -58,8 +57,8 @@ export function AgentStatus({
|
||||
|
||||
// Update global state when agent loading condition changes
|
||||
useEffect(() => {
|
||||
dispatch(setShouldShownAgentLoading(shouldShownAgentLoading));
|
||||
}, [shouldShownAgentLoading, dispatch]);
|
||||
setShouldShownAgentLoading(shouldShownAgentLoading);
|
||||
}, [shouldShownAgentLoading, setShouldShownAgentLoading]);
|
||||
|
||||
return (
|
||||
<div className={`flex items-center gap-1 ${className}`}>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { ContextMenu } from "#/ui/context-menu";
|
||||
import { ContextMenuListItem } from "../context-menu/context-menu-list-item";
|
||||
import { ToolsContextMenuIconText } from "./tools-context-menu-icon-text";
|
||||
@@ -11,7 +10,7 @@ import {
|
||||
getCreatePRPrompt,
|
||||
getCreateNewBranchPrompt,
|
||||
} from "#/utils/utils";
|
||||
import { setMessageToSend } from "#/state/conversation-slice";
|
||||
import { useConversationStore } from "#/state/conversation-store";
|
||||
|
||||
import ArrowUpIcon from "#/icons/u-arrow-up.svg?react";
|
||||
import ArrowDownIcon from "#/icons/u-arrow-down.svg?react";
|
||||
@@ -28,28 +27,28 @@ interface GitToolsSubmenuProps {
|
||||
|
||||
export function GitToolsSubmenu({ onClose }: GitToolsSubmenuProps) {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
const { setMessageToSend } = useConversationStore();
|
||||
const { data: conversation } = useActiveConversation();
|
||||
|
||||
const currentGitProvider = conversation?.git_provider as Provider;
|
||||
|
||||
const onGitPull = () => {
|
||||
dispatch(setMessageToSend(getGitPullPrompt()));
|
||||
setMessageToSend(getGitPullPrompt());
|
||||
onClose();
|
||||
};
|
||||
|
||||
const onGitPush = () => {
|
||||
dispatch(setMessageToSend(getGitPushPrompt(currentGitProvider)));
|
||||
setMessageToSend(getGitPushPrompt(currentGitProvider));
|
||||
onClose();
|
||||
};
|
||||
|
||||
const onCreatePR = () => {
|
||||
dispatch(setMessageToSend(getCreatePRPrompt(currentGitProvider)));
|
||||
setMessageToSend(getCreatePRPrompt(currentGitProvider));
|
||||
onClose();
|
||||
};
|
||||
|
||||
const onCreateNewBranch = () => {
|
||||
dispatch(setMessageToSend(getCreateNewBranchPrompt()));
|
||||
setMessageToSend(getCreateNewBranchPrompt());
|
||||
onClose();
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { ContextMenu } from "#/ui/context-menu";
|
||||
import { ContextMenuListItem } from "../context-menu/context-menu-list-item";
|
||||
import { ToolsContextMenuIconText } from "./tools-context-menu-icon-text";
|
||||
@@ -9,7 +8,7 @@ import PrStatusIcon from "#/icons/pr-status.svg?react";
|
||||
import DocumentIcon from "#/icons/document.svg?react";
|
||||
import WaterIcon from "#/icons/u-water.svg?react";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { setMessageToSend } from "#/state/conversation-slice";
|
||||
import { useConversationStore } from "#/state/conversation-store";
|
||||
import { REPO_SUGGESTIONS } from "#/utils/suggestions/repo-suggestions";
|
||||
import { CONTEXT_MENU_ICON_TEXT_CLASSNAME } from "#/utils/constants";
|
||||
|
||||
@@ -22,22 +21,22 @@ interface MacrosSubmenuProps {
|
||||
|
||||
export function MacrosSubmenu({ onClose }: MacrosSubmenuProps) {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useDispatch();
|
||||
const { setMessageToSend } = useConversationStore();
|
||||
|
||||
const onIncreaseTestCoverage = () => {
|
||||
dispatch(setMessageToSend(REPO_SUGGESTIONS.INCREASE_TEST_COVERAGE));
|
||||
setMessageToSend(REPO_SUGGESTIONS.INCREASE_TEST_COVERAGE);
|
||||
onClose();
|
||||
};
|
||||
const onFixReadme = () => {
|
||||
dispatch(setMessageToSend(REPO_SUGGESTIONS.FIX_README));
|
||||
setMessageToSend(REPO_SUGGESTIONS.FIX_README);
|
||||
onClose();
|
||||
};
|
||||
const onAutoMergePRs = () => {
|
||||
dispatch(setMessageToSend(REPO_SUGGESTIONS.AUTO_MERGE_PRS));
|
||||
setMessageToSend(REPO_SUGGESTIONS.AUTO_MERGE_PRS);
|
||||
onClose();
|
||||
};
|
||||
const onCleanDependencies = () => {
|
||||
dispatch(setMessageToSend(REPO_SUGGESTIONS.CLEAN_DEPENDENCIES));
|
||||
setMessageToSend(REPO_SUGGESTIONS.CLEAN_DEPENDENCIES);
|
||||
onClose();
|
||||
};
|
||||
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import { useSelector } from "react-redux";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useState } from "react";
|
||||
import DebugStackframeDot from "#/icons/debug-stackframe-dot.svg?react";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { ConversationStatus } from "#/types/conversation-status";
|
||||
import { RootState } from "#/store";
|
||||
import { AgentState } from "#/types/agent-state";
|
||||
import { ServerStatusContextMenu } from "./server-status-context-menu";
|
||||
import { useStartConversation } from "#/hooks/mutation/use-start-conversation";
|
||||
import { useConversationId } from "#/hooks/use-conversation-id";
|
||||
import { useUserProviders } from "#/hooks/use-user-providers";
|
||||
import { useStopConversation } from "#/hooks/mutation/use-stop-conversation";
|
||||
import { useAgentStore } from "#/stores/agent-store";
|
||||
|
||||
export interface ServerStatusProps {
|
||||
className?: string;
|
||||
@@ -23,7 +22,7 @@ export function ServerStatus({
|
||||
}: ServerStatusProps) {
|
||||
const [showContextMenu, setShowContextMenu] = useState(false);
|
||||
|
||||
const { curAgentState } = useSelector((state: RootState) => state.agent);
|
||||
const { curAgentState } = useAgentStore();
|
||||
const { t } = useTranslation();
|
||||
const { conversationId } = useConversationId();
|
||||
|
||||
@@ -35,8 +34,7 @@ export function ServerStatus({
|
||||
const isStartingStatus =
|
||||
curAgentState === AgentState.LOADING || curAgentState === AgentState.INIT;
|
||||
|
||||
const isStopStatus =
|
||||
curAgentState === AgentState.STOPPED || conversationStatus === "STOPPED";
|
||||
const isStopStatus = conversationStatus === "STOPPED";
|
||||
|
||||
// Get the appropriate color based on agent status
|
||||
const getStatusColor = (): string => {
|
||||
|
||||