mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
64 Commits
resolver-r
...
ray/nix
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f804a50a97 | ||
|
|
7cfecb6e52 | ||
|
|
8fe2e006ee | ||
|
|
6d62c341eb | ||
|
|
229f35093d | ||
|
|
21a5e3eed5 | ||
|
|
97e3310dd5 | ||
|
|
2053e72474 | ||
|
|
300f20368e | ||
|
|
0bed046fcc | ||
|
|
0bf0dc9316 | ||
|
|
0e8d9a8bb4 | ||
|
|
9280bc34ad | ||
|
|
b132348d22 | ||
|
|
1be77faf94 | ||
|
|
a6301075ec | ||
|
|
b98615bc1c | ||
|
|
29fdc701a3 | ||
|
|
8bc9207c24 | ||
|
|
96008736a4 | ||
|
|
38d5db0547 | ||
|
|
8af1f1cac9 | ||
|
|
ef502ccba8 | ||
|
|
ece556c047 | ||
|
|
55a09785ce | ||
|
|
2990c21d97 | ||
|
|
14c8ea93c9 | ||
|
|
764077ef3d | ||
|
|
63ead2a638 | ||
|
|
be0049c76e | ||
|
|
bafd1596dd | ||
|
|
ce58ccab8a | ||
|
|
b3c8b7c089 | ||
|
|
ac2947b7ff | ||
|
|
91cd647f20 | ||
|
|
c521fb7a8f | ||
|
|
f049411631 | ||
|
|
606ec59b33 | ||
|
|
d2fc5679ad | ||
|
|
7bfa05d38a | ||
|
|
12a95fb548 | ||
|
|
ae03c4eb80 | ||
|
|
8e486dfd6b | ||
|
|
48ee5659c9 | ||
|
|
b7613d7529 | ||
|
|
e05e627957 | ||
|
|
6da7e051be | ||
|
|
002e12a049 | ||
|
|
ed58858e03 | ||
|
|
11ae4f96c2 | ||
|
|
c2acf4e07e | ||
|
|
e9bdf761b7 | ||
|
|
04b93069b4 | ||
|
|
ec03ce1ca0 | ||
|
|
46157a85d8 | ||
|
|
a691e3148a | ||
|
|
4674e0b77a | ||
|
|
d7d0329d25 | ||
|
|
17853cd5bd | ||
|
|
c992b6d2a0 | ||
|
|
34bf645d64 | ||
|
|
1ae1c16b26 | ||
|
|
5099413729 | ||
|
|
b06a3bdb7c |
2
.github/workflows/ghcr-build.yml
vendored
2
.github/workflows/ghcr-build.yml
vendored
@@ -54,6 +54,7 @@ jobs:
|
||||
ghcr_build_app:
|
||||
name: Build App Image
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2204
|
||||
if: "!(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/ext-v'))"
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
@@ -103,6 +104,7 @@ jobs:
|
||||
ghcr_build_runtime:
|
||||
name: Build Image
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2204
|
||||
if: "!(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/ext-v'))"
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
2
.github/workflows/stale.yml
vendored
2
.github/workflows/stale.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
stale-issue-message: 'This issue is stale because it has been open for 30 days with no activity. Remove stale label or comment or this will be closed in 7 days.'
|
||||
stale-pr-message: 'This PR is stale because it has been open for 30 days with no activity. Remove stale label or comment or this will be closed in 7 days.'
|
||||
days-before-stale: 30
|
||||
exempt-issue-labels: 'tracked'
|
||||
exempt-issue-labels: 'roadmap'
|
||||
close-issue-message: 'This issue was closed because it has been stalled for over 30 days with no activity.'
|
||||
close-pr-message: 'This PR was closed because it has been stalled for over 30 days with no activity.'
|
||||
days-before-close: 7
|
||||
|
||||
156
.github/workflows/vscode-extension-build.yml
vendored
Normal file
156
.github/workflows/vscode-extension-build.yml
vendored
Normal file
@@ -0,0 +1,156 @@
|
||||
# Workflow that validates the VSCode extension builds correctly
|
||||
name: VSCode Extension CI
|
||||
|
||||
# * Always run on "main"
|
||||
# * Run on PRs that have changes in the VSCode extension folder or this workflow
|
||||
# * Run on tags that start with "ext-v"
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
tags:
|
||||
- 'ext-v*'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'openhands/integrations/vscode/**'
|
||||
- 'build_vscode.py'
|
||||
- '.github/workflows/vscode-extension-build.yml'
|
||||
|
||||
# If triggered by a PR, it will be in the same group. However, each commit on main will be in its own unique group
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ (github.head_ref && github.ref) || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
# Validate VSCode extension builds correctly
|
||||
validate-vscode-extension:
|
||||
name: Validate VSCode Extension Build
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2204
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: useblacksmith/setup-node@v5
|
||||
with:
|
||||
node-version: '22'
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
|
||||
- name: Install VSCode extension dependencies
|
||||
working-directory: ./openhands/integrations/vscode
|
||||
run: npm ci
|
||||
|
||||
- name: Build VSCode extension via build_vscode.py
|
||||
run: python build_vscode.py
|
||||
env:
|
||||
# Ensure we don't skip the build
|
||||
SKIP_VSCODE_BUILD: ""
|
||||
|
||||
- name: Validate .vsix file
|
||||
run: |
|
||||
# Verify the .vsix was created and is valid
|
||||
if [ -f "openhands/integrations/vscode/openhands-vscode-0.0.1.vsix" ]; then
|
||||
echo "✅ VSCode extension built successfully"
|
||||
ls -la openhands/integrations/vscode/openhands-vscode-0.0.1.vsix
|
||||
|
||||
# Basic validation that the .vsix is a valid zip file
|
||||
echo "🔍 Validating .vsix structure..."
|
||||
file openhands/integrations/vscode/openhands-vscode-0.0.1.vsix
|
||||
unzip -t openhands/integrations/vscode/openhands-vscode-0.0.1.vsix
|
||||
|
||||
echo "✅ VSCode extension validation passed"
|
||||
else
|
||||
echo "❌ VSCode extension build failed - .vsix not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Upload VSCode extension artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: vscode-extension
|
||||
path: openhands/integrations/vscode/openhands-vscode-0.0.1.vsix
|
||||
retention-days: 7
|
||||
|
||||
- name: Comment on PR with artifact link
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// Get file size for display
|
||||
const vsixPath = 'openhands/integrations/vscode/openhands-vscode-0.0.1.vsix';
|
||||
const stats = fs.statSync(vsixPath);
|
||||
const fileSizeKB = Math.round(stats.size / 1024);
|
||||
|
||||
const comment = `## 🔧 VSCode Extension Built Successfully!
|
||||
|
||||
The VSCode extension has been built and is ready for testing.
|
||||
|
||||
**📦 Download**: [openhands-vscode-0.0.1.vsix](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) (${fileSizeKB} KB)
|
||||
|
||||
**🚀 To install**:
|
||||
1. Download the artifact from the workflow run above
|
||||
2. In VSCode: \`Ctrl+Shift+P\` → "Extensions: Install from VSIX..."
|
||||
3. Select the downloaded \`.vsix\` file
|
||||
|
||||
**✅ Tested with**: Node.js 22
|
||||
**🔍 Validation**: File structure and integrity verified
|
||||
|
||||
---
|
||||
*Built from commit ${{ github.sha }}*`;
|
||||
|
||||
// Check if we already commented on this PR and delete it
|
||||
const { data: comments } = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
});
|
||||
|
||||
const botComment = comments.find(comment =>
|
||||
comment.user.login === 'github-actions[bot]' &&
|
||||
comment.body.includes('VSCode Extension Built Successfully')
|
||||
);
|
||||
|
||||
if (botComment) {
|
||||
await github.rest.issues.deleteComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
comment_id: botComment.id,
|
||||
});
|
||||
}
|
||||
|
||||
// Create a new comment
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
body: comment
|
||||
});
|
||||
|
||||
release:
|
||||
name: Create GitHub Release
|
||||
runs-on: blacksmith-4vcpu-ubuntu-2204
|
||||
needs: validate-vscode-extension
|
||||
if: startsWith(github.ref, 'refs/tags/ext-v')
|
||||
|
||||
steps:
|
||||
- name: Download .vsix artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: vscode-extension
|
||||
path: ./
|
||||
|
||||
- name: Create Release
|
||||
uses: ncipollo/release-action@v1.16.0
|
||||
with:
|
||||
artifacts: "*.vsix"
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
draft: true
|
||||
allowUpdates: true
|
||||
@@ -15,10 +15,13 @@ make build && make run FRONTEND_PORT=12000 FRONTEND_HOST=0.0.0.0 BACKEND_HOST=0.
|
||||
|
||||
IMPORTANT: Before making any changes to the codebase, ALWAYS run `make install-pre-commit-hooks` to ensure pre-commit hooks are properly installed.
|
||||
|
||||
|
||||
|
||||
Before pushing any changes, you MUST ensure that any lint errors or simple test errors have been fixed.
|
||||
|
||||
* If you've made changes to the backend, you should run `pre-commit run --config ./dev_config/python/.pre-commit-config.yaml` (this will run on staged files).
|
||||
* If you've made changes to the frontend, you should run `cd frontend && npm run lint:fix && npm run build ; cd ..`
|
||||
* If you've made changes to the VSCode extension, you should run `cd openhands/integrations/vscode && npm run lint:fix && npm run compile ; cd ../../..`
|
||||
|
||||
The pre-commit hooks MUST pass successfully before pushing any changes to the repository. This is a mandatory requirement to maintain code quality and consistency.
|
||||
|
||||
@@ -60,6 +63,22 @@ Frontend:
|
||||
- Mutation hooks should follow the pattern use[Action] (e.g., `useDeleteConversation`)
|
||||
- Architecture rule: UI components → TanStack Query hooks → Data Access Layer (`frontend/src/api`) → API endpoints
|
||||
|
||||
VSCode Extension:
|
||||
- Located in the `openhands/integrations/vscode` directory
|
||||
- Setup: Run `npm install` in the extension directory
|
||||
- Linting:
|
||||
- Run linting with fixes: `npm run lint:fix`
|
||||
- Check only: `npm run lint`
|
||||
- Type checking: `npm run typecheck`
|
||||
- Building:
|
||||
- Compile TypeScript: `npm run compile`
|
||||
- Package extension: `npm run package-vsix`
|
||||
- Testing:
|
||||
- Run tests: `npm run test`
|
||||
- Development Best Practices:
|
||||
- Use `vscode.window.createOutputChannel()` for debug logging instead of `showErrorMessage()` popups
|
||||
- Pre-commit process runs both frontend and backend checks when committing extension changes
|
||||
|
||||
## Template for Github Pull Request
|
||||
|
||||
If you are starting a pull request (PR), please follow the template in `.github/pull_request_template.md`.
|
||||
|
||||
@@ -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.47-nikolaik`
|
||||
Example: `export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.48-nikolaik`
|
||||
|
||||
## Develop inside Docker container
|
||||
|
||||
|
||||
@@ -62,17 +62,17 @@ system requirements and more information.
|
||||
|
||||
|
||||
```bash
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.47-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.48-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.47-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.48-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.47
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.48
|
||||
```
|
||||
|
||||
> **Note**: If you used OpenHands before version 0.44, you may want to run `mv ~/.openhands-state ~/.openhands` to migrate your conversation history to the new location.
|
||||
|
||||
@@ -51,17 +51,17 @@ OpenHands也可以使用Docker在本地系统上运行。
|
||||
|
||||
|
||||
```bash
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.47-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.48-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.47-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.48-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.47
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.48
|
||||
```
|
||||
|
||||
> **注意**: 如果您在0.44版本之前使用过OpenHands,您可能需要运行 `mv ~/.openhands-state ~/.openhands` 来将对话历史迁移到新位置。
|
||||
|
||||
@@ -42,17 +42,17 @@ OpenHandsはDockerを利用してローカル環境でも実行できます。
|
||||
> 公共ネットワークで実行していますか?[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.47-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.48-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.47-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.48-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.47
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.48
|
||||
```
|
||||
|
||||
**注**: バージョン0.44以前のOpenHandsを使用していた場合は、会話履歴を移行するために `mv ~/.openhands-state ~/.openhands` を実行してください。
|
||||
|
||||
156
REFACTOR_PLAN.md
156
REFACTOR_PLAN.md
@@ -1,156 +0,0 @@
|
||||
# Resolver Runtime Refactoring Plan
|
||||
|
||||
## Task Overview
|
||||
Refactor the resolver component to reuse setup.py functions for runtime initialization, connection, and completion instead of reinventing the wheel.
|
||||
|
||||
## Repository Cloning Patterns Analysis
|
||||
|
||||
### Repository Cloning Patterns Across OpenHands Entry Points
|
||||
|
||||
#### 1. **Resolver (issue_resolver.py)** - DIFFERENT PATTERN (Legacy)
|
||||
```python
|
||||
# Step 1: Clone to separate location
|
||||
subprocess.check_output(['git', 'clone', url, f'{output_dir}/repo'])
|
||||
|
||||
# Step 2: Later, copy repo to workspace
|
||||
shutil.copytree(os.path.join(self.output_dir, 'repo'), self.workspace_base)
|
||||
|
||||
# Step 3: Create and connect runtime
|
||||
runtime = create_runtime(config)
|
||||
await runtime.connect()
|
||||
|
||||
# Step 4: Initialize runtime (git config, setup scripts)
|
||||
self.initialize_runtime(runtime)
|
||||
```
|
||||
|
||||
#### 2. **Main.py** - STANDARD PATTERN
|
||||
```python
|
||||
# Step 1: Create and connect runtime
|
||||
runtime = create_runtime(config)
|
||||
await runtime.connect()
|
||||
|
||||
# Step 2: Clone directly into runtime workspace + setup
|
||||
repo_directory = initialize_repository_for_runtime(runtime, selected_repository)
|
||||
```
|
||||
|
||||
#### 3. **Server/Session** - STANDARD PATTERN
|
||||
```python
|
||||
# Step 1: Create and connect runtime
|
||||
# Step 2: Clone directly into runtime workspace
|
||||
await runtime.clone_or_init_repo(tokens, repo, branch)
|
||||
# Step 3: Run setup scripts
|
||||
await runtime.maybe_run_setup_script()
|
||||
await runtime.maybe_setup_git_hooks()
|
||||
```
|
||||
|
||||
#### 4. **Setup.py's initialize_repository_for_runtime()** - STANDARD PATTERN
|
||||
```python
|
||||
# Calls runtime.clone_or_init_repo() + setup scripts
|
||||
repo_directory = runtime.clone_or_init_repo(tokens, repo, branch)
|
||||
runtime.maybe_run_setup_script()
|
||||
runtime.maybe_setup_git_hooks()
|
||||
```
|
||||
|
||||
### The Issue
|
||||
The **resolver is the odd one out** - it uses a 2-step process (clone to temp location, then copy to workspace) due to **legacy reasons** (it was originally developed as a separate app built on OH, not a component of OH). All other entry points use the standard pattern (clone directly into runtime workspace).
|
||||
|
||||
## Current State Analysis
|
||||
|
||||
### ✅ What Resolver Already Does Right:
|
||||
- [x] Uses `create_runtime()` from setup.py for runtime creation
|
||||
|
||||
### ❌ What Needs to be Fixed:
|
||||
- [ ] **Resolver uses legacy 2-step cloning instead of standard runtime.clone_or_init_repo()**
|
||||
- [ ] Resolver has custom `initialize_runtime()` method that duplicates setup.py logic
|
||||
- [ ] Resolver has custom `complete_runtime()` method with no setup.py equivalent
|
||||
- [ ] Resolver doesn't follow proper runtime cleanup patterns like main.py
|
||||
- [ ] Runtime connection pattern is inconsistent across codebase
|
||||
|
||||
## Refactoring Steps
|
||||
|
||||
### Phase 1: Fix Repository Cloning Pattern (PRIORITY)
|
||||
**Goal**: Make resolver use the same repository cloning pattern as all other OpenHands entry points.
|
||||
|
||||
- [ ] **Step 1.1**: Replace resolver's legacy 2-step cloning with standard pattern
|
||||
- Remove `subprocess.check_output(['git', 'clone', ...])` from `resolve_issue()`
|
||||
- Remove `shutil.copytree()` from `process_issue()`
|
||||
- Use `initialize_repository_for_runtime()` instead
|
||||
- This will clone directly into runtime workspace AND run setup scripts
|
||||
|
||||
- [ ] **Step 1.2**: Update resolver workflow to match standard pattern
|
||||
- Create and connect runtime first
|
||||
- Then call `initialize_repository_for_runtime()` for cloning + setup
|
||||
- Remove the manual repo copying step entirely
|
||||
- Ensure base_commit is still captured correctly
|
||||
|
||||
### Phase 2: Refactor Runtime Initialization and Completion
|
||||
**Goal**: Remove code duplication between resolver and setup.py for runtime operations.
|
||||
|
||||
- [ ] **Step 2.1**: Create missing functions in setup.py
|
||||
- Create `setup_runtime_environment()` for git config and platform-specific setup
|
||||
- Create `complete_runtime_session()` for git patch generation
|
||||
- Create `cleanup_runtime()` for proper resource cleanup
|
||||
|
||||
- [ ] **Step 2.2**: Replace resolver's `initialize_runtime()`
|
||||
- Use setup.py's `setup_runtime_environment()` instead
|
||||
- Remove duplicate git configuration code
|
||||
- Maintain platform-specific behavior (GitLab CI)
|
||||
|
||||
- [ ] **Step 2.3**: Replace resolver's `complete_runtime()`
|
||||
- Use setup.py's `complete_runtime_session()` instead
|
||||
- Move git patch generation logic to setup.py
|
||||
- Ensure return values match resolver's expectations
|
||||
|
||||
- [ ] **Step 2.4**: Add proper runtime cleanup to resolver
|
||||
- Use setup.py's `cleanup_runtime()` function
|
||||
- Ensure resources are properly released in try/finally blocks
|
||||
|
||||
### Phase 3: Testing and Validation
|
||||
- [ ] **Step 3.1**: Test resolver functionality with refactored code
|
||||
- Verify git operations work correctly
|
||||
- Verify setup scripts are executed
|
||||
- Verify git hooks are set up
|
||||
|
||||
- [ ] **Step 3.2**: Test runtime lifecycle (create → connect → clone → initialize → complete → cleanup)
|
||||
- Ensure no resource leaks
|
||||
- Verify proper error handling
|
||||
|
||||
- [ ] **Step 3.3**: Verify resolver output remains consistent
|
||||
- Git patches are generated correctly
|
||||
- Issue resolution works as before
|
||||
- No regression in functionality
|
||||
|
||||
### Phase 4: Code Quality and Documentation
|
||||
- [ ] **Step 4.1**: Add proper documentation to new setup.py functions
|
||||
- Document parameters and return values
|
||||
- Add usage examples
|
||||
- Document platform-specific behavior
|
||||
|
||||
- [ ] **Step 4.2**: Remove obsolete code from resolver
|
||||
- Delete old `initialize_runtime()` method
|
||||
- Delete old `complete_runtime()` method
|
||||
- Clean up imports and unused code
|
||||
|
||||
- [ ] **Step 4.3**: Update any other components that might benefit from these functions
|
||||
- Check if other entry points could use the same patterns
|
||||
- Ensure consistency across the codebase
|
||||
|
||||
## Success Criteria
|
||||
- [ ] **Resolver uses standard repository cloning pattern (runtime.clone_or_init_repo)**
|
||||
- [ ] Resolver uses setup.py functions for all runtime operations
|
||||
- [ ] No code duplication between resolver and setup.py
|
||||
- [ ] Proper runtime lifecycle management (connect → initialize → complete → cleanup)
|
||||
- [ ] All existing resolver functionality preserved
|
||||
- [ ] Consistent patterns across all OpenHands entry points
|
||||
- [ ] Proper error handling and resource cleanup
|
||||
|
||||
## Files to Modify
|
||||
1. `/openhands/core/setup.py` - Add new runtime management functions
|
||||
2. `/openhands/resolver/issue_resolver.py` - Refactor to use setup.py functions
|
||||
3. Any tests related to resolver functionality
|
||||
|
||||
## Risk Mitigation
|
||||
- Maintain backward compatibility during refactoring
|
||||
- Test thoroughly before removing old code
|
||||
- Keep git patch generation logic identical to avoid breaking issue resolution
|
||||
- Ensure platform-specific behavior (GitLab CI) is preserved
|
||||
114
build_vscode.py
Normal file
114
build_vscode.py
Normal file
@@ -0,0 +1,114 @@
|
||||
import os
|
||||
import pathlib
|
||||
import subprocess
|
||||
|
||||
# This script is intended to be run by Poetry during the build process.
|
||||
|
||||
# Define the expected name of the .vsix file based on the extension's package.json
|
||||
# This should match the name and version in openhands-vscode/package.json
|
||||
EXTENSION_NAME = 'openhands-vscode'
|
||||
EXTENSION_VERSION = '0.0.1'
|
||||
VSIX_FILENAME = f'{EXTENSION_NAME}-{EXTENSION_VERSION}.vsix'
|
||||
|
||||
# Paths
|
||||
ROOT_DIR = pathlib.Path(__file__).parent.resolve()
|
||||
VSCODE_EXTENSION_DIR = ROOT_DIR / 'openhands' / 'integrations' / 'vscode'
|
||||
|
||||
|
||||
def check_node_version():
|
||||
"""Check if Node.js version is sufficient for building the extension."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['node', '--version'], capture_output=True, text=True, check=True
|
||||
)
|
||||
version_str = result.stdout.strip()
|
||||
# Extract major version number (e.g., "v12.22.9" -> 12)
|
||||
major_version = int(version_str.lstrip('v').split('.')[0])
|
||||
return major_version >= 18 # Align with frontend actual usage (18.20.1)
|
||||
except (subprocess.CalledProcessError, FileNotFoundError, ValueError):
|
||||
return False
|
||||
|
||||
|
||||
def build_vscode_extension():
|
||||
"""Builds the VS Code extension."""
|
||||
vsix_path = VSCODE_EXTENSION_DIR / VSIX_FILENAME
|
||||
|
||||
# Check if VSCode extension build is disabled via environment variable
|
||||
if os.environ.get('SKIP_VSCODE_BUILD', '').lower() in ('1', 'true', 'yes'):
|
||||
print('--- Skipping VS Code extension build (SKIP_VSCODE_BUILD is set) ---')
|
||||
if vsix_path.exists():
|
||||
print(f'--- Using existing VS Code extension: {vsix_path} ---')
|
||||
else:
|
||||
print('--- No pre-built VS Code extension found ---')
|
||||
return
|
||||
|
||||
# Check Node.js version - if insufficient, use pre-built extension as fallback
|
||||
if not check_node_version():
|
||||
print('--- Warning: Node.js version < 18 detected or Node.js not found ---')
|
||||
print('--- Skipping VS Code extension build (requires Node.js >= 18) ---')
|
||||
print('--- Using pre-built extension if available ---')
|
||||
|
||||
if not vsix_path.exists():
|
||||
print('--- Warning: No pre-built VS Code extension found ---')
|
||||
print('--- VS Code extension will not be available ---')
|
||||
else:
|
||||
print(f'--- Using pre-built VS Code extension: {vsix_path} ---')
|
||||
return
|
||||
|
||||
print(f'--- Building VS Code extension in {VSCODE_EXTENSION_DIR} ---')
|
||||
|
||||
try:
|
||||
# Ensure npm dependencies are installed
|
||||
print('--- Running npm install for VS Code extension ---')
|
||||
subprocess.run(
|
||||
['npm', 'install'],
|
||||
cwd=VSCODE_EXTENSION_DIR,
|
||||
check=True,
|
||||
shell=os.name == 'nt',
|
||||
)
|
||||
|
||||
# Package the extension
|
||||
print(f'--- Packaging VS Code extension ({VSIX_FILENAME}) ---')
|
||||
subprocess.run(
|
||||
['npm', 'run', 'package-vsix'],
|
||||
cwd=VSCODE_EXTENSION_DIR,
|
||||
check=True,
|
||||
shell=os.name == 'nt',
|
||||
)
|
||||
|
||||
# Verify the generated .vsix file exists
|
||||
if not vsix_path.exists():
|
||||
raise FileNotFoundError(
|
||||
f'VS Code extension package not found after build: {vsix_path}'
|
||||
)
|
||||
|
||||
print(f'--- VS Code extension built successfully: {vsix_path} ---')
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f'--- Warning: Failed to build VS Code extension: {e} ---')
|
||||
print('--- Continuing without building extension ---')
|
||||
if not vsix_path.exists():
|
||||
print('--- Warning: No pre-built VS Code extension found ---')
|
||||
print('--- VS Code extension will not be available ---')
|
||||
|
||||
|
||||
def build(setup_kwargs):
|
||||
"""
|
||||
This function is called by Poetry during the build process.
|
||||
`setup_kwargs` is a dictionary that will be passed to `setuptools.setup()`.
|
||||
"""
|
||||
print('--- Running custom Poetry build script (build_vscode.py) ---')
|
||||
|
||||
# Build the VS Code extension and place the .vsix file
|
||||
build_vscode_extension()
|
||||
|
||||
# Poetry will handle including files based on pyproject.toml `include` patterns.
|
||||
# Ensure openhands/integrations/vscode/*.vsix is included there.
|
||||
|
||||
print('--- Custom Poetry build script (build_vscode.py) finished ---')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
print('Running build_vscode.py directly for testing VS Code extension packaging...')
|
||||
build_vscode_extension()
|
||||
print('Direct execution of build_vscode.py finished.')
|
||||
@@ -49,6 +49,9 @@
|
||||
# Maximum file size for uploads, in megabytes
|
||||
#file_uploads_max_file_size_mb = 0
|
||||
|
||||
# Enable the browser environment
|
||||
#enable_browser = true
|
||||
|
||||
# Maximum budget per task, 0.0 means no limit
|
||||
#max_budget_per_task = 0.0
|
||||
|
||||
@@ -226,6 +229,7 @@ model = "gpt-4o"
|
||||
[agent]
|
||||
|
||||
# Whether the browsing tool is enabled
|
||||
# Note: when this is set to true, enable_browser in the core config must also be true
|
||||
enable_browsing = true
|
||||
|
||||
# Whether the LLM draft editor is enabled
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
ARG OPENHANDS_BUILD_VERSION=dev
|
||||
FROM node:22.16.0-bookworm-slim AS frontend-builder
|
||||
FROM node:24.3.0-bookworm-slim AS frontend-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.47-nikolaik}
|
||||
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.48-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.47-nikolaik}
|
||||
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-docker.all-hands.dev/all-hands-ai/runtime:0.48-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:
|
||||
|
||||
@@ -64,7 +64,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.47-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.48-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.47 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.48 \
|
||||
python -m openhands.cli.main --override-cli-mode true
|
||||
```
|
||||
|
||||
|
||||
@@ -18,42 +18,78 @@ poetry run python -m openhands.core.main -t "write a bash script that prints hi"
|
||||
You'll need to be sure to set your model, API key, and other settings via environment variables
|
||||
[or the `config.toml` file](https://github.com/All-Hands-AI/OpenHands/blob/main/config.template.toml).
|
||||
|
||||
## With Docker
|
||||
### Working with Repositories
|
||||
|
||||
To run OpenHands in Headless mode with Docker:
|
||||
You can specify a repository for OpenHands to work with using `--selected-repo` or the `SANDBOX_SELECTED_REPO` environment variable:
|
||||
|
||||
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_API_KEY` - your API key (e.g. `export LLM_API_KEY="sk_test_12345"`)
|
||||
|
||||
2. Run the following Docker command:
|
||||
> **Note**: Currently, authentication tokens (GITHUB_TOKEN, GITLAB_TOKEN, or BITBUCKET_TOKEN) are required for all repository operations, including public repositories. This is a known limitation that may be addressed in future versions to allow tokenless access to public repositories.
|
||||
|
||||
```bash
|
||||
# Using command-line argument
|
||||
poetry run python -m openhands.core.main \
|
||||
--selected-repo "owner/repo-name" \
|
||||
-t "analyze the codebase and suggest improvements"
|
||||
|
||||
# Using environment variable
|
||||
export SANDBOX_SELECTED_REPO="owner/repo-name"
|
||||
poetry run python -m openhands.core.main -t "fix any linting issues"
|
||||
|
||||
# Authentication tokens are currently required for ALL repository operations (public and private)
|
||||
# This includes GitHub, GitLab, and Bitbucket repositories
|
||||
export GITHUB_TOKEN="your-token" # or GITLAB_TOKEN, BITBUCKET_TOKEN
|
||||
poetry run python -m openhands.core.main \
|
||||
--selected-repo "owner/repo-name" \
|
||||
-t "review the security implementation"
|
||||
|
||||
# Using task files instead of inline task
|
||||
echo "Review the README and suggest improvements" > task.txt
|
||||
poetry run python -m openhands.core.main -f task.txt --selected-repo "owner/repo"
|
||||
```
|
||||
|
||||
## With Docker
|
||||
|
||||
Set environment variables and run the Docker command:
|
||||
|
||||
```bash
|
||||
# Set required environment variables
|
||||
export SANDBOX_VOLUMES="/path/to/workspace" # See SANDBOX_VOLUMES docs for details
|
||||
export LLM_MODEL="anthropic/claude-sonnet-4-20250514"
|
||||
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
|
||||
|
||||
# Run OpenHands
|
||||
docker run -it \
|
||||
--pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.47-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.48-nikolaik \
|
||||
-e SANDBOX_USER_ID=$(id -u) \
|
||||
-e SANDBOX_VOLUMES=$SANDBOX_VOLUMES \
|
||||
-e LLM_API_KEY=$LLM_API_KEY \
|
||||
-e LLM_MODEL=$LLM_MODEL \
|
||||
-e SANDBOX_SELECTED_REPO=$SANDBOX_SELECTED_REPO \
|
||||
-e GITHUB_TOKEN=$GITHUB_TOKEN \
|
||||
-e LOG_ALL_EVENTS=true \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-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.47 \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.48 \
|
||||
python -m openhands.core.main -t "write a bash script that prints hi"
|
||||
```
|
||||
> **Note**: If you used OpenHands before version 0.44, you may want to run `mv ~/.openhands-state ~/.openhands` to migrate your conversation history to the new location.
|
||||
|
||||
> **Note**: If you used OpenHands before version 0.44, run `mv ~/.openhands-state ~/.openhands` to migrate your conversation history.
|
||||
|
||||
The `-e SANDBOX_USER_ID=$(id -u)` is passed to the Docker command to ensure the sandbox user matches the host user’s
|
||||
permissions. This prevents the agent from creating root-owned files in the mounted workspace.
|
||||
|
||||
## Advanced Headless Configurations
|
||||
## Additional Options
|
||||
|
||||
To view all available configuration options for headless mode, run the Python command with the `--help` flag.
|
||||
Common command-line options:
|
||||
- `-d "/path/to/workspace"` - Set working directory
|
||||
- `-f task.txt` - Load task from file
|
||||
- `-i 50` - Set max iterations
|
||||
- `-b 10.0` - Set budget limit (USD)
|
||||
- `--no-auto-continue` - Interactive mode
|
||||
|
||||
### Additional Logs
|
||||
Run `poetry run python -m openhands.core.main --help` for all options, or use a [`config.toml` file](https://github.com/All-Hands-AI/OpenHands/blob/main/config.template.toml) for more flexibility.
|
||||
|
||||
For the headless mode to log all the agent actions, in the terminal run: `export LOG_ALL_EVENTS=true`
|
||||
Set `export LOG_ALL_EVENTS=true` to log all agent actions.
|
||||
|
||||
@@ -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.47-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.48-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.47-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.48-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.47
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.48
|
||||
```
|
||||
|
||||
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.47
|
||||
Status: Image is up to date for docker.all-hands.dev/all-hands-ai/openhands:0.48
|
||||
Starting OpenHands...
|
||||
Running OpenHands as root
|
||||
14:22:13 - openhands:INFO: server_config.py:50 - Using config class None
|
||||
@@ -175,6 +175,27 @@ vllm serve mistralai/Devstral-Small-2505 \
|
||||
--enable-prefix-caching
|
||||
```
|
||||
|
||||
If you are interested in further improved inference speed, you can also try Snowflake's version
|
||||
of vLLM, [ArcticInference](https://www.snowflake.com/en/engineering-blog/fast-speculative-decoding-vllm-arctic/),
|
||||
which can achieve up to 2x speedup in some cases.
|
||||
|
||||
1. Install the Arctic Inference library that automatically patches vLLM:
|
||||
|
||||
```bash
|
||||
pip install git+https://github.com/snowflakedb/ArcticInference.git
|
||||
```
|
||||
|
||||
2. Run the launch command with speculative decoding enabled:
|
||||
|
||||
```bash
|
||||
vllm serve mistralai/Devstral-Small-2505 \
|
||||
--host 0.0.0.0 --port 8000 \
|
||||
--api-key mykey \
|
||||
--tensor-parallel-size 2 \
|
||||
--served-model-name Devstral-Small-2505 \
|
||||
--speculative-config '{"method": "suffix"}'
|
||||
```
|
||||
|
||||
### Run OpenHands (Alternative Backends)
|
||||
|
||||
#### Using Docker
|
||||
|
||||
@@ -67,17 +67,17 @@ A system with a modern processor and a minimum of **4GB RAM** is recommended to
|
||||
### Start the App
|
||||
|
||||
```bash
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.47-nikolaik
|
||||
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.48-nikolaik
|
||||
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.47-nikolaik \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.48-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.47
|
||||
docker.all-hands.dev/all-hands-ai/openhands:0.48
|
||||
```
|
||||
|
||||
> **Note**: If you used OpenHands before version 0.44, you may want to run `mv ~/.openhands-state ~/.openhands` to migrate your conversation history to the new location.
|
||||
|
||||
@@ -38,6 +38,21 @@ On initial prompt, an error is seen with `Permission Denied` or `PermissionError
|
||||
* If mounting a local directory, ensure your `WORKSPACE_BASE` has the necessary permissions for the user running
|
||||
OpenHands.
|
||||
|
||||
### Internal Server Error. Ports are not available
|
||||
|
||||
**Description**
|
||||
|
||||
When running on Windows, the error `Internal Server Error ("ports are not available: exposing port TCP
|
||||
...: bind: An attempt was made to access a socket in a
|
||||
way forbidden by its access permissions.")` is encountered.
|
||||
|
||||
**Resolution**
|
||||
|
||||
* Run the following command in PowerShell, as Administrator to reset the NAT service and release the ports:
|
||||
```
|
||||
Restart-Service -Name "winnat"
|
||||
```
|
||||
|
||||
### Unable to access VS Code tab via local IP
|
||||
|
||||
**Description**
|
||||
|
||||
176
flake.lock
generated
Normal file
176
flake.lock
generated
Normal file
@@ -0,0 +1,176 @@
|
||||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731533236,
|
||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils_2": {
|
||||
"inputs": {
|
||||
"systems": "systems_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1726560853,
|
||||
"narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nix-github-actions": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"poetry2nix",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1729742964,
|
||||
"narHash": "sha256-B4mzTcQ0FZHdpeWcpDYPERtyjJd/NIuaQ9+BV1h+MpA=",
|
||||
"owner": "nix-community",
|
||||
"repo": "nix-github-actions",
|
||||
"rev": "e04df33f62cdcf93d73e9a04142464753a16db67",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"repo": "nix-github-actions",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1751792365,
|
||||
"narHash": "sha256-J1kI6oAj25IG4EdVlg2hQz8NZTBNYvIS0l4wpr9KcUo=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "1fd8bada0b6117e6c7eb54aad5813023eed37ccb",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"poetry2nix": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils_2",
|
||||
"nix-github-actions": "nix-github-actions",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
],
|
||||
"systems": "systems_3",
|
||||
"treefmt-nix": "treefmt-nix"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1743690424,
|
||||
"narHash": "sha256-cX98bUuKuihOaRp8dNV1Mq7u6/CQZWTPth2IJPATBXc=",
|
||||
"owner": "nix-community",
|
||||
"repo": "poetry2nix",
|
||||
"rev": "ce2369db77f45688172384bbeb962bc6c2ea6f94",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"repo": "poetry2nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs",
|
||||
"poetry2nix": "poetry2nix"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems_2": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"systems_3": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"treefmt-nix": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"poetry2nix",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1730120726,
|
||||
"narHash": "sha256-LqHYIxMrl/1p3/kvm2ir925tZ8DkI0KA10djk8wecSk=",
|
||||
"owner": "numtide",
|
||||
"repo": "treefmt-nix",
|
||||
"rev": "9ef337e492a5555d8e17a51c911ff1f02635be15",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "treefmt-nix",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
81
flake.nix
Normal file
81
flake.nix
Normal file
@@ -0,0 +1,81 @@
|
||||
{
|
||||
description = "Development environment with Python, Node.js, Poetry, and build tools";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
poetry2nix = {
|
||||
url = "github:nix-community/poetry2nix";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, flake-utils, poetry2nix }:
|
||||
flake-utils.lib.eachDefaultSystem (system:
|
||||
let
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
|
||||
# Check if we're on WSL
|
||||
isWSL = builtins.pathExists /proc/sys/fs/binfmt_misc/WSLInterop;
|
||||
|
||||
# Python 3.12 with development headers
|
||||
python312 = pkgs.python312;
|
||||
|
||||
# Poetry 1.8.x
|
||||
poetry = pkgs.poetry;
|
||||
|
||||
# Build essential tools
|
||||
buildTools = with pkgs; [
|
||||
gcc
|
||||
gnumake
|
||||
cmake
|
||||
pkg-config
|
||||
autoconf
|
||||
automake
|
||||
libtool
|
||||
];
|
||||
|
||||
# Development headers for Python
|
||||
pythonDev = with pkgs; [
|
||||
python312.pkgs.pip
|
||||
python312.pkgs.setuptools
|
||||
python312.pkgs.wheel
|
||||
# Python development headers are included with python312
|
||||
];
|
||||
|
||||
# Optional WSL-specific tools
|
||||
wslTools = with pkgs; lib.optionals isWSL [
|
||||
netcat-gnu
|
||||
];
|
||||
|
||||
in
|
||||
{
|
||||
devShells.default = pkgs.mkShell {
|
||||
buildInputs = with pkgs; [
|
||||
python312
|
||||
nodejs_22
|
||||
poetry
|
||||
pre-commit
|
||||
] ++ buildTools ++ pythonDev ++ wslTools;
|
||||
|
||||
shellHook = ''
|
||||
echo "Development environment loaded!"
|
||||
echo "Python: $(python --version)"
|
||||
echo "Node.js: $(node --version)"
|
||||
echo "Poetry: $(poetry --version)"
|
||||
echo "GCC: $(gcc --version | head -n1)"
|
||||
${if isWSL then ''echo "WSL detected - netcat included"'' else ""}
|
||||
echo ""
|
||||
echo "All development dependencies are ready."
|
||||
'';
|
||||
|
||||
# Set up environment variables
|
||||
PYTHON = "${python312}/bin/python";
|
||||
NODE_PATH = "${pkgs.nodejs_22}/lib/node_modules";
|
||||
|
||||
# Ensure Python can find its headers
|
||||
C_INCLUDE_PATH = "${python312}/include/python3.12";
|
||||
CPLUS_INCLUDE_PATH = "${python312}/include/python3.12";
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -27,9 +27,9 @@ vi.mock("react-i18next", async () => {
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => {
|
||||
const translations: Record<string, string> = {
|
||||
"CONVERSATION$CREATED": "Created",
|
||||
"CONVERSATION$AGO": "ago",
|
||||
"CONVERSATION$UPDATED": "Updated"
|
||||
CONVERSATION$CREATED: "Created",
|
||||
CONVERSATION$AGO: "ago",
|
||||
CONVERSATION$UPDATED: "Updated",
|
||||
};
|
||||
return translations[key] || key;
|
||||
},
|
||||
@@ -82,7 +82,9 @@ describe("ConversationCard", () => {
|
||||
expect(card).toHaveTextContent("ago");
|
||||
|
||||
// Use a regex to match the time part since it might have whitespace
|
||||
const timeRegex = new RegExp(formatTimeDelta(new Date("2021-10-01T12:00:00Z")));
|
||||
const timeRegex = new RegExp(
|
||||
formatTimeDelta(new Date("2021-10-01T12:00:00Z")),
|
||||
);
|
||||
expect(card).toHaveTextContent(timeRegex);
|
||||
});
|
||||
|
||||
@@ -108,7 +110,11 @@ describe("ConversationCard", () => {
|
||||
onChangeTitle={onChangeTitle}
|
||||
isActive
|
||||
title="Conversation 1"
|
||||
selectedRepository="org/selectedRepository"
|
||||
selectedRepository={{
|
||||
selected_repository: "org/selectedRepository",
|
||||
selected_branch: "main",
|
||||
git_provider: "github",
|
||||
}}
|
||||
lastUpdatedAt="2021-10-01T12:00:00Z"
|
||||
/>,
|
||||
);
|
||||
@@ -173,7 +179,11 @@ describe("ConversationCard", () => {
|
||||
isActive
|
||||
onChangeTitle={onChangeTitle}
|
||||
title="Conversation 1"
|
||||
selectedRepository="org/selectedRepository"
|
||||
selectedRepository={{
|
||||
selected_repository: "org/selectedRepository",
|
||||
selected_branch: "main",
|
||||
git_provider: "github",
|
||||
}}
|
||||
lastUpdatedAt="2021-10-01T12:00:00Z"
|
||||
/>,
|
||||
);
|
||||
|
||||
@@ -295,4 +295,238 @@ describe("ConversationPanel", () => {
|
||||
const newCards = await screen.findAllByTestId("conversation-card");
|
||||
expect(newCards).toHaveLength(3);
|
||||
});
|
||||
|
||||
it("should cancel stopping a conversation", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
// Create mock data with a RUNNING conversation
|
||||
const mockRunningConversations: Conversation[] = [
|
||||
{
|
||||
conversation_id: "1",
|
||||
title: "Running Conversation",
|
||||
selected_repository: null,
|
||||
git_provider: null,
|
||||
selected_branch: null,
|
||||
last_updated_at: "2021-10-01T12:00:00Z",
|
||||
created_at: "2021-10-01T12:00:00Z",
|
||||
status: "RUNNING" as const,
|
||||
runtime_status: null,
|
||||
url: null,
|
||||
session_api_key: null,
|
||||
},
|
||||
{
|
||||
conversation_id: "2",
|
||||
title: "Starting Conversation",
|
||||
selected_repository: null,
|
||||
git_provider: null,
|
||||
selected_branch: null,
|
||||
last_updated_at: "2021-10-02T12:00:00Z",
|
||||
created_at: "2021-10-02T12:00:00Z",
|
||||
status: "STARTING" as const,
|
||||
runtime_status: null,
|
||||
url: null,
|
||||
session_api_key: null,
|
||||
},
|
||||
{
|
||||
conversation_id: "3",
|
||||
title: "Stopped Conversation",
|
||||
selected_repository: null,
|
||||
git_provider: null,
|
||||
selected_branch: null,
|
||||
last_updated_at: "2021-10-03T12:00:00Z",
|
||||
created_at: "2021-10-03T12:00:00Z",
|
||||
status: "STOPPED" as const,
|
||||
runtime_status: null,
|
||||
url: null,
|
||||
session_api_key: null,
|
||||
},
|
||||
];
|
||||
|
||||
const getUserConversationsSpy = vi.spyOn(OpenHands, "getUserConversations");
|
||||
getUserConversationsSpy.mockResolvedValue(mockRunningConversations);
|
||||
|
||||
renderConversationPanel();
|
||||
|
||||
const cards = await screen.findAllByTestId("conversation-card");
|
||||
expect(cards).toHaveLength(3);
|
||||
|
||||
// Click ellipsis on the first card (RUNNING status)
|
||||
const ellipsisButton = within(cards[0]).getByTestId("ellipsis-button");
|
||||
await user.click(ellipsisButton);
|
||||
|
||||
// Stop button should be available for RUNNING conversation
|
||||
const stopButton = screen.getByTestId("stop-button");
|
||||
expect(stopButton).toBeInTheDocument();
|
||||
|
||||
// Click the stop button
|
||||
await user.click(stopButton);
|
||||
|
||||
// Cancel the stopping action
|
||||
const cancelButton = screen.getByRole("button", { name: /cancel/i });
|
||||
await user.click(cancelButton);
|
||||
|
||||
expect(
|
||||
screen.queryByRole("button", { name: /cancel/i }),
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
// Ensure the conversation status hasn't changed
|
||||
const updatedCards = await screen.findAllByTestId("conversation-card");
|
||||
expect(updatedCards).toHaveLength(3);
|
||||
});
|
||||
|
||||
it("should stop a conversation", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
const mockData: Conversation[] = [
|
||||
{
|
||||
conversation_id: "1",
|
||||
title: "Running Conversation",
|
||||
selected_repository: null,
|
||||
git_provider: null,
|
||||
selected_branch: null,
|
||||
last_updated_at: "2021-10-01T12:00:00Z",
|
||||
created_at: "2021-10-01T12:00:00Z",
|
||||
status: "RUNNING" as const,
|
||||
runtime_status: null,
|
||||
url: null,
|
||||
session_api_key: null,
|
||||
},
|
||||
{
|
||||
conversation_id: "2",
|
||||
title: "Starting Conversation",
|
||||
selected_repository: null,
|
||||
git_provider: null,
|
||||
selected_branch: null,
|
||||
last_updated_at: "2021-10-02T12:00:00Z",
|
||||
created_at: "2021-10-02T12:00:00Z",
|
||||
status: "STARTING" as const,
|
||||
runtime_status: null,
|
||||
url: null,
|
||||
session_api_key: null,
|
||||
},
|
||||
];
|
||||
|
||||
const getUserConversationsSpy = vi.spyOn(OpenHands, "getUserConversations");
|
||||
getUserConversationsSpy.mockImplementation(async () => mockData);
|
||||
|
||||
const stopConversationSpy = vi.spyOn(OpenHands, "stopConversation");
|
||||
stopConversationSpy.mockImplementation(async (id: string) => {
|
||||
const conversation = mockData.find((conv) => conv.conversation_id === id);
|
||||
if (conversation) {
|
||||
conversation.status = "STOPPED";
|
||||
return conversation;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
renderConversationPanel();
|
||||
|
||||
const cards = await screen.findAllByTestId("conversation-card");
|
||||
expect(cards).toHaveLength(2);
|
||||
|
||||
// Click ellipsis on the first card (RUNNING status)
|
||||
const ellipsisButton = within(cards[0]).getByTestId("ellipsis-button");
|
||||
await user.click(ellipsisButton);
|
||||
|
||||
const stopButton = screen.getByTestId("stop-button");
|
||||
|
||||
// Click the stop button
|
||||
await user.click(stopButton);
|
||||
|
||||
// Confirm the stopping action
|
||||
const confirmButton = screen.getByRole("button", { name: /confirm/i });
|
||||
await user.click(confirmButton);
|
||||
|
||||
expect(
|
||||
screen.queryByRole("button", { name: /confirm/i }),
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
// Verify the API was called
|
||||
expect(stopConversationSpy).toHaveBeenCalledWith("1");
|
||||
expect(stopConversationSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should only show stop button for STARTING or RUNNING conversations", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
const mockMixedStatusConversations: Conversation[] = [
|
||||
{
|
||||
conversation_id: "1",
|
||||
title: "Running Conversation",
|
||||
selected_repository: null,
|
||||
git_provider: null,
|
||||
selected_branch: null,
|
||||
last_updated_at: "2021-10-01T12:00:00Z",
|
||||
created_at: "2021-10-01T12:00:00Z",
|
||||
status: "RUNNING" as const,
|
||||
runtime_status: null,
|
||||
url: null,
|
||||
session_api_key: null,
|
||||
},
|
||||
{
|
||||
conversation_id: "2",
|
||||
title: "Starting Conversation",
|
||||
selected_repository: null,
|
||||
git_provider: null,
|
||||
selected_branch: null,
|
||||
last_updated_at: "2021-10-02T12:00:00Z",
|
||||
created_at: "2021-10-02T12:00:00Z",
|
||||
status: "STARTING" as const,
|
||||
runtime_status: null,
|
||||
url: null,
|
||||
session_api_key: null,
|
||||
},
|
||||
{
|
||||
conversation_id: "3",
|
||||
title: "Stopped Conversation",
|
||||
selected_repository: null,
|
||||
git_provider: null,
|
||||
selected_branch: null,
|
||||
last_updated_at: "2021-10-03T12:00:00Z",
|
||||
created_at: "2021-10-03T12:00:00Z",
|
||||
status: "STOPPED" as const,
|
||||
runtime_status: null,
|
||||
url: null,
|
||||
session_api_key: null,
|
||||
},
|
||||
];
|
||||
|
||||
const getUserConversationsSpy = vi.spyOn(OpenHands, "getUserConversations");
|
||||
getUserConversationsSpy.mockResolvedValue(mockMixedStatusConversations);
|
||||
|
||||
renderConversationPanel();
|
||||
|
||||
const cards = await screen.findAllByTestId("conversation-card");
|
||||
expect(cards).toHaveLength(3);
|
||||
|
||||
// Test RUNNING conversation - should show stop button
|
||||
const runningEllipsisButton = within(cards[0]).getByTestId(
|
||||
"ellipsis-button",
|
||||
);
|
||||
await user.click(runningEllipsisButton);
|
||||
|
||||
expect(screen.getByTestId("stop-button")).toBeInTheDocument();
|
||||
|
||||
// Click outside to close the menu
|
||||
await user.click(document.body);
|
||||
|
||||
// Test STARTING conversation - should show stop button
|
||||
const startingEllipsisButton = within(cards[1]).getByTestId(
|
||||
"ellipsis-button",
|
||||
);
|
||||
await user.click(startingEllipsisButton);
|
||||
|
||||
expect(screen.getByTestId("stop-button")).toBeInTheDocument();
|
||||
|
||||
// Click outside to close the menu
|
||||
await user.click(document.body);
|
||||
|
||||
// Test STOPPED conversation - should NOT show stop button
|
||||
const stoppedEllipsisButton = within(cards[2]).getByTestId(
|
||||
"ellipsis-button",
|
||||
);
|
||||
await user.click(stoppedEllipsisButton);
|
||||
|
||||
expect(screen.queryByTestId("stop-button")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -119,18 +119,48 @@ describe("RepoConnector", () => {
|
||||
expect(launchButton).toBeEnabled();
|
||||
});
|
||||
|
||||
it("should render the 'add git(hub|lab) repos' links if saas mode", async () => {
|
||||
it("should render the 'add github repos' link if saas mode and github provider is set", async () => {
|
||||
const getConfiSpy = vi.spyOn(OpenHands, "getConfig");
|
||||
// @ts-expect-error - only return the APP_MODE
|
||||
getConfiSpy.mockResolvedValue({
|
||||
APP_MODE: "saas",
|
||||
});
|
||||
|
||||
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
provider_tokens_set: {
|
||||
github: "some-token",
|
||||
gitlab: null,
|
||||
},
|
||||
});
|
||||
|
||||
renderRepoConnector();
|
||||
|
||||
await screen.findByText("HOME$ADD_GITHUB_REPOS");
|
||||
});
|
||||
|
||||
it("should not render the 'add github repos' link if github provider is not set", async () => {
|
||||
const getConfiSpy = vi.spyOn(OpenHands, "getConfig");
|
||||
// @ts-expect-error - only return the APP_MODE
|
||||
getConfiSpy.mockResolvedValue({
|
||||
APP_MODE: "saas",
|
||||
});
|
||||
|
||||
const getSettingsSpy = vi.spyOn(OpenHands, "getSettings");
|
||||
getSettingsSpy.mockResolvedValue({
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
provider_tokens_set: {
|
||||
gitlab: "some-token",
|
||||
github: null,
|
||||
},
|
||||
});
|
||||
|
||||
renderRepoConnector();
|
||||
|
||||
expect(screen.queryByText("HOME$ADD_GITHUB_REPOS")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should not render the 'add git(hub|lab) repos' links if oss mode", async () => {
|
||||
const getConfiSpy = vi.spyOn(OpenHands, "getConfig");
|
||||
// @ts-expect-error - only return the APP_MODE
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import { render, screen, waitFor, within } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { Provider } from "react-redux";
|
||||
@@ -7,6 +7,21 @@ 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";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
|
||||
// Mock the translation function
|
||||
vi.mock("react-i18next", async () => {
|
||||
const actual = await vi.importActual("react-i18next");
|
||||
return {
|
||||
...(actual as object),
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
i18n: {
|
||||
changeLanguage: () => new Promise(() => {}),
|
||||
},
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
const renderTaskSuggestions = () => {
|
||||
const RouterStub = createRoutesStub([
|
||||
@@ -93,4 +108,26 @@ describe("TaskSuggestions", () => {
|
||||
|
||||
expect(screen.queryByTestId("task-group-skeleton")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render the tooltip button", () => {
|
||||
renderTaskSuggestions();
|
||||
const tooltipButton = screen.getByTestId("task-suggestions-info");
|
||||
expect(tooltipButton).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should have the correct aria-label", () => {
|
||||
renderTaskSuggestions();
|
||||
const tooltipButton = screen.getByTestId("task-suggestions-info");
|
||||
expect(tooltipButton).toHaveAttribute(
|
||||
"aria-label",
|
||||
"TASKS$TASK_SUGGESTIONS_INFO",
|
||||
);
|
||||
});
|
||||
|
||||
it("should render the info icon", () => {
|
||||
renderTaskSuggestions();
|
||||
const tooltipButton = screen.getByTestId("task-suggestions-info");
|
||||
const icon = tooltipButton.querySelector("svg");
|
||||
expect(icon).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import { screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { renderWithProviders } from "test-utils";
|
||||
import { MicroagentsModal } from "#/components/features/conversation-panel/microagents-modal";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import { AgentState } from "#/types/agent-state";
|
||||
|
||||
vi.mock("react-redux", async () => {
|
||||
const actual = await vi.importActual("react-redux");
|
||||
return {
|
||||
...actual,
|
||||
useDispatch: () => vi.fn(),
|
||||
useSelector: () => ({
|
||||
agent: {
|
||||
curAgentState: AgentState.AWAITING_USER_INPUT,
|
||||
},
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
describe("MicroagentsModal - Refresh Button", () => {
|
||||
const mockOnClose = vi.fn();
|
||||
const conversationId = "test-conversation-id";
|
||||
|
||||
const defaultProps = {
|
||||
onClose: mockOnClose,
|
||||
conversationId,
|
||||
};
|
||||
|
||||
const mockMicroagents = [
|
||||
{
|
||||
name: "Test Agent 1",
|
||||
type: "repo" as const,
|
||||
triggers: ["test", "example"],
|
||||
content: "This is test content for agent 1",
|
||||
},
|
||||
{
|
||||
name: "Test Agent 2",
|
||||
type: "knowledge" as const,
|
||||
triggers: ["help", "support"],
|
||||
content: "This is test content for agent 2",
|
||||
},
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset all mocks before each test
|
||||
vi.clearAllMocks();
|
||||
|
||||
// Setup default mock for getUserConversations
|
||||
vi.spyOn(OpenHands, "getMicroagents").mockResolvedValue({
|
||||
microagents: mockMicroagents,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("Refresh Button Rendering", () => {
|
||||
it("should render the refresh button with correct text and test ID", () => {
|
||||
renderWithProviders(<MicroagentsModal {...defaultProps} />);
|
||||
|
||||
const refreshButton = screen.getByTestId("refresh-microagents");
|
||||
expect(refreshButton).toBeInTheDocument();
|
||||
expect(refreshButton).toHaveTextContent("BUTTON$REFRESH");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Refresh Button Functionality", () => {
|
||||
it("should call refetch when refresh button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
renderWithProviders(<MicroagentsModal {...defaultProps} />);
|
||||
|
||||
const refreshSpy = vi.spyOn(OpenHands, "getMicroagents");
|
||||
|
||||
const refreshButton = screen.getByTestId("refresh-microagents");
|
||||
await user.click(refreshButton);
|
||||
|
||||
expect(refreshSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -21,7 +21,12 @@ describe("UserActions", () => {
|
||||
});
|
||||
|
||||
it("should toggle the user menu when the user avatar is clicked", async () => {
|
||||
render(<UserActions onLogout={onLogoutMock} />);
|
||||
render(
|
||||
<UserActions
|
||||
onLogout={onLogoutMock}
|
||||
user={{ avatar_url: "https://example.com/avatar.png" }}
|
||||
/>,
|
||||
);
|
||||
|
||||
const userAvatar = screen.getByTestId("user-avatar");
|
||||
await user.click(userAvatar);
|
||||
@@ -57,15 +62,102 @@ describe("UserActions", () => {
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("logout button is always enabled", async () => {
|
||||
it("should NOT show context menu when user is undefined and avatar is clicked", async () => {
|
||||
render(<UserActions onLogout={onLogoutMock} />);
|
||||
|
||||
const userAvatar = screen.getByTestId("user-avatar");
|
||||
await user.click(userAvatar);
|
||||
|
||||
const logoutOption = screen.getByText("ACCOUNT_SETTINGS$LOGOUT");
|
||||
await user.click(logoutOption);
|
||||
// Context menu should NOT appear because user is undefined
|
||||
expect(
|
||||
screen.queryByTestId("account-settings-context-menu"),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(onLogoutMock).toHaveBeenCalledOnce();
|
||||
it("should show context menu even when user has no avatar_url", async () => {
|
||||
render(<UserActions onLogout={onLogoutMock} user={{ avatar_url: "" }} />);
|
||||
|
||||
const userAvatar = screen.getByTestId("user-avatar");
|
||||
await user.click(userAvatar);
|
||||
|
||||
// Context menu SHOULD appear because user object exists (even with empty avatar_url)
|
||||
expect(
|
||||
screen.getByTestId("account-settings-context-menu"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should NOT be able to access logout when no user is provided", async () => {
|
||||
render(<UserActions onLogout={onLogoutMock} />);
|
||||
|
||||
const userAvatar = screen.getByTestId("user-avatar");
|
||||
await user.click(userAvatar);
|
||||
|
||||
// Logout option should not be accessible because context menu doesn't appear
|
||||
expect(
|
||||
screen.queryByText("ACCOUNT_SETTINGS$LOGOUT"),
|
||||
).not.toBeInTheDocument();
|
||||
expect(onLogoutMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should handle user prop changing from undefined to defined", () => {
|
||||
const { rerender } = render(<UserActions onLogout={onLogoutMock} />);
|
||||
|
||||
// Initially no user - context menu shouldn't work
|
||||
expect(
|
||||
screen.queryByTestId("account-settings-context-menu"),
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
// Add user prop
|
||||
rerender(
|
||||
<UserActions
|
||||
onLogout={onLogoutMock}
|
||||
user={{ avatar_url: "https://example.com/avatar.png" }}
|
||||
/>,
|
||||
);
|
||||
|
||||
// Component should still render correctly
|
||||
expect(screen.getByTestId("user-actions")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("user-avatar")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should handle user prop changing from defined to undefined", async () => {
|
||||
const { rerender } = render(
|
||||
<UserActions
|
||||
onLogout={onLogoutMock}
|
||||
user={{ avatar_url: "https://example.com/avatar.png" }}
|
||||
/>,
|
||||
);
|
||||
|
||||
// Click to open menu
|
||||
const userAvatar = screen.getByTestId("user-avatar");
|
||||
await user.click(userAvatar);
|
||||
expect(
|
||||
screen.getByTestId("account-settings-context-menu"),
|
||||
).toBeInTheDocument();
|
||||
|
||||
// Remove user prop - menu should disappear
|
||||
rerender(<UserActions onLogout={onLogoutMock} />);
|
||||
|
||||
expect(
|
||||
screen.queryByTestId("account-settings-context-menu"),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should work with loading state and user provided", async () => {
|
||||
render(
|
||||
<UserActions
|
||||
onLogout={onLogoutMock}
|
||||
user={{ avatar_url: "https://example.com/avatar.png" }}
|
||||
isLoading={true}
|
||||
/>,
|
||||
);
|
||||
|
||||
const userAvatar = screen.getByTestId("user-avatar");
|
||||
await user.click(userAvatar);
|
||||
|
||||
// Context menu should still appear even when loading
|
||||
expect(
|
||||
screen.getByTestId("account-settings-context-menu"),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
140
frontend/__tests__/hooks/query/use-feedback-exists.test.tsx
Normal file
140
frontend/__tests__/hooks/query/use-feedback-exists.test.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
import { renderHook, waitFor } from "@testing-library/react";
|
||||
import { describe, expect, it, vi, beforeEach } from "vitest";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import { useFeedbackExists } from "#/hooks/query/use-feedback-exists";
|
||||
|
||||
// Mock the useConfig hook
|
||||
vi.mock("#/hooks/query/use-config", () => ({
|
||||
useConfig: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock the useConversationId hook
|
||||
vi.mock("#/hooks/use-conversation-id", () => ({
|
||||
useConversationId: () => ({ conversationId: "test-conversation-id" }),
|
||||
}));
|
||||
|
||||
describe("useFeedbackExists", () => {
|
||||
let queryClient: QueryClient;
|
||||
const mockCheckFeedbackExists = vi.spyOn(OpenHands, "checkFeedbackExists");
|
||||
|
||||
beforeEach(() => {
|
||||
queryClient = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false } },
|
||||
});
|
||||
mockCheckFeedbackExists.mockClear();
|
||||
});
|
||||
|
||||
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
);
|
||||
|
||||
it("should not call API when APP_MODE is not saas", async () => {
|
||||
const { useConfig } = await import("#/hooks/query/use-config");
|
||||
vi.mocked(useConfig).mockReturnValue({
|
||||
data: { APP_MODE: "oss" },
|
||||
isLoading: false,
|
||||
error: null,
|
||||
} as ReturnType<typeof useConfig>);
|
||||
|
||||
const { result } = renderHook(() => useFeedbackExists(123), {
|
||||
wrapper,
|
||||
});
|
||||
|
||||
// Wait for any potential async operations
|
||||
await waitFor(() => {
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
// Verify that the API was not called
|
||||
expect(mockCheckFeedbackExists).not.toHaveBeenCalled();
|
||||
|
||||
// Verify that the query is disabled
|
||||
expect(result.current.data).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should call API when APP_MODE is saas", async () => {
|
||||
const { useConfig } = await import("#/hooks/query/use-config");
|
||||
vi.mocked(useConfig).mockReturnValue({
|
||||
data: { APP_MODE: "saas" },
|
||||
isLoading: false,
|
||||
error: null,
|
||||
} as ReturnType<typeof useConfig>);
|
||||
|
||||
mockCheckFeedbackExists.mockResolvedValue({
|
||||
exists: true,
|
||||
rating: 5,
|
||||
reason: "Great job!",
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useFeedbackExists(123), {
|
||||
wrapper,
|
||||
});
|
||||
|
||||
// Wait for the query to complete
|
||||
await waitFor(() => {
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
// Verify that the API was called
|
||||
expect(mockCheckFeedbackExists).toHaveBeenCalledWith(
|
||||
"test-conversation-id",
|
||||
123,
|
||||
);
|
||||
|
||||
// Verify that the data is returned
|
||||
expect(result.current.data).toEqual({
|
||||
exists: true,
|
||||
rating: 5,
|
||||
reason: "Great job!",
|
||||
});
|
||||
});
|
||||
|
||||
it("should not call API when eventId is not provided", async () => {
|
||||
const { useConfig } = await import("#/hooks/query/use-config");
|
||||
vi.mocked(useConfig).mockReturnValue({
|
||||
data: { APP_MODE: "saas" },
|
||||
isLoading: false,
|
||||
error: null,
|
||||
} as ReturnType<typeof useConfig>);
|
||||
|
||||
const { result } = renderHook(() => useFeedbackExists(undefined), {
|
||||
wrapper,
|
||||
});
|
||||
|
||||
// Wait for any potential async operations
|
||||
await waitFor(() => {
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
// Verify that the API was not called
|
||||
expect(mockCheckFeedbackExists).not.toHaveBeenCalled();
|
||||
|
||||
// Verify that the query is disabled
|
||||
expect(result.current.data).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should not call API when config is not loaded yet", async () => {
|
||||
const { useConfig } = await import("#/hooks/query/use-config");
|
||||
vi.mocked(useConfig).mockReturnValue({
|
||||
data: undefined,
|
||||
isLoading: true,
|
||||
error: null,
|
||||
} as ReturnType<typeof useConfig>);
|
||||
|
||||
const { result } = renderHook(() => useFeedbackExists(123), {
|
||||
wrapper,
|
||||
});
|
||||
|
||||
// Wait for any potential async operations
|
||||
await waitFor(() => {
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
// Verify that the API was not called
|
||||
expect(mockCheckFeedbackExists).not.toHaveBeenCalled();
|
||||
|
||||
// Verify that the query is disabled
|
||||
expect(result.current.data).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -90,7 +90,7 @@ describe("HomeScreen", () => {
|
||||
const mainContainer = screen
|
||||
.getByTestId("home-screen")
|
||||
.querySelector("main");
|
||||
expect(mainContainer).toHaveClass("flex", "flex-col", "md:flex-row");
|
||||
expect(mainContainer).toHaveClass("flex", "flex-col", "lg:flex-row");
|
||||
});
|
||||
|
||||
it("should filter the suggested tasks based on the selected repository", async () => {
|
||||
|
||||
95
frontend/__tests__/services/actions.test.tsx
Normal file
95
frontend/__tests__/services/actions.test.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import ActionType from "#/types/action-type";
|
||||
import { ActionMessage } from "#/types/message";
|
||||
|
||||
// Mock the store and actions
|
||||
const mockDispatch = vi.fn();
|
||||
const mockAppendInput = vi.fn();
|
||||
const mockAppendJupyterInput = vi.fn();
|
||||
|
||||
vi.mock("#/store", () => ({
|
||||
default: {
|
||||
dispatch: mockDispatch,
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("#/state/command-slice", () => ({
|
||||
appendInput: mockAppendInput,
|
||||
}));
|
||||
|
||||
vi.mock("#/state/jupyter-slice", () => ({
|
||||
appendJupyterInput: mockAppendJupyterInput,
|
||||
}));
|
||||
|
||||
describe("handleActionMessage", () => {
|
||||
beforeEach(() => {
|
||||
// Clear all mocks before each test
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should handle RUN actions by adding input to terminal", async () => {
|
||||
const { handleActionMessage } = await import("#/services/actions");
|
||||
|
||||
const runAction: ActionMessage = {
|
||||
id: 1,
|
||||
source: "agent",
|
||||
action: ActionType.RUN,
|
||||
args: {
|
||||
command: "ls -la",
|
||||
},
|
||||
message: "Running command: ls -la",
|
||||
timestamp: "2023-01-01T00:00:00Z",
|
||||
};
|
||||
|
||||
// Handle the action
|
||||
handleActionMessage(runAction);
|
||||
|
||||
// Check that appendInput was called with the command
|
||||
expect(mockDispatch).toHaveBeenCalledWith(mockAppendInput("ls -la"));
|
||||
expect(mockAppendJupyterInput).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should handle RUN_IPYTHON actions by adding input to Jupyter", async () => {
|
||||
const { handleActionMessage } = await import("#/services/actions");
|
||||
|
||||
const ipythonAction: ActionMessage = {
|
||||
id: 2,
|
||||
source: "agent",
|
||||
action: ActionType.RUN_IPYTHON,
|
||||
args: {
|
||||
code: "print('Hello from Jupyter!')",
|
||||
},
|
||||
message: "Running Python code interactively: print('Hello from Jupyter!')",
|
||||
timestamp: "2023-01-01T00:00:00Z",
|
||||
};
|
||||
|
||||
// Handle the action
|
||||
handleActionMessage(ipythonAction);
|
||||
|
||||
// Check that appendJupyterInput was called with the code
|
||||
expect(mockDispatch).toHaveBeenCalledWith(mockAppendJupyterInput("print('Hello from Jupyter!')"));
|
||||
expect(mockAppendInput).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should not process hidden actions", async () => {
|
||||
const { handleActionMessage } = await import("#/services/actions");
|
||||
|
||||
const hiddenAction: ActionMessage = {
|
||||
id: 3,
|
||||
source: "agent",
|
||||
action: ActionType.RUN,
|
||||
args: {
|
||||
command: "secret command",
|
||||
hidden: "true",
|
||||
},
|
||||
message: "Running command: secret command",
|
||||
timestamp: "2023-01-01T00:00:00Z",
|
||||
};
|
||||
|
||||
// Handle the action
|
||||
handleActionMessage(hiddenAction);
|
||||
|
||||
// Check that nothing was dispatched
|
||||
expect(mockDispatch).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
1811
frontend/package-lock.json
generated
1811
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "openhands-frontend",
|
||||
"version": "0.47.0",
|
||||
"version": "0.48.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@heroui/react": "^2.8.0-beta.10",
|
||||
"@heroui/react": "^2.8.0-beta.13",
|
||||
"@microlink/react-json-view": "^1.26.2",
|
||||
"@monaco-editor/react": "^4.7.0-rc.0",
|
||||
"@react-router/node": "^7.6.3",
|
||||
@@ -25,20 +25,20 @@
|
||||
"axios": "^1.10.0",
|
||||
"clsx": "^2.1.1",
|
||||
"eslint-config-airbnb-typescript": "^18.0.0",
|
||||
"framer-motion": "^12.19.2",
|
||||
"i18next": "^25.2.1",
|
||||
"framer-motion": "^12.23.0",
|
||||
"i18next": "^25.3.1",
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"i18next-http-backend": "^3.0.2",
|
||||
"isbot": "^5.1.28",
|
||||
"jose": "^6.0.11",
|
||||
"lucide-react": "^0.525.0",
|
||||
"monaco-editor": "^0.52.2",
|
||||
"posthog-js": "^1.255.1",
|
||||
"posthog-js": "^1.256.2",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-highlight": "^0.15.0",
|
||||
"react-hot-toast": "^2.5.1",
|
||||
"react-i18next": "^15.5.3",
|
||||
"react-i18next": "^15.6.0",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-redux": "^9.2.0",
|
||||
@@ -49,7 +49,7 @@
|
||||
"sirv-cli": "^3.0.1",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"vite": "^7.0.0",
|
||||
"vite": "^7.0.2",
|
||||
"web-vitals": "^5.0.3",
|
||||
"ws": "^8.18.2"
|
||||
},
|
||||
@@ -80,11 +80,11 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/parser": "^7.27.7",
|
||||
"@babel/traverse": "^7.27.7",
|
||||
"@babel/parser": "^7.28.0",
|
||||
"@babel/traverse": "^7.28.0",
|
||||
"@babel/types": "^7.27.0",
|
||||
"@mswjs/socket.io-binding": "^0.2.0",
|
||||
"@playwright/test": "^1.53.1",
|
||||
"@playwright/test": "^1.53.2",
|
||||
"@react-router/dev": "^7.6.3",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@tanstack/eslint-plugin-query": "^5.81.2",
|
||||
@@ -92,7 +92,7 @@
|
||||
"@testing-library/jest-dom": "^6.6.1",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/node": "^24.0.5",
|
||||
"@types/node": "^24.0.10",
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"@types/react-highlight": "^0.12.8",
|
||||
@@ -118,7 +118,7 @@
|
||||
"lint-staged": "^16.1.2",
|
||||
"msw": "^2.6.6",
|
||||
"prettier": "^3.6.2",
|
||||
"stripe": "^18.2.1",
|
||||
"stripe": "^18.3.0",
|
||||
"tailwindcss": "^4.1.8",
|
||||
"typescript": "^5.8.3",
|
||||
"vite-plugin-svgr": "^4.2.0",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ConversationStatus } from "#/types/conversation-status";
|
||||
import { RuntimeStatus } from "#/types/runtime-status";
|
||||
import { Provider } from "#/types/settings";
|
||||
|
||||
export interface ErrorResponse {
|
||||
error: string;
|
||||
@@ -70,6 +71,12 @@ export interface AuthenticateResponse {
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface RepositorySelection {
|
||||
selected_repository: string | null;
|
||||
selected_branch: string | null;
|
||||
git_provider: Provider | null;
|
||||
}
|
||||
|
||||
export type ConversationTrigger = "resolver" | "gui" | "suggested_task";
|
||||
|
||||
export interface Conversation {
|
||||
|
||||
@@ -29,7 +29,7 @@ export function ChatInput({
|
||||
disabled,
|
||||
showButton = true,
|
||||
value,
|
||||
maxRows = 16,
|
||||
maxRows = 8,
|
||||
onSubmit,
|
||||
onStop,
|
||||
onChange,
|
||||
|
||||
164
frontend/src/components/features/chat/chat-interface.test.tsx
Normal file
164
frontend/src/components/features/chat/chat-interface.test.tsx
Normal file
@@ -0,0 +1,164 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { useParams } from "react-router";
|
||||
import { vi, describe, test, expect, beforeEach } from "vitest";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { ChatInterface } from "./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 { useConfig } from "#/hooks/query/use-config";
|
||||
import { useGetTrajectory } from "#/hooks/mutation/use-get-trajectory";
|
||||
import { useUploadFiles } from "#/hooks/mutation/use-upload-files";
|
||||
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("react-router");
|
||||
vi.mock("#/hooks/query/use-config");
|
||||
vi.mock("#/hooks/mutation/use-get-trajectory");
|
||||
vi.mock("#/hooks/mutation/use-upload-files");
|
||||
vi.mock("react-redux", () => ({
|
||||
useSelector: vi.fn(() => ({
|
||||
curAgentState: "AWAITING_USER_INPUT",
|
||||
selectedRepository: null,
|
||||
replayJson: null,
|
||||
})),
|
||||
}));
|
||||
|
||||
describe("ChatInterface", () => {
|
||||
// Create a new QueryClient for each test
|
||||
let queryClient: QueryClient;
|
||||
|
||||
beforeEach(() => {
|
||||
queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
retry: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Default mock implementations
|
||||
(useWsClient as unknown as ReturnType<typeof vi.fn>).mockReturnValue({
|
||||
send: vi.fn(),
|
||||
isLoadingMessages: false,
|
||||
parsedEvents: [],
|
||||
});
|
||||
(
|
||||
useOptimisticUserMessage 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),
|
||||
setErrorMessage: vi.fn(),
|
||||
removeErrorMessage: vi.fn(),
|
||||
});
|
||||
(useParams as unknown as ReturnType<typeof vi.fn>).mockReturnValue({
|
||||
conversationId: "test-id",
|
||||
});
|
||||
(useConfig as unknown as ReturnType<typeof vi.fn>).mockReturnValue({
|
||||
data: { APP_MODE: "local" },
|
||||
});
|
||||
(useGetTrajectory as unknown as ReturnType<typeof vi.fn>).mockReturnValue({
|
||||
mutate: vi.fn(),
|
||||
mutateAsync: vi.fn(),
|
||||
isLoading: false,
|
||||
});
|
||||
(useUploadFiles as unknown as ReturnType<typeof vi.fn>).mockReturnValue({
|
||||
mutateAsync: vi
|
||||
.fn()
|
||||
.mockResolvedValue({ skipped_files: [], uploaded_files: [] }),
|
||||
isLoading: false,
|
||||
});
|
||||
});
|
||||
|
||||
// Helper function to render with QueryClientProvider
|
||||
const renderWithQueryClient = (ui: React.ReactElement) =>
|
||||
render(
|
||||
<QueryClientProvider client={queryClient}>{ui}</QueryClientProvider>,
|
||||
);
|
||||
|
||||
test("should show chat suggestions when there are no events", () => {
|
||||
(useWsClient as unknown as ReturnType<typeof vi.fn>).mockReturnValue({
|
||||
send: vi.fn(),
|
||||
isLoadingMessages: false,
|
||||
parsedEvents: [],
|
||||
});
|
||||
|
||||
renderWithQueryClient(<ChatInterface />);
|
||||
|
||||
// Check if ChatSuggestions is rendered
|
||||
expect(screen.getByTestId("chat-suggestions")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("should show chat suggestions when there are only environment events", () => {
|
||||
const environmentEvent: OpenHandsAction = {
|
||||
id: 1,
|
||||
source: "environment",
|
||||
action: "system",
|
||||
args: {
|
||||
content: "source .openhands/setup.sh",
|
||||
tools: null,
|
||||
openhands_version: null,
|
||||
agent_class: null,
|
||||
},
|
||||
message: "Running setup script",
|
||||
timestamp: "2025-07-01T00:00:00Z",
|
||||
};
|
||||
|
||||
(useWsClient as unknown as ReturnType<typeof vi.fn>).mockReturnValue({
|
||||
send: vi.fn(),
|
||||
isLoadingMessages: false,
|
||||
parsedEvents: [environmentEvent],
|
||||
});
|
||||
|
||||
renderWithQueryClient(<ChatInterface />);
|
||||
|
||||
// Check if ChatSuggestions is still rendered with environment events
|
||||
expect(screen.getByTestId("chat-suggestions")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("should hide chat suggestions when there is a user message", () => {
|
||||
const userEvent: OpenHandsAction = {
|
||||
id: 1,
|
||||
source: "user",
|
||||
action: "message",
|
||||
args: {
|
||||
content: "Hello",
|
||||
image_urls: [],
|
||||
file_urls: [],
|
||||
},
|
||||
message: "Hello",
|
||||
timestamp: "2025-07-01T00:00:00Z",
|
||||
};
|
||||
|
||||
(useWsClient as unknown as ReturnType<typeof vi.fn>).mockReturnValue({
|
||||
send: vi.fn(),
|
||||
isLoadingMessages: false,
|
||||
parsedEvents: [userEvent],
|
||||
});
|
||||
|
||||
renderWithQueryClient(<ChatInterface />);
|
||||
|
||||
// Check if ChatSuggestions is not rendered with user events
|
||||
expect(screen.queryByTestId("chat-suggestions")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("should hide chat suggestions when there is an optimistic user message", () => {
|
||||
(
|
||||
useOptimisticUserMessage as unknown as ReturnType<typeof vi.fn>
|
||||
).mockReturnValue({
|
||||
setOptimisticUserMessage: vi.fn(),
|
||||
getOptimisticUserMessage: vi.fn(() => "Optimistic message"),
|
||||
});
|
||||
|
||||
renderWithQueryClient(<ChatInterface />);
|
||||
|
||||
// Check if ChatSuggestions is not rendered with optimistic user message
|
||||
expect(screen.queryByTestId("chat-suggestions")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -10,6 +10,7 @@ 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";
|
||||
import { FeedbackModal } from "../feedback/feedback-modal";
|
||||
import { useScrollToBottom } from "#/hooks/use-scroll-to-bottom";
|
||||
@@ -31,6 +32,7 @@ import { ErrorMessageBanner } from "./error-message-banner";
|
||||
import { shouldRenderEvent } from "./event-content-helpers/should-render-event";
|
||||
import { useUploadFiles } from "#/hooks/mutation/use-upload-files";
|
||||
import { useConfig } from "#/hooks/query/use-config";
|
||||
import { validateFiles } from "#/utils/file-validation";
|
||||
|
||||
function getEntryPoint(
|
||||
hasRepository: boolean | null,
|
||||
@@ -77,11 +79,26 @@ export function ChatInterface() {
|
||||
|
||||
const events = parsedEvents.filter(shouldRenderEvent);
|
||||
|
||||
// Check if there are any substantive agent actions (not just system messages)
|
||||
const hasSubstantiveAgentActions = React.useMemo(
|
||||
() =>
|
||||
parsedEvents.some(
|
||||
(event) =>
|
||||
isOpenHandsAction(event) &&
|
||||
event.source === "agent" &&
|
||||
event.action !== "system",
|
||||
),
|
||||
[parsedEvents],
|
||||
);
|
||||
|
||||
const handleSendMessage = async (
|
||||
content: string,
|
||||
images: File[],
|
||||
files: File[],
|
||||
originalImages: File[],
|
||||
originalFiles: File[],
|
||||
) => {
|
||||
// Create mutable copies of the arrays
|
||||
const images = [...originalImages];
|
||||
const files = [...originalFiles];
|
||||
if (events.length === 0) {
|
||||
posthog.capture("initial_query_submitted", {
|
||||
entry_point: getEntryPoint(
|
||||
@@ -97,6 +114,16 @@ export function ChatInterface() {
|
||||
current_message_length: content.length,
|
||||
});
|
||||
}
|
||||
|
||||
// Validate file sizes before any processing
|
||||
const allFiles = [...images, ...files];
|
||||
const validation = validateFiles(allFiles);
|
||||
|
||||
if (!validation.isValid) {
|
||||
displayErrorToast(`Error: ${validation.errorMessage}`);
|
||||
return; // Stop processing if validation fails
|
||||
}
|
||||
|
||||
const promises = images.map((image) => convertImageToBase64(image));
|
||||
const imageUrls = await Promise.all(promises);
|
||||
|
||||
@@ -167,9 +194,12 @@ export function ChatInterface() {
|
||||
return (
|
||||
<ScrollProvider value={scrollProviderValue}>
|
||||
<div className="h-full flex flex-col justify-between">
|
||||
{events.length === 0 && !optimisticUserMessage && (
|
||||
<ChatSuggestions onSuggestionsClick={setMessageToSend} />
|
||||
)}
|
||||
{!hasSubstantiveAgentActions &&
|
||||
!optimisticUserMessage &&
|
||||
!events.some(
|
||||
(event) => isOpenHandsAction(event) && event.source === "user",
|
||||
) && <ChatSuggestions onSuggestionsClick={setMessageToSend} />}
|
||||
{/* Note: We only hide chat suggestions when there's a user message */}
|
||||
|
||||
<div
|
||||
ref={scrollRef}
|
||||
@@ -192,7 +222,7 @@ export function ChatInterface() {
|
||||
)}
|
||||
|
||||
{isWaitingForUserInput &&
|
||||
events.length > 0 &&
|
||||
hasSubstantiveAgentActions &&
|
||||
!optimisticUserMessage && (
|
||||
<ActionSuggestions
|
||||
onSuggestionsClick={(value) => handleSendMessage(value, [], [])}
|
||||
|
||||
@@ -12,7 +12,10 @@ export function ChatSuggestions({ onSuggestionsClick }: ChatSuggestionsProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6 h-full px-4 items-center justify-center">
|
||||
<div
|
||||
data-testid="chat-suggestions"
|
||||
className="flex flex-col gap-6 h-full px-4 items-center justify-center"
|
||||
>
|
||||
<div className="flex flex-col items-center p-4 bg-tertiary rounded-xl w-full">
|
||||
<BuildIt width={45} height={54} />
|
||||
<span className="font-semibold text-[20px] leading-6 -tracking-[0.01em] gap-1">
|
||||
|
||||
@@ -5,6 +5,8 @@ import { ImageCarousel } from "../images/image-carousel";
|
||||
import { UploadImageInput } from "../images/upload-image-input";
|
||||
import { FileList } from "../files/file-list";
|
||||
import { isFileImage } from "#/utils/is-file-image";
|
||||
import { displayErrorToast } from "#/utils/custom-toast-handlers";
|
||||
import { validateFiles } from "#/utils/file-validation";
|
||||
|
||||
interface InteractiveChatBoxProps {
|
||||
isDisabled?: boolean;
|
||||
@@ -27,14 +29,20 @@ export function InteractiveChatBox({
|
||||
const [files, setFiles] = React.useState<File[]>([]);
|
||||
|
||||
const handleUpload = (selectedFiles: File[]) => {
|
||||
setFiles((prevFiles) => [
|
||||
...prevFiles,
|
||||
...selectedFiles.filter((f) => !isFileImage(f)),
|
||||
]);
|
||||
setImages((prevImages) => [
|
||||
...prevImages,
|
||||
...selectedFiles.filter((f) => isFileImage(f)),
|
||||
]);
|
||||
// Validate files before adding them
|
||||
const validation = validateFiles(selectedFiles, [...images, ...files]);
|
||||
|
||||
if (!validation.isValid) {
|
||||
displayErrorToast(`Error: ${validation.errorMessage}`);
|
||||
return; // Don't add any files if validation fails
|
||||
}
|
||||
|
||||
// Filter valid files by type
|
||||
const validFiles = selectedFiles.filter((f) => !isFileImage(f));
|
||||
const validImages = selectedFiles.filter((f) => isFileImage(f));
|
||||
|
||||
setFiles((prevFiles) => [...prevFiles, ...validFiles]);
|
||||
setImages((prevImages) => [...prevImages, ...validImages]);
|
||||
};
|
||||
|
||||
const removeElementByIndex = (array: Array<File>, index: number) => {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { AgentStatusBar } from "./agent-status-bar";
|
||||
import { SecurityLock } from "./security-lock";
|
||||
import { useActiveConversation } from "#/hooks/query/use-active-conversation";
|
||||
import { ConversationCard } from "../conversation-panel/conversation-card";
|
||||
import { Provider } from "#/types/settings";
|
||||
|
||||
interface ControlsProps {
|
||||
setSecurityOpen: (isOpen: boolean) => void;
|
||||
@@ -29,7 +30,11 @@ export function Controls({ setSecurityOpen, showSecurityLock }: ControlsProps) {
|
||||
showOptions
|
||||
title={conversation?.title ?? ""}
|
||||
lastUpdatedAt={conversation?.created_at ?? ""}
|
||||
selectedRepository={conversation?.selected_repository ?? null}
|
||||
selectedRepository={{
|
||||
selected_repository: conversation?.selected_repository ?? null,
|
||||
selected_branch: conversation?.selected_branch ?? null,
|
||||
git_provider: (conversation?.git_provider as Provider) ?? null,
|
||||
}}
|
||||
conversationStatus={conversation?.status}
|
||||
conversationId={conversation?.conversation_id}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
BaseModalDescription,
|
||||
BaseModalTitle,
|
||||
} from "#/components/shared/modals/confirmation-modals/base-modal";
|
||||
import { ModalBackdrop } from "#/components/shared/modals/modal-backdrop";
|
||||
import { ModalBody } from "#/components/shared/modals/modal-body";
|
||||
import { BrandButton } from "../settings/brand-button";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
|
||||
interface ConfirmStopModalProps {
|
||||
onConfirm: () => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export function ConfirmStopModal({
|
||||
onConfirm,
|
||||
onCancel,
|
||||
}: ConfirmStopModalProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<ModalBackdrop>
|
||||
<ModalBody className="items-start border border-tertiary">
|
||||
<div className="flex flex-col gap-2">
|
||||
<BaseModalTitle title={t(I18nKey.CONVERSATION$CONFIRM_STOP)} />
|
||||
<BaseModalDescription
|
||||
description={t(I18nKey.CONVERSATION$STOP_WARNING)}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="flex flex-col gap-2 w-full"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
<BrandButton
|
||||
type="button"
|
||||
variant="primary"
|
||||
onClick={onConfirm}
|
||||
className="w-full"
|
||||
data-testid="confirm-button"
|
||||
>
|
||||
{t(I18nKey.ACTION$CONFIRM)}
|
||||
</BrandButton>
|
||||
<BrandButton
|
||||
type="button"
|
||||
variant="secondary"
|
||||
onClick={onCancel}
|
||||
className="w-full"
|
||||
data-testid="cancel-button"
|
||||
>
|
||||
{t(I18nKey.BUTTON$CANCEL)}
|
||||
</BrandButton>
|
||||
</div>
|
||||
</ModalBody>
|
||||
</ModalBackdrop>
|
||||
);
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import { I18nKey } from "#/i18n/declaration";
|
||||
interface ConversationCardContextMenuProps {
|
||||
onClose: () => void;
|
||||
onDelete?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
onStop?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
onEdit?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
onDisplayCost?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
onShowAgentTools?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
@@ -19,6 +20,7 @@ interface ConversationCardContextMenuProps {
|
||||
export function ConversationCardContextMenu({
|
||||
onClose,
|
||||
onDelete,
|
||||
onStop,
|
||||
onEdit,
|
||||
onDisplayCost,
|
||||
onShowAgentTools,
|
||||
@@ -41,12 +43,17 @@ export function ConversationCardContextMenu({
|
||||
>
|
||||
{onDelete && (
|
||||
<ContextMenuListItem testId="delete-button" onClick={onDelete}>
|
||||
Delete
|
||||
{t(I18nKey.BUTTON$DELETE)}
|
||||
</ContextMenuListItem>
|
||||
)}
|
||||
{onStop && (
|
||||
<ContextMenuListItem testId="stop-button" onClick={onStop}>
|
||||
{t(I18nKey.BUTTON$STOP)}
|
||||
</ContextMenuListItem>
|
||||
)}
|
||||
{onEdit && (
|
||||
<ContextMenuListItem testId="edit-button" onClick={onEdit}>
|
||||
Edit Title
|
||||
{t(I18nKey.BUTTON$EDIT_TITLE)}
|
||||
</ContextMenuListItem>
|
||||
)}
|
||||
{onDownloadViaVSCode && (
|
||||
@@ -54,7 +61,7 @@ export function ConversationCardContextMenu({
|
||||
testId="download-vscode-button"
|
||||
onClick={onDownloadViaVSCode}
|
||||
>
|
||||
Download via VS Code
|
||||
{t(I18nKey.BUTTON$DOWNLOAD_VIA_VSCODE)}
|
||||
</ContextMenuListItem>
|
||||
)}
|
||||
{onDisplayCost && (
|
||||
@@ -62,7 +69,7 @@ export function ConversationCardContextMenu({
|
||||
testId="display-cost-button"
|
||||
onClick={onDisplayCost}
|
||||
>
|
||||
Display Cost
|
||||
{t(I18nKey.BUTTON$DISPLAY_COST)}
|
||||
</ContextMenuListItem>
|
||||
)}
|
||||
{onShowAgentTools && (
|
||||
@@ -70,7 +77,7 @@ export function ConversationCardContextMenu({
|
||||
testId="show-agent-tools-button"
|
||||
onClick={onShowAgentTools}
|
||||
>
|
||||
Show Agent Tools & Metadata
|
||||
{t(I18nKey.BUTTON$SHOW_AGENT_TOOLS_AND_METADATA)}
|
||||
</ContextMenuListItem>
|
||||
)}
|
||||
{onShowMicroagents && (
|
||||
|
||||
@@ -19,15 +19,17 @@ import OpenHands from "#/api/open-hands";
|
||||
import { useWsClient } from "#/context/ws-client-provider";
|
||||
import { isSystemMessage } from "#/types/core/guards";
|
||||
import { ConversationStatus } from "#/types/conversation-status";
|
||||
import { RepositorySelection } from "#/api/open-hands.types";
|
||||
|
||||
interface ConversationCardProps {
|
||||
onClick?: () => void;
|
||||
onDelete?: () => void;
|
||||
onStop?: () => void;
|
||||
onChangeTitle?: (title: string) => void;
|
||||
showOptions?: boolean;
|
||||
isActive?: boolean;
|
||||
title: string;
|
||||
selectedRepository: string | null;
|
||||
selectedRepository: RepositorySelection | null;
|
||||
lastUpdatedAt: string; // ISO 8601
|
||||
createdAt?: string; // ISO 8601
|
||||
conversationStatus?: ConversationStatus;
|
||||
@@ -40,6 +42,7 @@ const MAX_TIME_BETWEEN_CREATION_AND_UPDATE = 1000 * 60 * 30; // 30 minutes
|
||||
export function ConversationCard({
|
||||
onClick,
|
||||
onDelete,
|
||||
onStop,
|
||||
onChangeTitle,
|
||||
showOptions,
|
||||
isActive,
|
||||
@@ -101,6 +104,13 @@ export function ConversationCard({
|
||||
setContextMenuVisible(false);
|
||||
};
|
||||
|
||||
const handleStop = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
onStop?.();
|
||||
setContextMenuVisible(false);
|
||||
};
|
||||
|
||||
const handleEdit = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
@@ -171,7 +181,7 @@ export function ConversationCard({
|
||||
data-testid="conversation-card"
|
||||
onClick={onClick}
|
||||
className={cn(
|
||||
"h-[100px] w-full px-[18px] py-4 border-b border-neutral-600 cursor-pointer",
|
||||
"h-auto w-full px-[18px] py-4 border-b border-neutral-600 cursor-pointer",
|
||||
variant === "compact" &&
|
||||
"md:w-fit h-auto rounded-xl border border-[#525252]",
|
||||
)}
|
||||
@@ -224,6 +234,11 @@ export function ConversationCard({
|
||||
<ConversationCardContextMenu
|
||||
onClose={() => setContextMenuVisible(false)}
|
||||
onDelete={onDelete && handleDelete}
|
||||
onStop={
|
||||
conversationStatus !== "STOPPED"
|
||||
? onStop && handleStop
|
||||
: undefined
|
||||
}
|
||||
onEdit={onChangeTitle && handleEdit}
|
||||
onDownloadViaVSCode={
|
||||
conversationId && showOptions
|
||||
@@ -250,28 +265,33 @@ export function ConversationCard({
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
variant === "compact" && "flex items-center justify-between mt-1",
|
||||
variant === "compact" && "flex flex-col justify-between mt-1",
|
||||
)}
|
||||
>
|
||||
{selectedRepository && (
|
||||
<ConversationRepoLink selectedRepository={selectedRepository} />
|
||||
{selectedRepository?.selected_repository && (
|
||||
<ConversationRepoLink
|
||||
selectedRepository={selectedRepository}
|
||||
variant={variant}
|
||||
/>
|
||||
)}
|
||||
{(createdAt || lastUpdatedAt) && (
|
||||
<p className="text-xs text-neutral-400">
|
||||
<span>{t(I18nKey.CONVERSATION$CREATED)} </span>
|
||||
<time>
|
||||
{formatTimeDelta(new Date(createdAt || lastUpdatedAt))}{" "}
|
||||
{t(I18nKey.CONVERSATION$AGO)}
|
||||
</time>
|
||||
{showUpdateTime && (
|
||||
<>
|
||||
<span>{t(I18nKey.CONVERSATION$UPDATED)} </span>
|
||||
<time>
|
||||
{formatTimeDelta(new Date(lastUpdatedAt))}{" "}
|
||||
{t(I18nKey.CONVERSATION$AGO)}
|
||||
</time>
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
<p className="text-xs text-neutral-400">
|
||||
<span>{t(I18nKey.CONVERSATION$CREATED)} </span>
|
||||
<time>
|
||||
{formatTimeDelta(new Date(createdAt || lastUpdatedAt))}{" "}
|
||||
{t(I18nKey.CONVERSATION$AGO)}
|
||||
</time>
|
||||
{showUpdateTime && (
|
||||
<>
|
||||
<span>{t(I18nKey.CONVERSATION$UPDATED)} </span>
|
||||
<time>
|
||||
{formatTimeDelta(new Date(lastUpdatedAt))}{" "}
|
||||
{t(I18nKey.CONVERSATION$AGO)}
|
||||
</time>
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -5,10 +5,13 @@ import { I18nKey } from "#/i18n/declaration";
|
||||
import { ConversationCard } from "./conversation-card";
|
||||
import { useUserConversations } from "#/hooks/query/use-user-conversations";
|
||||
import { useDeleteConversation } from "#/hooks/mutation/use-delete-conversation";
|
||||
import { useStopConversation } from "#/hooks/mutation/use-stop-conversation";
|
||||
import { ConfirmDeleteModal } from "./confirm-delete-modal";
|
||||
import { ConfirmStopModal } from "./confirm-stop-modal";
|
||||
import { LoadingSpinner } from "#/components/shared/loading-spinner";
|
||||
import { ExitConversationModal } from "./exit-conversation-modal";
|
||||
import { useClickOutsideElement } from "#/hooks/use-click-outside-element";
|
||||
import { Provider } from "#/types/settings";
|
||||
|
||||
interface ConversationPanelProps {
|
||||
onClose: () => void;
|
||||
@@ -22,6 +25,8 @@ export function ConversationPanel({ onClose }: ConversationPanelProps) {
|
||||
|
||||
const [confirmDeleteModalVisible, setConfirmDeleteModalVisible] =
|
||||
React.useState(false);
|
||||
const [confirmStopModalVisible, setConfirmStopModalVisible] =
|
||||
React.useState(false);
|
||||
const [
|
||||
confirmExitConversationModalVisible,
|
||||
setConfirmExitConversationModalVisible,
|
||||
@@ -33,12 +38,18 @@ export function ConversationPanel({ onClose }: ConversationPanelProps) {
|
||||
const { data: conversations, isFetching, error } = useUserConversations();
|
||||
|
||||
const { mutate: deleteConversation } = useDeleteConversation();
|
||||
const { mutate: stopConversation } = useStopConversation();
|
||||
|
||||
const handleDeleteProject = (conversationId: string) => {
|
||||
setConfirmDeleteModalVisible(true);
|
||||
setSelectedConversationId(conversationId);
|
||||
};
|
||||
|
||||
const handleStopConversation = (conversationId: string) => {
|
||||
setConfirmStopModalVisible(true);
|
||||
setSelectedConversationId(conversationId);
|
||||
};
|
||||
|
||||
const handleConfirmDelete = () => {
|
||||
if (selectedConversationId) {
|
||||
deleteConversation(
|
||||
@@ -54,6 +65,21 @@ export function ConversationPanel({ onClose }: ConversationPanelProps) {
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirmStop = () => {
|
||||
if (selectedConversationId) {
|
||||
stopConversation(
|
||||
{ conversationId: selectedConversationId },
|
||||
{
|
||||
onSuccess: () => {
|
||||
if (selectedConversationId === currentConversationId) {
|
||||
navigate("/");
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
@@ -87,8 +113,13 @@ export function ConversationPanel({ onClose }: ConversationPanelProps) {
|
||||
<ConversationCard
|
||||
isActive={isActive}
|
||||
onDelete={() => handleDeleteProject(project.conversation_id)}
|
||||
onStop={() => handleStopConversation(project.conversation_id)}
|
||||
title={project.title}
|
||||
selectedRepository={project.selected_repository}
|
||||
selectedRepository={{
|
||||
selected_repository: project.selected_repository,
|
||||
selected_branch: project.selected_branch,
|
||||
git_provider: project.git_provider as Provider,
|
||||
}}
|
||||
lastUpdatedAt={project.last_updated_at}
|
||||
createdAt={project.created_at}
|
||||
conversationStatus={project.status}
|
||||
@@ -108,6 +139,16 @@ export function ConversationPanel({ onClose }: ConversationPanelProps) {
|
||||
/>
|
||||
)}
|
||||
|
||||
{confirmStopModalVisible && (
|
||||
<ConfirmStopModal
|
||||
onConfirm={() => {
|
||||
handleConfirmStop();
|
||||
setConfirmStopModalVisible(false);
|
||||
}}
|
||||
onCancel={() => setConfirmStopModalVisible(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{confirmExitConversationModalVisible && (
|
||||
<ExitConversationModal
|
||||
onConfirm={() => {
|
||||
|
||||
@@ -1,16 +1,44 @@
|
||||
import { FaBitbucket, FaGithub, FaGitlab } from "react-icons/fa6";
|
||||
import { RepositorySelection } from "#/api/open-hands.types";
|
||||
|
||||
interface ConversationRepoLinkProps {
|
||||
selectedRepository: string;
|
||||
selectedRepository: RepositorySelection;
|
||||
variant: "compact" | "default";
|
||||
}
|
||||
|
||||
export function ConversationRepoLink({
|
||||
selectedRepository,
|
||||
variant = "default",
|
||||
}: ConversationRepoLinkProps) {
|
||||
if (variant === "compact") {
|
||||
return (
|
||||
<span
|
||||
data-testid="conversation-card-selected-repository"
|
||||
className="text-xs text-neutral-400"
|
||||
>
|
||||
{selectedRepository.selected_repository}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<span
|
||||
data-testid="conversation-card-selected-repository"
|
||||
className="text-xs text-neutral-400"
|
||||
>
|
||||
{selectedRepository}
|
||||
</span>
|
||||
<div className="flex items-center gap-1">
|
||||
{selectedRepository.git_provider === "github" && <FaGithub size={14} />}
|
||||
{selectedRepository.git_provider === "gitlab" && <FaGitlab />}
|
||||
{selectedRepository.git_provider === "bitbucket" && <FaBitbucket />}
|
||||
|
||||
<span
|
||||
data-testid="conversation-card-selected-repository"
|
||||
className="text-xs text-neutral-400"
|
||||
>
|
||||
{selectedRepository.selected_repository}
|
||||
</span>
|
||||
<code
|
||||
data-testid="conversation-card-selected-branch"
|
||||
className="text-xs text-neutral-400 border border-neutral-700 rounded px-1 py-0.5 w-fit bg-neutral-800"
|
||||
>
|
||||
{selectedRepository.selected_branch}
|
||||
</code>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,12 @@ interface EllipsisButtonProps {
|
||||
|
||||
export function EllipsisButton({ onClick }: EllipsisButtonProps) {
|
||||
return (
|
||||
<button data-testid="ellipsis-button" type="button" onClick={onClick}>
|
||||
<button
|
||||
data-testid="ellipsis-button"
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<FaEllipsisV fill="#a3a3a3" />
|
||||
</button>
|
||||
);
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import React, { useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ChevronDown, ChevronRight } from "lucide-react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { ChevronDown, ChevronRight, RefreshCw } from "lucide-react";
|
||||
import { BaseModalTitle } from "#/components/shared/modals/confirmation-modals/base-modal";
|
||||
import { ModalBackdrop } from "#/components/shared/modals/modal-backdrop";
|
||||
import { ModalBody } from "#/components/shared/modals/modal-body";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { useConversationMicroagents } from "#/hooks/query/use-conversation-microagents";
|
||||
import { RootState } from "#/store";
|
||||
import { AgentState } from "#/types/agent-state";
|
||||
import { BrandButton } from "../settings/brand-button";
|
||||
|
||||
interface MicroagentsModalProps {
|
||||
onClose: () => void;
|
||||
@@ -17,15 +21,18 @@ export function MicroagentsModal({
|
||||
conversationId,
|
||||
}: MicroagentsModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const { curAgentState } = useSelector((state: RootState) => state.agent);
|
||||
const [expandedAgents, setExpandedAgents] = useState<Record<string, boolean>>(
|
||||
{},
|
||||
);
|
||||
|
||||
const {
|
||||
data: microagents,
|
||||
isLoading,
|
||||
isError,
|
||||
refetch,
|
||||
isRefetching,
|
||||
} = useConversationMicroagents({
|
||||
agentState: curAgentState,
|
||||
conversationId,
|
||||
enabled: true,
|
||||
});
|
||||
@@ -37,6 +44,10 @@ export function MicroagentsModal({
|
||||
}));
|
||||
};
|
||||
|
||||
const isAgentReady = ![AgentState.LOADING, AgentState.INIT].includes(
|
||||
curAgentState,
|
||||
);
|
||||
|
||||
return (
|
||||
<ModalBackdrop onClose={onClose}>
|
||||
<ModalBody
|
||||
@@ -45,10 +56,40 @@ export function MicroagentsModal({
|
||||
testID="microagents-modal"
|
||||
>
|
||||
<div className="flex flex-col gap-6 w-full">
|
||||
<BaseModalTitle title={t(I18nKey.MICROAGENTS_MODAL$TITLE)} />
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<BaseModalTitle title={t(I18nKey.MICROAGENTS_MODAL$TITLE)} />
|
||||
{isAgentReady && (
|
||||
<BrandButton
|
||||
testId="refresh-microagents"
|
||||
type="button"
|
||||
variant="primary"
|
||||
className="flex items-center gap-2"
|
||||
onClick={refetch}
|
||||
isDisabled={isLoading || isRefetching}
|
||||
>
|
||||
<RefreshCw
|
||||
size={16}
|
||||
className={`${isRefetching ? "animate-spin" : ""}`}
|
||||
/>
|
||||
{t(I18nKey.BUTTON$REFRESH)}
|
||||
</BrandButton>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isAgentReady && (
|
||||
<span className="text-sm text-gray-400">
|
||||
{t(I18nKey.MICROAGENTS_MODAL$WARNING)}
|
||||
</span>
|
||||
)}
|
||||
|
||||
<div className="w-full h-[60vh] overflow-auto rounded-md">
|
||||
{!isAgentReady && (
|
||||
<div className="w-full h-full flex items-center text-center justify-center text-2xl text-tertiary-light">
|
||||
{t(I18nKey.DIFF_VIEWER$WAITING_FOR_RUNTIME)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isLoading && (
|
||||
<div className="flex justify-center items-center py-8">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-primary" />
|
||||
@@ -56,6 +97,7 @@ export function MicroagentsModal({
|
||||
)}
|
||||
|
||||
{!isLoading &&
|
||||
isAgentReady &&
|
||||
(isError || !microagents || microagents.length === 0) && (
|
||||
<div className="flex items-center justify-center h-full p-4">
|
||||
<p className="text-gray-400">
|
||||
@@ -66,75 +108,81 @@ export function MicroagentsModal({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isLoading && microagents && microagents.length > 0 && (
|
||||
<div className="p-2 space-y-3">
|
||||
{microagents.map((agent) => {
|
||||
const isExpanded = expandedAgents[agent.name] || false;
|
||||
{!isLoading &&
|
||||
isAgentReady &&
|
||||
microagents &&
|
||||
microagents.length > 0 && (
|
||||
<div className="p-2 space-y-3">
|
||||
{microagents.map((agent) => {
|
||||
const isExpanded = expandedAgents[agent.name] || false;
|
||||
|
||||
return (
|
||||
<div key={agent.name} className="rounded-md overflow-hidden">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleAgent(agent.name)}
|
||||
className="w-full py-3 px-2 text-left flex items-center justify-between hover:bg-gray-700 transition-colors"
|
||||
return (
|
||||
<div
|
||||
key={agent.name}
|
||||
className="rounded-md overflow-hidden"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<h3 className="font-bold text-gray-100">
|
||||
{agent.name}
|
||||
</h3>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<span className="px-2 py-1 text-xs rounded-full bg-gray-800 mr-2">
|
||||
{agent.type === "repo" ? "Repository" : "Knowledge"}
|
||||
</span>
|
||||
<span className="text-gray-300">
|
||||
{isExpanded ? (
|
||||
<ChevronDown size={18} />
|
||||
) : (
|
||||
<ChevronRight size={18} />
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleAgent(agent.name)}
|
||||
className="w-full py-3 px-2 text-left flex items-center justify-between hover:bg-gray-700 transition-colors"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<h3 className="font-bold text-gray-100">
|
||||
{agent.name}
|
||||
</h3>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<span className="px-2 py-1 text-xs rounded-full bg-gray-800 mr-2">
|
||||
{agent.type === "repo" ? "Repository" : "Knowledge"}
|
||||
</span>
|
||||
<span className="text-gray-300">
|
||||
{isExpanded ? (
|
||||
<ChevronDown size={18} />
|
||||
) : (
|
||||
<ChevronRight size={18} />
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{isExpanded && (
|
||||
<div className="px-2 pb-3 pt-1">
|
||||
{agent.triggers && agent.triggers.length > 0 && (
|
||||
<div className="mt-2 mb-3">
|
||||
{isExpanded && (
|
||||
<div className="px-2 pb-3 pt-1">
|
||||
{agent.triggers && agent.triggers.length > 0 && (
|
||||
<div className="mt-2 mb-3">
|
||||
<h4 className="text-sm font-semibold text-gray-300 mb-2">
|
||||
{t(I18nKey.MICROAGENTS_MODAL$TRIGGERS)}
|
||||
</h4>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{agent.triggers.map((trigger) => (
|
||||
<span
|
||||
key={trigger}
|
||||
className="px-2 py-1 text-xs rounded-full bg-blue-900"
|
||||
>
|
||||
{trigger}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-2">
|
||||
<h4 className="text-sm font-semibold text-gray-300 mb-2">
|
||||
{t(I18nKey.MICROAGENTS_MODAL$TRIGGERS)}
|
||||
{t(I18nKey.MICROAGENTS_MODAL$CONTENT)}
|
||||
</h4>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{agent.triggers.map((trigger) => (
|
||||
<span
|
||||
key={trigger}
|
||||
className="px-2 py-1 text-xs rounded-full bg-blue-900"
|
||||
>
|
||||
{trigger}
|
||||
</span>
|
||||
))}
|
||||
<div className="text-sm mt-2 p-3 bg-gray-900 rounded-md overflow-auto text-gray-300 max-h-[400px] shadow-inner">
|
||||
<pre className="whitespace-pre-wrap font-mono text-sm leading-relaxed">
|
||||
{agent.content ||
|
||||
t(I18nKey.MICROAGENTS_MODAL$NO_CONTENT)}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-2">
|
||||
<h4 className="text-sm font-semibold text-gray-300 mb-2">
|
||||
{t(I18nKey.MICROAGENTS_MODAL$CONTENT)}
|
||||
</h4>
|
||||
<div className="text-sm mt-2 p-3 bg-gray-900 rounded-md overflow-auto text-gray-300 max-h-[400px] shadow-inner">
|
||||
<pre className="whitespace-pre-wrap font-mono text-sm leading-relaxed">
|
||||
{agent.content ||
|
||||
t(I18nKey.MICROAGENTS_MODAL$NO_CONTENT)}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ModalBody>
|
||||
</ModalBackdrop>
|
||||
|
||||
@@ -118,7 +118,7 @@ export function SystemMessageModal({
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="h-[60vh] overflow-auto rounded-md">
|
||||
<div className="max-h-[51vh] overflow-auto rounded-md">
|
||||
{activeTab === "system" && (
|
||||
<div className="p-4 whitespace-pre-wrap font-mono text-sm leading-relaxed text-gray-300 shadow-inner">
|
||||
{systemMessage.content}
|
||||
|
||||
@@ -1,20 +1,26 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useConfig } from "#/hooks/query/use-config";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { useUserProviders } from "#/hooks/use-user-providers";
|
||||
|
||||
export function RepoProviderLinks() {
|
||||
const { t } = useTranslation();
|
||||
const { data: config } = useConfig();
|
||||
const { providers } = useUserProviders();
|
||||
|
||||
const githubHref = config
|
||||
? `https://github.com/apps/${config.APP_SLUG}/installations/new`
|
||||
: "";
|
||||
|
||||
const hasGithubProvider = providers.includes("github");
|
||||
|
||||
return (
|
||||
<div className="flex flex-col text-sm underline underline-offset-2 text-content-2 gap-4 w-fit">
|
||||
<a href={githubHref} target="_blank" rel="noopener noreferrer">
|
||||
{t(I18nKey.HOME$ADD_GITHUB_REPOS)}
|
||||
</a>
|
||||
{hasGithubProvider && (
|
||||
<a href={githubHref} target="_blank" rel="noopener noreferrer">
|
||||
{t(I18nKey.HOME$ADD_GITHUB_REPOS)}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { FaInfoCircle } from "react-icons/fa";
|
||||
import { TaskGroup } from "./task-group";
|
||||
import { useSuggestedTasks } from "#/hooks/query/use-suggested-tasks";
|
||||
import { TaskSuggestionsSkeleton } from "./task-suggestions-skeleton";
|
||||
import { cn } from "#/utils/utils";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { TooltipButton } from "#/components/shared/buttons/tooltip-button";
|
||||
|
||||
interface TaskSuggestionsProps {
|
||||
filterFor?: string | null;
|
||||
@@ -23,7 +25,19 @@ export function TaskSuggestions({ filterFor }: TaskSuggestionsProps) {
|
||||
data-testid="task-suggestions"
|
||||
className={cn("flex flex-col w-full", !hasSuggestedTasks && "gap-6")}
|
||||
>
|
||||
<h2 className="heading">{t(I18nKey.TASKS$SUGGESTED_TASKS)}</h2>
|
||||
<div className="flex items-center gap-2">
|
||||
<h2 className="heading">{t(I18nKey.TASKS$SUGGESTED_TASKS)}</h2>
|
||||
<TooltipButton
|
||||
testId="task-suggestions-info"
|
||||
tooltip={t(I18nKey.TASKS$TASK_SUGGESTIONS_TOOLTIP)}
|
||||
ariaLabel={t(I18nKey.TASKS$TASK_SUGGESTIONS_INFO)}
|
||||
className="text-[#9099AC] hover:text-white"
|
||||
placement="bottom"
|
||||
tooltipClassName="max-w-[348px]"
|
||||
>
|
||||
<FaInfoCircle size={16} />
|
||||
</TooltipButton>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-6">
|
||||
{isLoading && <TaskSuggestionsSkeleton />}
|
||||
|
||||
@@ -108,7 +108,12 @@ export function ApiKeysManager() {
|
||||
<tbody>
|
||||
{apiKeys.map((key) => (
|
||||
<tr key={key.id} className="border-t border-tertiary">
|
||||
<td className="p-3 text-sm">{key.name}</td>
|
||||
<td
|
||||
className="p-3 text-sm truncate max-w-[160px]"
|
||||
title={key.name}
|
||||
>
|
||||
{key.name}
|
||||
</td>
|
||||
<td className="p-3 text-sm">
|
||||
{formatDate(key.created_at)}
|
||||
</td>
|
||||
|
||||
@@ -32,7 +32,7 @@ export function BrandButton({
|
||||
type={type}
|
||||
onClick={onClick}
|
||||
className={cn(
|
||||
"w-fit p-2 text-sm rounded-sm disabled:opacity-30 disabled:cursor-not-allowed hover:opacity-80",
|
||||
"w-fit p-2 text-sm rounded-sm disabled:opacity-30 disabled:cursor-not-allowed hover:opacity-80 cursor-pointer",
|
||||
variant === "primary" && "bg-primary text-[#0D0F11]",
|
||||
variant === "secondary" && "border border-primary text-primary",
|
||||
variant === "danger" && "bg-red-600 text-white hover:bg-red-700",
|
||||
|
||||
@@ -73,7 +73,7 @@ export function DeleteApiKeyModal({
|
||||
footer={modalFooter}
|
||||
>
|
||||
<div data-testid="delete-api-key-modal">
|
||||
<p className="text-sm">
|
||||
<p className="text-sm break-all">
|
||||
{t(I18nKey.SETTINGS$DELETE_API_KEY_CONFIRMATION, {
|
||||
name: keyToDelete.name,
|
||||
})}
|
||||
|
||||
@@ -34,10 +34,15 @@ export function SecretListItem({
|
||||
data-testid="secret-item"
|
||||
className="border-t border-[#717888] last-of-type:border-b max-w-[830px] py-[13px] flex w-full items-center"
|
||||
>
|
||||
<td className="w-1/4 text-sm text-content-2">{title}</td>
|
||||
<td className="w-1/4 text-sm text-content-2 truncate" title={title}>
|
||||
{title}
|
||||
</td>
|
||||
|
||||
<td className="w-1/2 truncate overflow-hidden whitespace-nowrap text-sm text-content-2 opacity-80 italic">
|
||||
{description || "-"}
|
||||
<td
|
||||
className="w-1/2 truncate overflow-hidden whitespace-nowrap text-sm text-content-2 opacity-80 italic"
|
||||
title={description || ""}
|
||||
>
|
||||
{description || ""}
|
||||
</td>
|
||||
|
||||
<td className="w-1/4 flex items-center justify-end gap-4">
|
||||
|
||||
@@ -26,14 +26,14 @@ export function UserActions({ onLogout, user, isLoading }: UserActionsProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div data-testid="user-actions" className="w-8 h-8 relative">
|
||||
<div data-testid="user-actions" className="w-8 h-8 relative cursor-pointer">
|
||||
<UserAvatar
|
||||
avatarUrl={user?.avatar_url}
|
||||
onClick={toggleAccountMenu}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
|
||||
{accountContextMenuIsVisible && (
|
||||
{accountContextMenuIsVisible && !!user && (
|
||||
<AccountSettingsContextMenu
|
||||
onLogout={handleLogout}
|
||||
onClose={closeAccountMenu}
|
||||
|
||||
@@ -21,7 +21,7 @@ export function UserAvatar({ onClick, avatarUrl, isLoading }: UserAvatarProps) {
|
||||
ariaLabel={t(I18nKey.USER$ACCOUNT_SETTINGS)}
|
||||
onClick={onClick}
|
||||
className={cn(
|
||||
"w-8 h-8 rounded-full flex items-center justify-center",
|
||||
"w-8 h-8 rounded-full flex items-center justify-center cursor-pointer",
|
||||
isLoading && "bg-transparent",
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -16,7 +16,7 @@ export function SuggestionItem({ suggestion, onClick }: SuggestionItemProps) {
|
||||
type="button"
|
||||
data-testid="suggestion"
|
||||
onClick={() => onClick(suggestion.value)}
|
||||
className="text-[16px] leading-6 -tracking-[0.01em] text-center w-full p-3 font-semibold"
|
||||
className="text-[16px] leading-6 -tracking-[0.01em] text-center w-full p-3 font-semibold cursor-pointer"
|
||||
>
|
||||
{t(suggestion.label)}
|
||||
</button>
|
||||
|
||||
@@ -20,13 +20,12 @@ export function ActionButton({
|
||||
<button
|
||||
onClick={() => handleAction(action)}
|
||||
disabled={isDisabled}
|
||||
className="relative overflow-visible cursor-default hover:cursor-pointer group disabled:cursor-not-allowed transition-all duration-300 ease-in-out"
|
||||
className="relative overflow-visible cursor-default hover:cursor-pointer group disabled:cursor-not-allowed transition-colors duration-300 ease-in-out border border-transparent hover:border-red-400/40 rounded-full p-1"
|
||||
type="button"
|
||||
>
|
||||
<span className="relative group-hover:filter group-hover:drop-shadow-[0_0_5px_rgba(255,64,0,0.4)]">
|
||||
{children}
|
||||
</span>
|
||||
<span className="absolute -inset-[5px] border-2 border-red-400/40 rounded-full opacity-0 group-hover:opacity-100 transition-opacity duration-300 ease-in-out" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
@@ -29,6 +29,7 @@ export function ConversationPanelButton({
|
||||
<FaListUl
|
||||
size={22}
|
||||
className={cn(
|
||||
"cursor-pointer",
|
||||
isOpen ? "text-white" : "text-[#9099AC]",
|
||||
disabled && "opacity-50",
|
||||
)}
|
||||
|
||||
@@ -15,7 +15,7 @@ export function StopButton({ isDisabled, onClick }: StopButtonProps) {
|
||||
disabled={isDisabled}
|
||||
onClick={onClick}
|
||||
type="button"
|
||||
className="border border-white rounded-lg w-6 h-6 hover:bg-neutral-500 focus:bg-neutral-500 flex items-center justify-center"
|
||||
className="border border-white rounded-lg w-6 h-6 hover:bg-neutral-500 focus:bg-neutral-500 flex items-center justify-center cursor-pointer"
|
||||
>
|
||||
<div className="w-[10px] h-[10px] bg-white" />
|
||||
</button>
|
||||
|
||||
@@ -15,7 +15,7 @@ export function SubmitButton({ isDisabled, onClick }: SubmitButtonProps) {
|
||||
disabled={isDisabled}
|
||||
onClick={onClick}
|
||||
type="submit"
|
||||
className="border border-white rounded-lg w-6 h-6 hover:bg-neutral-500 focus:bg-neutral-500 flex items-center justify-center"
|
||||
className="border border-white rounded-lg w-6 h-6 hover:bg-neutral-500 focus:bg-neutral-500 flex items-center justify-center cursor-pointer"
|
||||
>
|
||||
<ArrowSendIcon />
|
||||
</button>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Tooltip } from "@heroui/react";
|
||||
import { Tooltip, TooltipProps } from "@heroui/react";
|
||||
import React, { ReactNode } from "react";
|
||||
import { NavLink } from "react-router";
|
||||
import { cn } from "#/utils/utils";
|
||||
@@ -12,7 +12,9 @@ export interface TooltipButtonProps {
|
||||
ariaLabel: string;
|
||||
testId?: string;
|
||||
className?: React.HTMLAttributes<HTMLButtonElement>["className"];
|
||||
tooltipClassName?: React.HTMLAttributes<HTMLDivElement>["className"];
|
||||
disabled?: boolean;
|
||||
placement?: TooltipProps["placement"];
|
||||
}
|
||||
|
||||
export function TooltipButton({
|
||||
@@ -24,7 +26,9 @@ export function TooltipButton({
|
||||
ariaLabel,
|
||||
testId,
|
||||
className,
|
||||
tooltipClassName,
|
||||
disabled = false,
|
||||
placement = "right",
|
||||
}: TooltipButtonProps) {
|
||||
const handleClick = (e: React.MouseEvent) => {
|
||||
if (onClick && !disabled) {
|
||||
@@ -118,7 +122,12 @@ export function TooltipButton({
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip content={tooltip} closeDelay={100} placement="right">
|
||||
<Tooltip
|
||||
content={tooltip}
|
||||
closeDelay={100}
|
||||
placement={placement}
|
||||
className={tooltipClassName}
|
||||
>
|
||||
{content}
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
@@ -18,7 +18,7 @@ export function TrajectoryActionButton({
|
||||
type="button"
|
||||
data-testid={testId}
|
||||
onClick={onClick}
|
||||
className="button-base p-1 hover:bg-neutral-500"
|
||||
className="button-base p-1 hover:bg-neutral-500 cursor-pointer"
|
||||
>
|
||||
{icon}
|
||||
</button>
|
||||
|
||||
@@ -68,7 +68,7 @@ export function ModelSelector({
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col md:flex-row w-[full] md:w-[680px] justify-between gap-4 md:gap-[46px]">
|
||||
<div className="flex flex-col md:flex-row w-[full] max-w-[680px] justify-between gap-4 md:gap-[46px]">
|
||||
<fieldset className="flex flex-col gap-2.5 w-full">
|
||||
<label className="text-sm">{t(I18nKey.LLM$PROVIDER)}</label>
|
||||
<Autocomplete
|
||||
|
||||
31
frontend/src/hooks/mutation/use-stop-conversation.ts
Normal file
31
frontend/src/hooks/mutation/use-stop-conversation.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
|
||||
export const useStopConversation = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: (variables: { conversationId: string }) =>
|
||||
OpenHands.stopConversation(variables.conversationId),
|
||||
onMutate: async () => {
|
||||
await queryClient.cancelQueries({ queryKey: ["user", "conversations"] });
|
||||
const previousConversations = queryClient.getQueryData([
|
||||
"user",
|
||||
"conversations",
|
||||
]);
|
||||
|
||||
return { previousConversations };
|
||||
},
|
||||
onError: (_, __, context) => {
|
||||
if (context?.previousConversations) {
|
||||
queryClient.setQueryData(
|
||||
["user", "conversations"],
|
||||
context.previousConversations,
|
||||
);
|
||||
}
|
||||
},
|
||||
onSettled: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["user", "conversations"] });
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -1,12 +1,15 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import { AgentState } from "#/types/agent-state";
|
||||
|
||||
interface UseConversationMicroagentsOptions {
|
||||
agentState?: AgentState;
|
||||
conversationId: string | undefined;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
export const useConversationMicroagents = ({
|
||||
agentState,
|
||||
conversationId,
|
||||
enabled = true,
|
||||
}: UseConversationMicroagentsOptions) =>
|
||||
@@ -19,7 +22,11 @@ export const useConversationMicroagents = ({
|
||||
const data = await OpenHands.getMicroagents(conversationId);
|
||||
return data.microagents;
|
||||
},
|
||||
enabled: !!conversationId && enabled,
|
||||
enabled:
|
||||
!!conversationId &&
|
||||
enabled &&
|
||||
agentState !== AgentState.LOADING &&
|
||||
agentState !== AgentState.INIT,
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
gcTime: 1000 * 60 * 15, // 15 minutes
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import { useConversationId } from "#/hooks/use-conversation-id";
|
||||
import { useConfig } from "#/hooks/query/use-config";
|
||||
|
||||
export interface FeedbackData {
|
||||
exists: boolean;
|
||||
@@ -10,6 +11,7 @@ export interface FeedbackData {
|
||||
|
||||
export const useFeedbackExists = (eventId?: number) => {
|
||||
const { conversationId } = useConversationId();
|
||||
const { data: config } = useConfig();
|
||||
|
||||
return useQuery<FeedbackData>({
|
||||
queryKey: ["feedback", "exists", conversationId, eventId],
|
||||
@@ -17,7 +19,7 @@ export const useFeedbackExists = (eventId?: number) => {
|
||||
if (!eventId) return { exists: false };
|
||||
return OpenHands.checkFeedbackExists(conversationId, eventId);
|
||||
},
|
||||
enabled: !!eventId,
|
||||
enabled: !!eventId && config?.APP_MODE === "saas",
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
gcTime: 1000 * 60 * 15, // 15 minutes
|
||||
});
|
||||
|
||||
@@ -97,6 +97,9 @@ export enum I18nKey {
|
||||
BROWSER$EMPTY_MESSAGE = "BROWSER$EMPTY_MESSAGE",
|
||||
SETTINGS$TITLE = "SETTINGS$TITLE",
|
||||
CONVERSATION$START_NEW = "CONVERSATION$START_NEW",
|
||||
CONVERSATION$REPOSITORY = "CONVERSATION$REPOSITORY",
|
||||
CONVERSATION$BRANCH = "CONVERSATION$BRANCH",
|
||||
CONVERSATION$GIT_PROVIDER = "CONVERSATION$GIT_PROVIDER",
|
||||
ACCOUNT_SETTINGS$TITLE = "ACCOUNT_SETTINGS$TITLE",
|
||||
WORKSPACE$TERMINAL_TAB_LABEL = "WORKSPACE$TERMINAL_TAB_LABEL",
|
||||
WORKSPACE$BROWSER_TAB_LABEL = "WORKSPACE$BROWSER_TAB_LABEL",
|
||||
@@ -259,6 +262,7 @@ export enum I18nKey {
|
||||
CHAT_INTERFACE$AGENT_RUNNING_MESSAGE = "CHAT_INTERFACE$AGENT_RUNNING_MESSAGE",
|
||||
CHAT_INTERFACE$AGENT_AWAITING_USER_INPUT_MESSAGE = "CHAT_INTERFACE$AGENT_AWAITING_USER_INPUT_MESSAGE",
|
||||
CHAT_INTERFACE$AGENT_RATE_LIMITED_MESSAGE = "CHAT_INTERFACE$AGENT_RATE_LIMITED_MESSAGE",
|
||||
CHAT_INTERFACE$AGENT_RATE_LIMITED_STOPPED_MESSAGE = "CHAT_INTERFACE$AGENT_RATE_LIMITED_STOPPED_MESSAGE",
|
||||
CHAT_INTERFACE$AGENT_PAUSED_MESSAGE = "CHAT_INTERFACE$AGENT_PAUSED_MESSAGE",
|
||||
LANDING$TITLE = "LANDING$TITLE",
|
||||
LANDING$SUBTITLE = "LANDING$SUBTITLE",
|
||||
@@ -290,12 +294,18 @@ export enum I18nKey {
|
||||
USER$ACCOUNT_SETTINGS = "USER$ACCOUNT_SETTINGS",
|
||||
JUPYTER$OUTPUT_LABEL = "JUPYTER$OUTPUT_LABEL",
|
||||
BUTTON$STOP = "BUTTON$STOP",
|
||||
BUTTON$EDIT_TITLE = "BUTTON$EDIT_TITLE",
|
||||
BUTTON$DOWNLOAD_VIA_VSCODE = "BUTTON$DOWNLOAD_VIA_VSCODE",
|
||||
BUTTON$DISPLAY_COST = "BUTTON$DISPLAY_COST",
|
||||
BUTTON$SHOW_AGENT_TOOLS_AND_METADATA = "BUTTON$SHOW_AGENT_TOOLS_AND_METADATA",
|
||||
LANDING$ATTACH_IMAGES = "LANDING$ATTACH_IMAGES",
|
||||
LANDING$OPEN_REPO = "LANDING$OPEN_REPO",
|
||||
LANDING$REPLAY = "LANDING$REPLAY",
|
||||
LANDING$UPLOAD_TRAJECTORY = "LANDING$UPLOAD_TRAJECTORY",
|
||||
LANDING$RECENT_CONVERSATION = "LANDING$RECENT_CONVERSATION",
|
||||
CONVERSATION$CONFIRM_DELETE = "CONVERSATION$CONFIRM_DELETE",
|
||||
CONVERSATION$CONFIRM_STOP = "CONVERSATION$CONFIRM_STOP",
|
||||
CONVERSATION$STOP_WARNING = "CONVERSATION$STOP_WARNING",
|
||||
CONVERSATION$METRICS_INFO = "CONVERSATION$METRICS_INFO",
|
||||
CONVERSATION$CREATED = "CONVERSATION$CREATED",
|
||||
CONVERSATION$AGO = "CONVERSATION$AGO",
|
||||
@@ -345,6 +355,7 @@ export enum I18nKey {
|
||||
SETTINGS$DONT_KNOW_API_KEY = "SETTINGS$DONT_KNOW_API_KEY",
|
||||
SETTINGS$CLICK_FOR_INSTRUCTIONS = "SETTINGS$CLICK_FOR_INSTRUCTIONS",
|
||||
SETTINGS$SAVED = "SETTINGS$SAVED",
|
||||
SETTINGS$SAVED_WARNING = "SETTINGS$SAVED_WARNING",
|
||||
SETTINGS$RESET = "SETTINGS$RESET",
|
||||
SETTINGS$API_KEYS = "SETTINGS$API_KEYS",
|
||||
SETTINGS$API_KEYS_DESCRIPTION = "SETTINGS$API_KEYS_DESCRIPTION",
|
||||
@@ -562,6 +573,7 @@ export enum I18nKey {
|
||||
CONVERSATION$NO_MICROAGENTS = "CONVERSATION$NO_MICROAGENTS",
|
||||
CONVERSATION$FAILED_TO_FETCH_MICROAGENTS = "CONVERSATION$FAILED_TO_FETCH_MICROAGENTS",
|
||||
MICROAGENTS_MODAL$TITLE = "MICROAGENTS_MODAL$TITLE",
|
||||
MICROAGENTS_MODAL$WARNING = "MICROAGENTS_MODAL$WARNING",
|
||||
MICROAGENTS_MODAL$TRIGGERS = "MICROAGENTS_MODAL$TRIGGERS",
|
||||
MICROAGENTS_MODAL$INPUTS = "MICROAGENTS_MODAL$INPUTS",
|
||||
MICROAGENTS_MODAL$TOOLS = "MICROAGENTS_MODAL$TOOLS",
|
||||
@@ -611,6 +623,8 @@ export enum I18nKey {
|
||||
REPOSITORY$SELECT_REPO = "REPOSITORY$SELECT_REPO",
|
||||
TASKS$SUGGESTED_TASKS = "TASKS$SUGGESTED_TASKS",
|
||||
TASKS$NO_TASKS_AVAILABLE = "TASKS$NO_TASKS_AVAILABLE",
|
||||
TASKS$TASK_SUGGESTIONS_INFO = "TASKS$TASK_SUGGESTIONS_INFO",
|
||||
TASKS$TASK_SUGGESTIONS_TOOLTIP = "TASKS$TASK_SUGGESTIONS_TOOLTIP",
|
||||
PAYMENT$SPECIFY_AMOUNT_USD = "PAYMENT$SPECIFY_AMOUNT_USD",
|
||||
GIT$BITBUCKET_TOKEN_HELP_LINK = "GIT$BITBUCKET_TOKEN_HELP_LINK",
|
||||
GIT$BITBUCKET_TOKEN_SEE_MORE_LINK = "GIT$BITBUCKET_TOKEN_SEE_MORE_LINK",
|
||||
|
||||
@@ -27,7 +27,7 @@ i18n
|
||||
.init({
|
||||
fallbackLng: "en",
|
||||
debug: import.meta.env.NODE_ENV === "development",
|
||||
load: "languageOnly",
|
||||
load: "currentOnly",
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
|
||||
@@ -1551,6 +1551,54 @@
|
||||
"de": "Neue Unterhaltung starten",
|
||||
"uk": "Почати нову розмову"
|
||||
},
|
||||
"CONVERSATION$REPOSITORY": {
|
||||
"en": "Repository",
|
||||
"ja": "リポジトリ",
|
||||
"zh-CN": "仓库",
|
||||
"zh-TW": "倉庫",
|
||||
"ko-KR": "저장소",
|
||||
"no": "Repository",
|
||||
"it": "Repository",
|
||||
"pt": "Repositório",
|
||||
"es": "Repositorio",
|
||||
"ar": "المستودع",
|
||||
"fr": "Dépôt",
|
||||
"tr": "Depo",
|
||||
"de": "Repository",
|
||||
"uk": "Репозиторій"
|
||||
},
|
||||
"CONVERSATION$BRANCH": {
|
||||
"en": "Branch",
|
||||
"ja": "ブランチ",
|
||||
"zh-CN": "分支",
|
||||
"zh-TW": "分支",
|
||||
"ko-KR": "브랜치",
|
||||
"no": "Gren",
|
||||
"it": "Ramo",
|
||||
"pt": "Ramo",
|
||||
"es": "Rama",
|
||||
"ar": "الفرع",
|
||||
"fr": "Branche",
|
||||
"tr": "Dal",
|
||||
"de": "Zweig",
|
||||
"uk": "Гілка"
|
||||
},
|
||||
"CONVERSATION$GIT_PROVIDER": {
|
||||
"en": "Git Provider",
|
||||
"ja": "Git プロバイダー",
|
||||
"zh-CN": "Git 提供商",
|
||||
"zh-TW": "Git 提供商",
|
||||
"ko-KR": "Git 제공업체",
|
||||
"no": "Git-leverandør",
|
||||
"it": "Provider Git",
|
||||
"pt": "Provedor Git",
|
||||
"es": "Proveedor Git",
|
||||
"ar": "مزود Git",
|
||||
"fr": "Fournisseur Git",
|
||||
"tr": "Git Sağlayıcısı",
|
||||
"de": "Git-Anbieter",
|
||||
"uk": "Git-провайдер"
|
||||
},
|
||||
"ACCOUNT_SETTINGS$TITLE": {
|
||||
"en": "Account Settings",
|
||||
"ja": "アカウント設定",
|
||||
@@ -4064,7 +4112,7 @@
|
||||
"uk": "Зупинено"
|
||||
},
|
||||
"CHAT_INTERFACE$INITIALIZING_AGENT_LOADING_MESSAGE": {
|
||||
"en": "Initializing Agent...",
|
||||
"en": "Initializing agent...",
|
||||
"de": "Agent wird initialisiert...",
|
||||
"zh-CN": "正在初始化智能体...",
|
||||
"zh-TW": "正在初始化智能體...",
|
||||
@@ -4128,20 +4176,36 @@
|
||||
"uk": "Агент очікує на введення даних від користувача..."
|
||||
},
|
||||
"CHAT_INTERFACE$AGENT_RATE_LIMITED_MESSAGE": {
|
||||
"en": "Agent is Rate Limited",
|
||||
"zh-CN": "智能体已达到速率限制",
|
||||
"zh-TW": "智慧代理已達到速率限制",
|
||||
"de": "Agent ist ratenbegrenzt",
|
||||
"ko-KR": "에이전트가 속도 제한되었습니다",
|
||||
"no": "Agenten er hastighetsbegrenset",
|
||||
"it": "L'agente è limitato dalla frequenza",
|
||||
"pt": "O agente está com limite de taxa",
|
||||
"es": "El agente está limitado por tasa",
|
||||
"ar": "الوكيل مقيد بحد السرعة",
|
||||
"fr": "L'agent est limité en fréquence",
|
||||
"tr": "Ajan hız sınırına ulaştı",
|
||||
"ja": "エージェントがレート制限中",
|
||||
"uk": "Агента обмежено кількістю запитів"
|
||||
"en": "Agent is Rate Limited. Retrying...",
|
||||
"zh-CN": "智能体已达到速率限制。正在重试...",
|
||||
"zh-TW": "智慧代理已達到速率限制。正在重試...",
|
||||
"de": "Agent ist ratenbegrenzt. Wiederholungsversuch...",
|
||||
"ko-KR": "에이전트가 속도 제한되었습니다. 재시도 중...",
|
||||
"no": "Agenten er hastighetsbegrenset. Prøver på nytt...",
|
||||
"it": "L'agente è limitato dalla frequenza. Riprovando...",
|
||||
"pt": "O agente está com limite de taxa. Tentando novamente...",
|
||||
"es": "El agente está limitado por tasa. Reintentando...",
|
||||
"ar": "الوكيل مقيد بحد السرعة. إعادة المحاولة...",
|
||||
"fr": "L'agent est limité en fréquence. Nouvelle tentative...",
|
||||
"tr": "Ajan hız sınırına ulaştı. Yeniden deniyor...",
|
||||
"ja": "エージェントがレート制限中。再試行しています...",
|
||||
"uk": "Агента обмежено кількістю запитів. Повторюємо спробу..."
|
||||
},
|
||||
"CHAT_INTERFACE$AGENT_RATE_LIMITED_STOPPED_MESSAGE": {
|
||||
"en": "Agent is rate-limited. Stopped.",
|
||||
"zh-CN": "智能体已达到速率限制。已停止。",
|
||||
"zh-TW": "智慧代理已達到速率限制。已停止。",
|
||||
"de": "Agent ist ratenbegrenzt. Angehalten.",
|
||||
"ko-KR": "에이전트가 속도 제한되었습니다. 중지됨.",
|
||||
"no": "Agenten er hastighetsbegrenset. Stoppet.",
|
||||
"it": "L'agente è limitato dalla frequenza. Fermato.",
|
||||
"pt": "O agente está com limite de taxa. Parado.",
|
||||
"es": "El agente está limitado por tasa. Detenido.",
|
||||
"ar": "الوكيل مقيد بحد السرعة. توقف.",
|
||||
"fr": "L'agent est limité en fréquence. Arrêté.",
|
||||
"tr": "Ajan hız sınırına ulaştı. Durduruldu.",
|
||||
"ja": "エージェントがレート制限中。停止しました。",
|
||||
"uk": "Агента обмежено кількістю запитів. Зупинено."
|
||||
},
|
||||
"CHAT_INTERFACE$AGENT_PAUSED_MESSAGE": {
|
||||
"en": "Agent has paused.",
|
||||
@@ -4639,6 +4703,70 @@
|
||||
"tr": "Durdur",
|
||||
"uk": "Стоп"
|
||||
},
|
||||
"BUTTON$EDIT_TITLE": {
|
||||
"en": "Edit Title",
|
||||
"ja": "タイトルを編集",
|
||||
"zh-CN": "编辑标题",
|
||||
"zh-TW": "編輯標題",
|
||||
"ko-KR": "제목 편집",
|
||||
"fr": "Modifier le titre",
|
||||
"es": "Editar título",
|
||||
"de": "Titel bearbeiten",
|
||||
"it": "Modifica titolo",
|
||||
"pt": "Editar título",
|
||||
"ar": "تحرير العنوان",
|
||||
"no": "Rediger tittel",
|
||||
"tr": "Başlığı Düzenle",
|
||||
"uk": "Редагувати заголовок"
|
||||
},
|
||||
"BUTTON$DOWNLOAD_VIA_VSCODE": {
|
||||
"en": "Download via VS Code",
|
||||
"ja": "VS Code経由でダウンロード",
|
||||
"zh-CN": "通过VS Code下载",
|
||||
"zh-TW": "透過VS Code下載",
|
||||
"ko-KR": "VS Code를 통해 다운로드",
|
||||
"fr": "Télécharger via VS Code",
|
||||
"es": "Descargar a través de VS Code",
|
||||
"de": "Über VS Code herunterladen",
|
||||
"it": "Scarica tramite VS Code",
|
||||
"pt": "Baixar via VS Code",
|
||||
"ar": "تحميل عبر VS Code",
|
||||
"no": "Last ned via VS Code",
|
||||
"tr": "VS Code ile İndir",
|
||||
"uk": "Завантажити через VS Code"
|
||||
},
|
||||
"BUTTON$DISPLAY_COST": {
|
||||
"en": "Display Cost",
|
||||
"ja": "コストを表示",
|
||||
"zh-CN": "显示成本",
|
||||
"zh-TW": "顯示成本",
|
||||
"ko-KR": "비용 표시",
|
||||
"fr": "Afficher le coût",
|
||||
"es": "Mostrar costo",
|
||||
"de": "Kosten anzeigen",
|
||||
"it": "Mostra costo",
|
||||
"pt": "Mostrar custo",
|
||||
"ar": "عرض التكلفة",
|
||||
"no": "Vis kostnad",
|
||||
"tr": "Maliyeti Göster",
|
||||
"uk": "Показати вартість"
|
||||
},
|
||||
"BUTTON$SHOW_AGENT_TOOLS_AND_METADATA": {
|
||||
"en": "Show Agent Tools & Metadata",
|
||||
"ja": "エージェントツールとメタデータを表示",
|
||||
"zh-CN": "显示代理工具和元数据",
|
||||
"zh-TW": "顯示代理工具和元數據",
|
||||
"ko-KR": "에이전트 도구 및 메타데이터 표시",
|
||||
"fr": "Afficher les outils et métadonnées de l'agent",
|
||||
"es": "Mostrar herramientas y metadatos del agente",
|
||||
"de": "Agent-Tools und Metadaten anzeigen",
|
||||
"it": "Mostra strumenti e metadati dell'agente",
|
||||
"pt": "Mostrar ferramentas e metadados do agente",
|
||||
"ar": "عرض أدوات الوكيل والبيانات الوصفية",
|
||||
"no": "Vis agentverktøy og metadata",
|
||||
"tr": "Ajan Araçları ve Meta Verileri Göster",
|
||||
"uk": "Показати інструменти агента та метадані"
|
||||
},
|
||||
"LANDING$ATTACH_IMAGES": {
|
||||
"en": "Attach images",
|
||||
"ja": "画像を添付",
|
||||
@@ -4735,6 +4863,38 @@
|
||||
"de": "Löschen bestätigen",
|
||||
"uk": "Підтвердити видалення"
|
||||
},
|
||||
"CONVERSATION$CONFIRM_STOP": {
|
||||
"en": "Confirm Stop",
|
||||
"ja": "停止の確認",
|
||||
"zh-CN": "确认停止",
|
||||
"zh-TW": "確認停止",
|
||||
"ko-KR": "중지 확인",
|
||||
"no": "Bekreft stopp",
|
||||
"it": "Conferma arresto",
|
||||
"pt": "Confirmar parada",
|
||||
"es": "Confirmar detención",
|
||||
"ar": "تأكيد الإيقاف",
|
||||
"fr": "Confirmer l'arrêt",
|
||||
"tr": "Durdurmayı Onayla",
|
||||
"de": "Stopp bestätigen",
|
||||
"uk": "Підтвердити зупинку"
|
||||
},
|
||||
"CONVERSATION$STOP_WARNING": {
|
||||
"en": "Are you sure you want to stop this conversation?",
|
||||
"ja": "この会話を停止してもよろしいですか?",
|
||||
"zh-CN": "您确定要停止此对话吗?",
|
||||
"zh-TW": "您確定要停止此對話嗎?",
|
||||
"ko-KR": "이 대화를 중지하시겠습니까?",
|
||||
"no": "Er du sikker på at du vil stoppe denne samtalen?",
|
||||
"it": "Sei sicuro di voler fermare questa conversazione?",
|
||||
"pt": "Tem certeza de que deseja parar esta conversa?",
|
||||
"es": "¿Está seguro de que desea detener esta conversación?",
|
||||
"ar": "هل أنت متأكد أنك تريد إيقاف هذه المحادثة؟",
|
||||
"fr": "Êtes-vous sûr de vouloir arrêter cette conversation ?",
|
||||
"tr": "Bu konuşmayı durdurmak istediğinizden emin misiniz?",
|
||||
"de": "Sind Sie sicher, dass Sie dieses Gespräch stoppen möchten?",
|
||||
"uk": "Ви впевнені, що хочете зупинити цю розмову?"
|
||||
},
|
||||
"CONVERSATION$METRICS_INFO": {
|
||||
"en": "Conversation Metrics",
|
||||
"ja": "会話メトリクス",
|
||||
@@ -5519,6 +5679,22 @@
|
||||
"de": "Einstellungen gespeichert",
|
||||
"uk": "Налаштування збережено"
|
||||
},
|
||||
"SETTINGS$SAVED_WARNING": {
|
||||
"en": "Settings saved. For old conversations, you will need to stop and restart the conversation to see the changes.",
|
||||
"ja": "設定が保存されました。古い会話では、変更を確認するために会話を停止して再開する必要があります。",
|
||||
"zh-CN": "设置已保存。对于旧对话,您需要停止并重新开始对话才能看到更改。",
|
||||
"zh-TW": "設置已保存。對於舊對話,您需要停止並重新開始對話才能看到更改。",
|
||||
"ko-KR": "설정이 저장되었습니다. 기존 대화의 경우 변경사항을 보려면 대화를 중지하고 다시 시작해야 합니다.",
|
||||
"no": "Innstillinger lagret. For gamle samtaler må du stoppe og starte samtalen på nytt for å se endringene.",
|
||||
"it": "Impostazioni salvate. Per le conversazioni precedenti, dovrai fermare e riavviare la conversazione per vedere le modifiche.",
|
||||
"pt": "Configurações salvas. Para conversas antigas, você precisará parar e reiniciar a conversa para ver as alterações.",
|
||||
"es": "Configuración guardada. Para conversaciones antiguas, necesitarás detener y reiniciar la conversación para ver los cambios.",
|
||||
"ar": "تم حفظ الإعدادات. بالنسبة للمحادثات القديمة، ستحتاج إلى إيقاف وإعادة تشغيل المحادثة لرؤية التغييرات.",
|
||||
"fr": "Paramètres enregistrés. Pour les anciennes conversations, vous devrez arrêter et redémarrer la conversation pour voir les changements.",
|
||||
"tr": "Ayarlar kaydedildi. Eski konuşmalar için değişiklikleri görmek üzere konuşmayı durdurup yeniden başlatmanız gerekecek.",
|
||||
"de": "Einstellungen gespeichert. Für alte Gespräche müssen Sie das Gespräch stoppen und neu starten, um die Änderungen zu sehen.",
|
||||
"uk": "Налаштування збережено. Для старих розмов вам потрібно буде зупинити та перезапустити розмову, щоб побачити зміни."
|
||||
},
|
||||
"SETTINGS$RESET": {
|
||||
"en": "Settings reset",
|
||||
"ja": "設定がリセットされました",
|
||||
@@ -8991,6 +9167,22 @@
|
||||
"tr": "Kullanılabilir mikro ajanlar",
|
||||
"uk": "Доступні мікроагенти"
|
||||
},
|
||||
"MICROAGENTS_MODAL$WARNING": {
|
||||
"en": "If you update the microagents, you will need to stop the conversation and then click on the refresh button to see the changes.",
|
||||
"ja": "マイクロエージェントを更新する場合、会話を停止してから更新ボタンをクリックして変更を確認する必要があります。",
|
||||
"zh-CN": "如果您更新微代理,您需要停止对话,然后点击刷新按钮以查看更改。",
|
||||
"zh-TW": "如果您更新微代理,您需要停止對話,然後點擊重新整理按鈕以查看更改。",
|
||||
"ko-KR": "마이크로에이전트를 업데이트하는 경우 대화를 중지한 후 새로고침 버튼을 클릭하여 변경사항을 확인해야 합니다.",
|
||||
"no": "Hvis du oppdaterer mikroagentene, må du stoppe samtalen og deretter klikke på oppdater-knappen for å se endringene.",
|
||||
"ar": "إذا قمت بتحديث الوكلاء المصغرين، فستحتاج إلى إيقاف المحادثة ثم النقر على زر التحديث لرؤية التغييرات.",
|
||||
"de": "Wenn Sie die Mikroagenten aktualisieren, müssen Sie das Gespräch beenden und dann auf die Aktualisieren-Schaltfläche klicken, um die Änderungen zu sehen.",
|
||||
"fr": "Si vous mettez à jour les micro-agents, vous devrez arrêter la conversation puis cliquer sur le bouton actualiser pour voir les changements.",
|
||||
"it": "Se aggiorni i microagenti, dovrai fermare la conversazione e poi cliccare sul pulsante aggiorna per vedere le modifiche.",
|
||||
"pt": "Se você atualizar os microagentes, precisará parar a conversa e depois clicar no botão atualizar para ver as alterações.",
|
||||
"es": "Si actualiza los microagentes, necesitará detener la conversación y luego hacer clic en el botón actualizar para ver los cambios.",
|
||||
"tr": "Mikro ajanları güncellerseniz, konuşmayı durdurmanız ve ardından değişiklikleri görmek için yenile düğmesine tıklamanız gerekecektir.",
|
||||
"uk": "Якщо ви оновите мікроагенти, вам потрібно буде зупинити розмову, а потім натиснути кнопку оновлення, щоб побачити зміни."
|
||||
},
|
||||
"MICROAGENTS_MODAL$TRIGGERS": {
|
||||
"en": "Triggers",
|
||||
"ja": "トリガー",
|
||||
@@ -9775,6 +9967,38 @@
|
||||
"de": "Keine Aufgaben verfügbar",
|
||||
"uk": "Немає доступних завдань"
|
||||
},
|
||||
"TASKS$TASK_SUGGESTIONS_INFO": {
|
||||
"en": "Task suggestions information",
|
||||
"ja": "タスク提案情報",
|
||||
"zh-CN": "任务建议信息",
|
||||
"zh-TW": "任務建議資訊",
|
||||
"ko-KR": "작업 제안 정보",
|
||||
"no": "Oppgaveforslag informasjon",
|
||||
"it": "Informazioni sui suggerimenti di attività",
|
||||
"pt": "Informações de sugestões de tarefas",
|
||||
"es": "Información de sugerencias de tareas",
|
||||
"ar": "معلومات اقتراحات المهام",
|
||||
"fr": "Informations sur les suggestions de tâches",
|
||||
"tr": "Görev önerisi bilgileri",
|
||||
"de": "Aufgabenvorschlag-Informationen",
|
||||
"uk": "Інформація про пропозиції завдань"
|
||||
},
|
||||
"TASKS$TASK_SUGGESTIONS_TOOLTIP": {
|
||||
"en": "These are AI-curated task suggestions to help you get started with common development activities and best practices for your repository.",
|
||||
"ja": "これらは、リポジトリの一般的な開発活動とベストプラクティスを始めるのに役立つAIによってキュレーションされたタスク提案です。",
|
||||
"zh-CN": "这些是AI策划的任务建议,帮助您开始进行常见的开发活动和存储库的最佳实践。",
|
||||
"zh-TW": "這些是AI策劃的任務建議,幫助您開始進行常見的開發活動和存儲庫的最佳實踐。",
|
||||
"ko-KR": "이것은 저장소의 일반적인 개발 활동과 모범 사례를 시작할 수 있도록 도와주는 AI가 선별한 작업 제안입니다.",
|
||||
"no": "Dette er AI-kuraterte oppgaveforslag som hjelper deg å komme i gang med vanlige utviklingsaktiviteter og beste praksis for ditt depot.",
|
||||
"it": "Questi sono suggerimenti di attività curati dall'IA per aiutarti a iniziare con le attività di sviluppo comuni e le migliori pratiche per il tuo repository.",
|
||||
"pt": "Estas são sugestões de tarefas curadas por IA para ajudá-lo a começar com atividades de desenvolvimento comuns e melhores práticas para seu repositório.",
|
||||
"es": "Estas son sugerencias de tareas curadas por IA para ayudarte a comenzar con actividades de desarrollo comunes y mejores prácticas para tu repositorio.",
|
||||
"ar": "هذه اقتراحات مهام منسقة بواسطة الذكاء الاصطناعي لمساعدتك على البدء بأنشطة التطوير الشائعة وأفضل الممارسات لمستودعك.",
|
||||
"fr": "Ce sont des suggestions de tâches curées par l'IA pour vous aider à commencer avec les activités de développement courantes et les meilleures pratiques pour votre dépôt.",
|
||||
"tr": "Bunlar, deponuz için yaygın geliştirme faaliyetleri ve en iyi uygulamalarla başlamanıza yardımcı olmak için AI tarafından düzenlenmiş görev önerileridir.",
|
||||
"de": "Dies sind KI-kuratierte Aufgabenvorschläge, die Ihnen helfen, mit gängigen Entwicklungsaktivitäten und bewährten Praktiken für Ihr Repository zu beginnen.",
|
||||
"uk": "Це AI-курировані пропозиції завдань, які допоможуть вам розпочати з поширеними діяльностями розробки та найкращими практиками для вашого репозиторію."
|
||||
},
|
||||
"PAYMENT$SPECIFY_AMOUNT_USD": {
|
||||
"en": "Specify an amount in USD to add - min $10",
|
||||
"ja": "追加するUSD金額を指定してください - 最小$10",
|
||||
|
||||
@@ -192,7 +192,7 @@ function AppSettingsScreen() {
|
||||
placeholder={t(I18nKey.SETTINGS$MAXIMUM_BUDGET_USD)}
|
||||
min={1}
|
||||
step={1}
|
||||
className="w-[680px]" // Match the width of the language field
|
||||
className="w-full max-w-[680px]" // Match the width of the language field
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -24,7 +24,7 @@ function HomeScreen() {
|
||||
|
||||
<hr className="border-[#717888]" />
|
||||
|
||||
<main className="flex flex-col md:flex-row justify-between gap-8">
|
||||
<main className="flex flex-col lg:flex-row justify-between gap-8">
|
||||
<RepoConnector
|
||||
onRepoSelection={(title) => setSelectedRepoTitle(title)}
|
||||
/>
|
||||
|
||||
@@ -75,7 +75,7 @@ function LlmSettingsScreen() {
|
||||
}, [settings, resources]);
|
||||
|
||||
const handleSuccessfulMutation = () => {
|
||||
displaySuccessToast(t(I18nKey.SETTINGS$SAVED));
|
||||
displaySuccessToast(t(I18nKey.SETTINGS$SAVED_WARNING));
|
||||
setDirtyInputs({
|
||||
model: false,
|
||||
apiKey: false,
|
||||
|
||||
@@ -197,7 +197,7 @@ export default function MainApp() {
|
||||
return (
|
||||
<div
|
||||
data-testid="root-layout"
|
||||
className="bg-base p-3 h-screen md:min-w-[1024px] flex flex-col md:flex-row gap-3"
|
||||
className="bg-base p-3 h-screen lg:min-w-[1024px] flex flex-col md:flex-row gap-3"
|
||||
>
|
||||
<Sidebar />
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ function EmailInputSection({
|
||||
isEmailChanged && !isEmailValid
|
||||
? "border-red-500"
|
||||
: "border-tertiary"
|
||||
} flex-grow focus:outline-hidden focus:border-transparent focus:ring-0`}
|
||||
} flex-grow`}
|
||||
placeholder={t("SETTINGS$USER_EMAIL_LOADING")}
|
||||
data-testid="email-input"
|
||||
/>
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from "#/types/message";
|
||||
import { handleObservationMessage } from "./observations";
|
||||
import { appendInput } from "#/state/command-slice";
|
||||
import { appendJupyterInput } from "#/state/jupyter-slice";
|
||||
import { queryClient } from "#/query-client-config";
|
||||
|
||||
export function handleActionMessage(message: ActionMessage) {
|
||||
@@ -32,6 +33,10 @@ export function handleActionMessage(message: ActionMessage) {
|
||||
store.dispatch(appendInput(message.args.command));
|
||||
}
|
||||
|
||||
if (message.action === ActionType.RUN_IPYTHON) {
|
||||
store.dispatch(appendJupyterInput(message.args.code));
|
||||
}
|
||||
|
||||
if ("args" in message && "security_risk" in message.args) {
|
||||
store.dispatch(appendSecurityAnalyzerInput(message));
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ export interface UserMessageAction extends OpenHandsActionEvent<"message"> {
|
||||
}
|
||||
|
||||
export interface SystemMessageAction extends OpenHandsActionEvent<"system"> {
|
||||
source: "agent";
|
||||
source: "agent" | "environment";
|
||||
args: {
|
||||
content: string;
|
||||
tools: Array<Record<string, unknown>> | null;
|
||||
|
||||
104
frontend/src/utils/__tests__/custom-toast-handlers.test.ts
Normal file
104
frontend/src/utils/__tests__/custom-toast-handlers.test.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import toast from "react-hot-toast";
|
||||
import {
|
||||
displaySuccessToast,
|
||||
displayErrorToast,
|
||||
} from "../custom-toast-handlers";
|
||||
|
||||
// Mock react-hot-toast
|
||||
vi.mock("react-hot-toast", () => ({
|
||||
default: {
|
||||
success: vi.fn(),
|
||||
error: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe("custom-toast-handlers", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("displaySuccessToast", () => {
|
||||
it("should call toast.success with calculated duration for short message", () => {
|
||||
const shortMessage = "Settings saved";
|
||||
displaySuccessToast(shortMessage);
|
||||
|
||||
expect(toast.success).toHaveBeenCalledWith(
|
||||
shortMessage,
|
||||
expect.objectContaining({
|
||||
duration: 5000, // Should use minimum duration of 5000ms
|
||||
position: "top-right",
|
||||
style: expect.any(Object),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("should call toast.success with longer duration for long message", () => {
|
||||
const longMessage =
|
||||
"Settings saved. For old conversations, you will need to stop and restart the conversation to see the changes.";
|
||||
displaySuccessToast(longMessage);
|
||||
|
||||
expect(toast.success).toHaveBeenCalledWith(
|
||||
longMessage,
|
||||
expect.objectContaining({
|
||||
duration: expect.any(Number),
|
||||
position: "top-right",
|
||||
style: expect.any(Object),
|
||||
}),
|
||||
);
|
||||
|
||||
// Get the actual duration that was passed
|
||||
const callArgs = (
|
||||
toast.success as unknown as { mock: { calls: unknown[][] } }
|
||||
).mock.calls[0][1] as { duration: number };
|
||||
const actualDuration = callArgs.duration;
|
||||
|
||||
// For a long message, duration should be more than the minimum 5000ms
|
||||
expect(actualDuration).toBeGreaterThan(5000);
|
||||
// But should not exceed the maximum 10000ms
|
||||
expect(actualDuration).toBeLessThanOrEqual(10000);
|
||||
});
|
||||
});
|
||||
|
||||
describe("displayErrorToast", () => {
|
||||
it("should call toast.error with calculated duration for short message", () => {
|
||||
const shortMessage = "Error occurred";
|
||||
displayErrorToast(shortMessage);
|
||||
|
||||
expect(toast.error).toHaveBeenCalledWith(
|
||||
shortMessage,
|
||||
expect.objectContaining({
|
||||
duration: 4000, // Should use minimum duration of 4000ms for errors
|
||||
position: "top-right",
|
||||
style: expect.any(Object),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("should call toast.error with longer duration for long error message", () => {
|
||||
const longMessage =
|
||||
"A very long error message that should take more time to read and understand what went wrong with the operation.";
|
||||
displayErrorToast(longMessage);
|
||||
|
||||
expect(toast.error).toHaveBeenCalledWith(
|
||||
longMessage,
|
||||
expect.objectContaining({
|
||||
duration: expect.any(Number),
|
||||
position: "top-right",
|
||||
style: expect.any(Object),
|
||||
}),
|
||||
);
|
||||
|
||||
// Get the actual duration that was passed
|
||||
const callArgs = (
|
||||
toast.error as unknown as { mock: { calls: unknown[][] } }
|
||||
).mock.calls[0][1] as { duration: number };
|
||||
const actualDuration = callArgs.duration;
|
||||
|
||||
// For a long message, duration should be more than the minimum 4000ms
|
||||
expect(actualDuration).toBeGreaterThan(4000);
|
||||
// But should not exceed the maximum 10000ms
|
||||
expect(actualDuration).toBeLessThanOrEqual(10000);
|
||||
});
|
||||
});
|
||||
});
|
||||
53
frontend/src/utils/__tests__/toast-duration.test.ts
Normal file
53
frontend/src/utils/__tests__/toast-duration.test.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { calculateToastDuration } from "../toast-duration";
|
||||
|
||||
describe("calculateToastDuration", () => {
|
||||
it("should return minimum duration for short messages", () => {
|
||||
const shortMessage = "OK";
|
||||
const duration = calculateToastDuration(shortMessage, 5000);
|
||||
expect(duration).toBe(5000);
|
||||
});
|
||||
|
||||
it("should return minimum duration for messages that calculate below minimum", () => {
|
||||
const shortMessage = "Settings saved";
|
||||
const duration = calculateToastDuration(shortMessage, 5000);
|
||||
expect(duration).toBe(5000);
|
||||
});
|
||||
|
||||
it("should calculate longer duration for long messages", () => {
|
||||
const longMessage =
|
||||
"Settings saved. For old conversations, you will need to stop and restart the conversation to see the changes.";
|
||||
const duration = calculateToastDuration(longMessage, 5000);
|
||||
expect(duration).toBeGreaterThan(5000);
|
||||
expect(duration).toBeLessThanOrEqual(10000);
|
||||
});
|
||||
|
||||
it("should respect maximum duration cap", () => {
|
||||
const veryLongMessage = "A".repeat(10000); // Very long message
|
||||
const duration = calculateToastDuration(veryLongMessage, 5000, 10000);
|
||||
expect(duration).toBe(10000);
|
||||
});
|
||||
|
||||
it("should use custom minimum and maximum durations", () => {
|
||||
const message = "Test message";
|
||||
const customMin = 3000;
|
||||
const customMax = 8000;
|
||||
const duration = calculateToastDuration(message, customMin, customMax);
|
||||
expect(duration).toBeGreaterThanOrEqual(customMin);
|
||||
expect(duration).toBeLessThanOrEqual(customMax);
|
||||
});
|
||||
|
||||
it("should calculate duration based on reading speed", () => {
|
||||
// Test with a message that should take exactly the calculated time
|
||||
// At 200 WPM (1000 chars/min), 60 chars should take 3.6 seconds
|
||||
// With 1.5x buffer, that's 5.4 seconds
|
||||
const message = "This is a test message that contains exactly sixty chars.";
|
||||
expect(message.length).toBe(57); // Close to 60 chars
|
||||
|
||||
const duration = calculateToastDuration(message, 0, 20000); // No min/max constraints
|
||||
|
||||
// Should be around 5.4 seconds (5400ms) for 57 characters
|
||||
expect(duration).toBeGreaterThan(5000);
|
||||
expect(duration).toBeLessThan(6000);
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
import { CSSProperties } from "react";
|
||||
import toast, { ToastOptions } from "react-hot-toast";
|
||||
import { calculateToastDuration } from "./toast-duration";
|
||||
|
||||
const TOAST_STYLE: CSSProperties = {
|
||||
background: "#454545",
|
||||
@@ -14,9 +15,11 @@ const TOAST_OPTIONS: ToastOptions = {
|
||||
};
|
||||
|
||||
export const displayErrorToast = (error: string) => {
|
||||
toast.error(error, TOAST_OPTIONS);
|
||||
const duration = calculateToastDuration(error, 4000);
|
||||
toast.error(error, { ...TOAST_OPTIONS, duration });
|
||||
};
|
||||
|
||||
export const displaySuccessToast = (message: string) => {
|
||||
toast.success(message, TOAST_OPTIONS);
|
||||
const duration = calculateToastDuration(message, 5000);
|
||||
toast.success(message, { ...TOAST_OPTIONS, duration });
|
||||
};
|
||||
|
||||
70
frontend/src/utils/file-validation.ts
Normal file
70
frontend/src/utils/file-validation.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
export const MAX_FILE_SIZE = 3 * 1024 * 1024; // 3MB maximum file size
|
||||
export const MAX_TOTAL_SIZE = 3 * 1024 * 1024; // 3MB maximum total size for all files combined
|
||||
|
||||
export interface FileValidationResult {
|
||||
isValid: boolean;
|
||||
errorMessage?: string;
|
||||
oversizedFiles?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates individual file sizes
|
||||
*/
|
||||
export function validateIndividualFileSizes(
|
||||
files: File[],
|
||||
): FileValidationResult {
|
||||
const oversizedFiles = files.filter((file) => file.size > MAX_FILE_SIZE);
|
||||
|
||||
if (oversizedFiles.length > 0) {
|
||||
const fileNames = oversizedFiles.map((f) => f.name);
|
||||
return {
|
||||
isValid: false,
|
||||
errorMessage: `Files exceeding 3MB are not allowed: ${fileNames.join(", ")}`,
|
||||
oversizedFiles: fileNames,
|
||||
};
|
||||
}
|
||||
|
||||
return { isValid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates total file size including existing files
|
||||
*/
|
||||
export function validateTotalFileSize(
|
||||
newFiles: File[],
|
||||
existingFiles: File[] = [],
|
||||
): FileValidationResult {
|
||||
const currentTotalSize = existingFiles.reduce(
|
||||
(sum, file) => sum + file.size,
|
||||
0,
|
||||
);
|
||||
const newFilesSize = newFiles.reduce((sum, file) => sum + file.size, 0);
|
||||
const totalSize = currentTotalSize + newFilesSize;
|
||||
|
||||
if (totalSize > MAX_TOTAL_SIZE) {
|
||||
const totalSizeMB = (totalSize / (1024 * 1024)).toFixed(1);
|
||||
return {
|
||||
isValid: false,
|
||||
errorMessage: `Total file size would be ${totalSizeMB}MB, exceeding the 3MB limit. Please select fewer or smaller files.`,
|
||||
};
|
||||
}
|
||||
|
||||
return { isValid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates both individual and total file sizes
|
||||
*/
|
||||
export function validateFiles(
|
||||
newFiles: File[],
|
||||
existingFiles: File[] = [],
|
||||
): FileValidationResult {
|
||||
// First check individual file sizes
|
||||
const individualValidation = validateIndividualFileSizes(newFiles);
|
||||
if (!individualValidation.isValid) {
|
||||
return individualValidation;
|
||||
}
|
||||
|
||||
// Then check total size
|
||||
return validateTotalFileSize(newFiles, existingFiles);
|
||||
}
|
||||
27
frontend/src/utils/toast-duration.ts
Normal file
27
frontend/src/utils/toast-duration.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Calculate toast duration based on message length
|
||||
* @param message - The message to display
|
||||
* @param minDuration - Minimum duration in milliseconds (default: 5000 for success, 4000 for error)
|
||||
* @param maxDuration - Maximum duration in milliseconds (default: 10000)
|
||||
* @returns Duration in milliseconds
|
||||
*/
|
||||
export const calculateToastDuration = (
|
||||
message: string,
|
||||
minDuration: number = 5000,
|
||||
maxDuration: number = 10000,
|
||||
): number => {
|
||||
// Calculate duration based on reading speed (average 200 words per minute)
|
||||
// Assuming average word length of 5 characters
|
||||
const wordsPerMinute = 200;
|
||||
const charactersPerMinute = wordsPerMinute * 5;
|
||||
const charactersPerSecond = charactersPerMinute / 60;
|
||||
|
||||
// Calculate time needed to read the message
|
||||
const readingTimeMs = (message.length / charactersPerSecond) * 1000;
|
||||
|
||||
// Add some buffer time (50% extra) for processing
|
||||
const durationWithBuffer = readingTimeMs * 1.5;
|
||||
|
||||
// Ensure duration is within min/max bounds
|
||||
return Math.min(Math.max(durationWithBuffer, minDuration), maxDuration);
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
import toast from "react-hot-toast";
|
||||
import { calculateToastDuration } from "./toast-duration";
|
||||
|
||||
const idMap = new Map<string, string>();
|
||||
|
||||
@@ -37,7 +38,7 @@ export default {
|
||||
toast(msg, {
|
||||
position: "bottom-right",
|
||||
className: "bg-tertiary",
|
||||
|
||||
duration: calculateToastDuration(msg, 5000),
|
||||
icon: "⚙️",
|
||||
style: {
|
||||
background: "#333",
|
||||
|
||||
1
openhands-ui/.bun-version
Normal file
1
openhands-ui/.bun-version
Normal file
@@ -0,0 +1 @@
|
||||
1.2.16
|
||||
3
openhands-ui/.gitignore
vendored
3
openhands-ui/.gitignore
vendored
@@ -32,3 +32,6 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||
|
||||
# Finder (MacOS) folder config
|
||||
.DS_Store
|
||||
|
||||
*storybook.log
|
||||
storybook-static
|
||||
|
||||
20
openhands-ui/.storybook/main.ts
Normal file
20
openhands-ui/.storybook/main.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import type { StorybookConfig } from "@storybook/react-vite";
|
||||
|
||||
const config: StorybookConfig = {
|
||||
stories: [
|
||||
"../components/**/*.mdx",
|
||||
"../components/**/*.stories.@(js|jsx|mjs|ts|tsx)",
|
||||
],
|
||||
addons: [
|
||||
"@chromatic-com/storybook",
|
||||
"@storybook/addon-docs",
|
||||
"@storybook/addon-onboarding",
|
||||
"@storybook/addon-a11y",
|
||||
"@storybook/addon-vitest",
|
||||
],
|
||||
framework: {
|
||||
name: "@storybook/react-vite",
|
||||
options: {},
|
||||
},
|
||||
};
|
||||
export default config;
|
||||
29
openhands-ui/.storybook/preview.tsx
Normal file
29
openhands-ui/.storybook/preview.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import type { Preview } from "@storybook/react-vite";
|
||||
import "../index.css";
|
||||
|
||||
const preview: Preview = {
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<div className="bg-light-neutral-950 h-full px-4 py-4 rounded-sm min-h-96 min-w-96 flex flex-row items-center justify-center">
|
||||
<Story />
|
||||
</div>
|
||||
),
|
||||
],
|
||||
parameters: {
|
||||
backgrounds: {
|
||||
default: "light",
|
||||
values: [
|
||||
{ name: "light", value: "#ffffff" },
|
||||
{ name: "dark", value: "#1a1a1a" },
|
||||
],
|
||||
},
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/i,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default preview;
|
||||
4
openhands-ui/.storybook/prompt.txt
Normal file
4
openhands-ui/.storybook/prompt.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
TODO WRITE PROMPT WHICH WILL
|
||||
- EXPLAIN AGENT WHERE THE COMPONENTS ARE
|
||||
- EXPLAIN AGENT WHERE TO PUT SB STORIES
|
||||
- EXPLAIN AGENT TO USE FURTHER INSTRUCTIONS FROM components/<name>/prompt.txt (this will contain specific information about component, for example how to construct stories and which prop to use)
|
||||
@@ -6,10 +6,10 @@ To install dependencies:
|
||||
bun install
|
||||
```
|
||||
|
||||
To run:
|
||||
To run storybook:
|
||||
|
||||
```bash
|
||||
bun run index.ts
|
||||
bun run sb
|
||||
```
|
||||
|
||||
This project was created using `bun init` in bun v1.2.16. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.
|
||||
|
||||
@@ -2,24 +2,769 @@
|
||||
"lockfileVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "openhands-ui",
|
||||
"name": "@openhands/ui",
|
||||
"dependencies": {
|
||||
"@floating-ui/react": "^0.27.12",
|
||||
"clsx": "^2.1.1",
|
||||
"react": "^19.1.0",
|
||||
"react-bootstrap-icons": "^1.11.6",
|
||||
"react-dom": "^19.1.0",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tailwind-scrollbar": "^4.0.2",
|
||||
"tailwindcss": "^4.1.10",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@chromatic-com/storybook": "^4.0.0",
|
||||
"@storybook/addon-a11y": "^9.0.12",
|
||||
"@storybook/addon-docs": "^9.0.12",
|
||||
"@storybook/addon-onboarding": "^9.0.12",
|
||||
"@storybook/addon-vitest": "^9.0.12",
|
||||
"@storybook/react-vite": "^9.0.12",
|
||||
"@tailwindcss/vite": "^4.1.10",
|
||||
"@types/bun": "latest",
|
||||
"@vitejs/plugin-react": "^4.5.2",
|
||||
"@vitest/browser": "^3.2.4",
|
||||
"@vitest/coverage-v8": "^3.2.4",
|
||||
"playwright": "^1.53.1",
|
||||
"storybook": "^9.0.12",
|
||||
"vitest": "^3.2.4",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5",
|
||||
"typescript": "^5.8.3",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@types/bun": ["@types/bun@1.2.16", "", { "dependencies": { "bun-types": "1.2.16" } }, "sha512-1aCZJ/6nSiViw339RsaNhkNoEloLaPzZhxMOYEa7OzRzO41IGg5n/7I43/ZIAW/c+Q6cT12Vf7fOZOoVIzb5BQ=="],
|
||||
"@adobe/css-tools": ["@adobe/css-tools@4.4.3", "", {}, "sha512-VQKMkwriZbaOgVCby1UDY/LDk5fIjhQicCvVPFqfe+69fWaPWydbWJ3wRt59/YzIwda1I81loas3oCoHxnqvdA=="],
|
||||
|
||||
"@types/node": ["@types/node@24.0.3", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-R4I/kzCYAdRLzfiCabn9hxWfbuHS573x+r0dJMkkzThEa7pbrcDWK+9zu3e7aBOouf+rQAciqPFMnxwr0aWgKg=="],
|
||||
"@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="],
|
||||
|
||||
"bun-types": ["bun-types@1.2.16", "", { "dependencies": { "@types/node": "*" } }, "sha512-ciXLrHV4PXax9vHvUrkvun9VPVGOVwbbbBF/Ev1cXz12lyEZMoJpIJABOfPcN9gDJRaiKF9MVbSygLg4NXu3/A=="],
|
||||
"@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
|
||||
|
||||
"@babel/compat-data": ["@babel/compat-data@7.28.0", "", {}, "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw=="],
|
||||
|
||||
"@babel/core": ["@babel/core@7.28.0", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.6", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ=="],
|
||||
|
||||
"@babel/generator": ["@babel/generator@7.28.0", "", { "dependencies": { "@babel/parser": "^7.28.0", "@babel/types": "^7.28.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg=="],
|
||||
|
||||
"@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.2", "", { "dependencies": { "@babel/compat-data": "^7.27.2", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ=="],
|
||||
|
||||
"@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
|
||||
|
||||
"@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="],
|
||||
|
||||
"@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.27.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.27.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg=="],
|
||||
|
||||
"@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.27.1", "", {}, "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw=="],
|
||||
|
||||
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
|
||||
|
||||
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="],
|
||||
|
||||
"@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
|
||||
|
||||
"@babel/helpers": ["@babel/helpers@7.27.6", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.27.6" } }, "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug=="],
|
||||
|
||||
"@babel/parser": ["@babel/parser@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.0" }, "bin": "./bin/babel-parser.js" }, "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g=="],
|
||||
|
||||
"@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="],
|
||||
|
||||
"@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="],
|
||||
|
||||
"@babel/runtime": ["@babel/runtime@7.27.6", "", {}, "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q=="],
|
||||
|
||||
"@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="],
|
||||
|
||||
"@babel/traverse": ["@babel/traverse@7.28.0", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/types": "^7.28.0", "debug": "^4.3.1" } }, "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg=="],
|
||||
|
||||
"@babel/types": ["@babel/types@7.28.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-jYnje+JyZG5YThjHiF28oT4SIZLnYOcSBb6+SDaFIyzDVSkXQmQQYclJ2R+YxcdmK0AX6x1E5OQNtuh3jHDrUg=="],
|
||||
|
||||
"@bcoe/v8-coverage": ["@bcoe/v8-coverage@1.0.2", "", {}, "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA=="],
|
||||
|
||||
"@chromatic-com/storybook": ["@chromatic-com/storybook@4.0.1", "", { "dependencies": { "@neoconfetti/react": "^1.0.0", "chromatic": "^12.0.0", "filesize": "^10.0.12", "jsonfile": "^6.1.0", "strip-ansi": "^7.1.0" }, "peerDependencies": { "storybook": "^0.0.0-0 || ^9.0.0 || ^9.1.0-0" } }, "sha512-GQXe5lyZl3yLewLJQyFXEpOp2h+mfN2bPrzYaOFNCJjO4Js9deKbRHTOSaiP2FRwZqDLdQwy2+SEGeXPZ94yYw=="],
|
||||
|
||||
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA=="],
|
||||
|
||||
"@esbuild/android-arm": ["@esbuild/android-arm@0.25.5", "", { "os": "android", "cpu": "arm" }, "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA=="],
|
||||
|
||||
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.5", "", { "os": "android", "cpu": "arm64" }, "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg=="],
|
||||
|
||||
"@esbuild/android-x64": ["@esbuild/android-x64@0.25.5", "", { "os": "android", "cpu": "x64" }, "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw=="],
|
||||
|
||||
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ=="],
|
||||
|
||||
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ=="],
|
||||
|
||||
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw=="],
|
||||
|
||||
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw=="],
|
||||
|
||||
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.5", "", { "os": "linux", "cpu": "arm" }, "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw=="],
|
||||
|
||||
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg=="],
|
||||
|
||||
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA=="],
|
||||
|
||||
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.5", "", { "os": "linux", "cpu": "none" }, "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg=="],
|
||||
|
||||
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.5", "", { "os": "linux", "cpu": "none" }, "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg=="],
|
||||
|
||||
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ=="],
|
||||
|
||||
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.5", "", { "os": "linux", "cpu": "none" }, "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA=="],
|
||||
|
||||
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ=="],
|
||||
|
||||
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.5", "", { "os": "linux", "cpu": "x64" }, "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw=="],
|
||||
|
||||
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.5", "", { "os": "none", "cpu": "arm64" }, "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw=="],
|
||||
|
||||
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.5", "", { "os": "none", "cpu": "x64" }, "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ=="],
|
||||
|
||||
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.5", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw=="],
|
||||
|
||||
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg=="],
|
||||
|
||||
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA=="],
|
||||
|
||||
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw=="],
|
||||
|
||||
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ=="],
|
||||
|
||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.5", "", { "os": "win32", "cpu": "x64" }, "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g=="],
|
||||
|
||||
"@floating-ui/core": ["@floating-ui/core@1.7.2", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-wNB5ooIKHQc+Kui96jE/n69rHFWAVoxn5CAzL1Xdd8FG03cgY3MLO+GF9U3W737fYDSgPWA6MReKhBQBop6Pcw=="],
|
||||
|
||||
"@floating-ui/dom": ["@floating-ui/dom@1.7.2", "", { "dependencies": { "@floating-ui/core": "^1.7.2", "@floating-ui/utils": "^0.2.10" } }, "sha512-7cfaOQuCS27HD7DX+6ib2OrnW+b4ZBwDNnCcT0uTyidcmyWb03FnQqJybDBoCnpdxwBSfA94UAYlRCt7mV+TbA=="],
|
||||
|
||||
"@floating-ui/react": ["@floating-ui/react@0.27.13", "", { "dependencies": { "@floating-ui/react-dom": "^2.1.4", "@floating-ui/utils": "^0.2.10", "tabbable": "^6.0.0" }, "peerDependencies": { "react": ">=17.0.0", "react-dom": ">=17.0.0" } }, "sha512-Qmj6t9TjgWAvbygNEu1hj4dbHI9CY0ziCMIJrmYoDIn9TUAH5lRmiIeZmRd4c6QEZkzdoH7jNnoNyoY1AIESiA=="],
|
||||
|
||||
"@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.4", "", { "dependencies": { "@floating-ui/dom": "^1.7.2" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-JbbpPhp38UmXDDAu60RJmbeme37Jbgsm7NrHGgzYYFKmblzRUh6Pa641dII6LsjwF4XlScDrde2UAzDo/b9KPw=="],
|
||||
|
||||
"@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="],
|
||||
|
||||
"@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="],
|
||||
|
||||
"@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="],
|
||||
|
||||
"@istanbuljs/schema": ["@istanbuljs/schema@0.1.3", "", {}, "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA=="],
|
||||
|
||||
"@joshwooding/vite-plugin-react-docgen-typescript": ["@joshwooding/vite-plugin-react-docgen-typescript@0.6.1", "", { "dependencies": { "glob": "^10.0.0", "magic-string": "^0.30.0", "react-docgen-typescript": "^2.2.2" }, "peerDependencies": { "typescript": ">= 4.3.x", "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" }, "optionalPeers": ["typescript"] }, "sha512-J4BaTocTOYFkMHIra1JDWrMWpNmBl4EkplIwHEsV8aeUOtdWjwSnln9U7twjMFTAEB7mptNtSKyVi1Y2W9sDJw=="],
|
||||
|
||||
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.12", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg=="],
|
||||
|
||||
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
|
||||
|
||||
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.4", "", {}, "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw=="],
|
||||
|
||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.29", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ=="],
|
||||
|
||||
"@mdx-js/react": ["@mdx-js/react@3.1.0", "", { "dependencies": { "@types/mdx": "^2.0.0" }, "peerDependencies": { "@types/react": ">=16", "react": ">=16" } }, "sha512-QjHtSaoameoalGnKDT3FoIl4+9RwyTmo9ZJGBdLOks/YOiWHoRDI3PUwEzOE7kEmGcV3AFcp9K6dYu9rEuKLAQ=="],
|
||||
|
||||
"@neoconfetti/react": ["@neoconfetti/react@1.0.0", "", {}, "sha512-klcSooChXXOzIm+SE5IISIAn3bYzYfPjbX7D7HoqZL84oAfgREeSg5vSIaSFH+DaGzzvImTyWe1OyrJ67vik4A=="],
|
||||
|
||||
"@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
|
||||
|
||||
"@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="],
|
||||
|
||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.19", "", {}, "sha512-3FL3mnMbPu0muGOCaKAhhFEYmqv9eTfPSJRJmANrCwtgK8VuxpsZDGK+m0LYAGoyO8+0j5uRe4PeyPDK1yA/hA=="],
|
||||
|
||||
"@rollup/pluginutils": ["@rollup/pluginutils@5.2.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-qWJ2ZTbmumwiLFomfzTyt5Kng4hwPi9rwCYN4SHb6eaRU1KNO4ccxINHr/VhH4GgPlt1XfSTLX2LBTme8ne4Zw=="],
|
||||
|
||||
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.44.1", "", { "os": "android", "cpu": "arm" }, "sha512-JAcBr1+fgqx20m7Fwe1DxPUl/hPkee6jA6Pl7n1v2EFiktAHenTaXl5aIFjUIEsfn9w3HE4gK1lEgNGMzBDs1w=="],
|
||||
|
||||
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.44.1", "", { "os": "android", "cpu": "arm64" }, "sha512-RurZetXqTu4p+G0ChbnkwBuAtwAbIwJkycw1n6GvlGlBuS4u5qlr5opix8cBAYFJgaY05TWtM+LaoFggUmbZEQ=="],
|
||||
|
||||
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.44.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-fM/xPesi7g2M7chk37LOnmnSTHLG/v2ggWqKj3CCA1rMA4mm5KVBT1fNoswbo1JhPuNNZrVwpTvlCVggv8A2zg=="],
|
||||
|
||||
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.44.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-gDnWk57urJrkrHQ2WVx9TSVTH7lSlU7E3AFqiko+bgjlh78aJ88/3nycMax52VIVjIm3ObXnDL2H00e/xzoipw=="],
|
||||
|
||||
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.44.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-wnFQmJ/zPThM5zEGcnDcCJeYJgtSLjh1d//WuHzhf6zT3Md1BvvhJnWoy+HECKu2bMxaIcfWiu3bJgx6z4g2XA=="],
|
||||
|
||||
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.44.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-uBmIxoJ4493YATvU2c0upGz87f99e3wop7TJgOA/bXMFd2SvKCI7xkxY/5k50bv7J6dw1SXT4MQBQSLn8Bb/Uw=="],
|
||||
|
||||
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.44.1", "", { "os": "linux", "cpu": "arm" }, "sha512-n0edDmSHlXFhrlmTK7XBuwKlG5MbS7yleS1cQ9nn4kIeW+dJH+ExqNgQ0RrFRew8Y+0V/x6C5IjsHrJmiHtkxQ=="],
|
||||
|
||||
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.44.1", "", { "os": "linux", "cpu": "arm" }, "sha512-8WVUPy3FtAsKSpyk21kV52HCxB+me6YkbkFHATzC2Yd3yuqHwy2lbFL4alJOLXKljoRw08Zk8/xEj89cLQ/4Nw=="],
|
||||
|
||||
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.44.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-yuktAOaeOgorWDeFJggjuCkMGeITfqvPgkIXhDqsfKX8J3jGyxdDZgBV/2kj/2DyPaLiX6bPdjJDTu9RB8lUPQ=="],
|
||||
|
||||
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.44.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-W+GBM4ifET1Plw8pdVaecwUgxmiH23CfAUj32u8knq0JPFyK4weRy6H7ooxYFD19YxBulL0Ktsflg5XS7+7u9g=="],
|
||||
|
||||
"@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.44.1", "", { "os": "linux", "cpu": "none" }, "sha512-1zqnUEMWp9WrGVuVak6jWTl4fEtrVKfZY7CvcBmUUpxAJ7WcSowPSAWIKa/0o5mBL/Ij50SIf9tuirGx63Ovew=="],
|
||||
|
||||
"@rollup/rollup-linux-powerpc64le-gnu": ["@rollup/rollup-linux-powerpc64le-gnu@4.44.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Rl3JKaRu0LHIx7ExBAAnf0JcOQetQffaw34T8vLlg9b1IhzcBgaIdnvEbbsZq9uZp3uAH+JkHd20Nwn0h9zPjA=="],
|
||||
|
||||
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.44.1", "", { "os": "linux", "cpu": "none" }, "sha512-j5akelU3snyL6K3N/iX7otLBIl347fGwmd95U5gS/7z6T4ftK288jKq3A5lcFKcx7wwzb5rgNvAg3ZbV4BqUSw=="],
|
||||
|
||||
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.44.1", "", { "os": "linux", "cpu": "none" }, "sha512-ppn5llVGgrZw7yxbIm8TTvtj1EoPgYUAbfw0uDjIOzzoqlZlZrLJ/KuiE7uf5EpTpCTrNt1EdtzF0naMm0wGYg=="],
|
||||
|
||||
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.44.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-Hu6hEdix0oxtUma99jSP7xbvjkUM/ycke/AQQ4EC5g7jNRLLIwjcNwaUy95ZKBJJwg1ZowsclNnjYqzN4zwkAw=="],
|
||||
|
||||
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.44.1", "", { "os": "linux", "cpu": "x64" }, "sha512-EtnsrmZGomz9WxK1bR5079zee3+7a+AdFlghyd6VbAjgRJDbTANJ9dcPIPAi76uG05micpEL+gPGmAKYTschQw=="],
|
||||
|
||||
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.44.1", "", { "os": "linux", "cpu": "x64" }, "sha512-iAS4p+J1az6Usn0f8xhgL4PaU878KEtutP4hqw52I4IO6AGoyOkHCxcc4bqufv1tQLdDWFx8lR9YlwxKuv3/3g=="],
|
||||
|
||||
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.44.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-NtSJVKcXwcqozOl+FwI41OH3OApDyLk3kqTJgx8+gp6On9ZEt5mYhIsKNPGuaZr3p9T6NWPKGU/03Vw4CNU9qg=="],
|
||||
|
||||
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.44.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-JYA3qvCOLXSsnTR3oiyGws1Dm0YTuxAAeaYGVlGpUsHqloPcFjPg+X0Fj2qODGLNwQOAcCiQmHub/V007kiH5A=="],
|
||||
|
||||
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.44.1", "", { "os": "win32", "cpu": "x64" }, "sha512-J8o22LuF0kTe7m+8PvW9wk3/bRq5+mRo5Dqo6+vXb7otCm3TPhYOJqOaQtGU9YMWQSL3krMnoOxMr0+9E6F3Ug=="],
|
||||
|
||||
"@storybook/addon-a11y": ["@storybook/addon-a11y@9.0.15", "", { "dependencies": { "@storybook/global": "^5.0.0", "axe-core": "^4.2.0" }, "peerDependencies": { "storybook": "^9.0.15" } }, "sha512-/oborGUeN7KT6jyTMhGRET9tXvZ080OCB/Hw6txSfsVxgZ4Z1QTJcOreejHGeYyxHN1ugEJ26K95agk4M13WZg=="],
|
||||
|
||||
"@storybook/addon-docs": ["@storybook/addon-docs@9.0.15", "", { "dependencies": { "@mdx-js/react": "^3.0.0", "@storybook/csf-plugin": "9.0.15", "@storybook/icons": "^1.2.12", "@storybook/react-dom-shim": "9.0.15", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "ts-dedent": "^2.0.0" }, "peerDependencies": { "storybook": "^9.0.15" } }, "sha512-HOb45DkF23T1tRzakb9q33qnBRso15S/GM28ippPZWi5ZXR9RAyKVgOSMA/ViEpK4ezASxN+Tee+H7m4ksEFZw=="],
|
||||
|
||||
"@storybook/addon-onboarding": ["@storybook/addon-onboarding@9.0.15", "", { "peerDependencies": { "storybook": "^9.0.15" } }, "sha512-g2FqO0aS6vvjMZdY+0xjV1C7YGcDE0GkuPAv1JqejNYGyX2Z8nuLHy2zqhLIBpfoap4S9PZO+obqEKVeo70Q0Q=="],
|
||||
|
||||
"@storybook/addon-vitest": ["@storybook/addon-vitest@9.0.15", "", { "dependencies": { "@storybook/global": "^5.0.0", "@storybook/icons": "^1.4.0", "prompts": "^2.4.0", "ts-dedent": "^2.2.0" }, "peerDependencies": { "@vitest/browser": "^3.0.0", "@vitest/runner": "^3.0.0", "storybook": "^9.0.15", "vitest": "^3.0.0" }, "optionalPeers": ["@vitest/browser", "@vitest/runner", "vitest"] }, "sha512-4TynzdZgJMsvneT5lZGp+WrUoFtp8+LRL3y35EepJa3GMBc+9WgsKQrso+xnDQh1gLvVNe46n3klZvunVr4AFA=="],
|
||||
|
||||
"@storybook/builder-vite": ["@storybook/builder-vite@9.0.15", "", { "dependencies": { "@storybook/csf-plugin": "9.0.15", "ts-dedent": "^2.0.0" }, "peerDependencies": { "storybook": "^9.0.15", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-ogPec1V+e3MgTY5DBlq/6hBBui0Y4TmolYQh0eL3cATHrwZlwkTTDWQfsOnMALd5w+4Jq8n0gk0cQgR5rh1FHw=="],
|
||||
|
||||
"@storybook/csf-plugin": ["@storybook/csf-plugin@9.0.15", "", { "dependencies": { "unplugin": "^1.3.1" }, "peerDependencies": { "storybook": "^9.0.15" } }, "sha512-KszyGjrocMiNbkmpBGARF1ugLYMVaw1J8Z31kmwTHsMgMZwAKcOsofJ0fPgFno0yV59DUVkWxVBdPs9V0hhvxA=="],
|
||||
|
||||
"@storybook/global": ["@storybook/global@5.0.0", "", {}, "sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ=="],
|
||||
|
||||
"@storybook/icons": ["@storybook/icons@1.4.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta" } }, "sha512-Td73IeJxOyalzvjQL+JXx72jlIYHgs+REaHiREOqfpo3A2AYYG71AUbcv+lg7mEDIweKVCxsMQ0UKo634c8XeA=="],
|
||||
|
||||
"@storybook/react": ["@storybook/react@9.0.15", "", { "dependencies": { "@storybook/global": "^5.0.0", "@storybook/react-dom-shim": "9.0.15" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", "storybook": "^9.0.15", "typescript": ">= 4.9.x" }, "optionalPeers": ["typescript"] }, "sha512-hewpSH8Ij4Bg7S9Tfw7ecfGPv7YDycRxsfpsDX7Mw3JhLuCdqjpmmTL2RgoNojg7TAW3FPdixcgQi/b4PH50ag=="],
|
||||
|
||||
"@storybook/react-dom-shim": ["@storybook/react-dom-shim@9.0.15", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", "storybook": "^9.0.15" } }, "sha512-X5VlYKoZSIMU9HEshIwtNzp41nPt4kiJtJ2c5HzFa5F6M8rEHM5n059CGcCZQqff3FnZtK/y6v/kCVZO+8oETA=="],
|
||||
|
||||
"@storybook/react-vite": ["@storybook/react-vite@9.0.15", "", { "dependencies": { "@joshwooding/vite-plugin-react-docgen-typescript": "0.6.1", "@rollup/pluginutils": "^5.0.2", "@storybook/builder-vite": "9.0.15", "@storybook/react": "9.0.15", "find-up": "^7.0.0", "magic-string": "^0.30.0", "react-docgen": "^8.0.0", "resolve": "^1.22.8", "tsconfig-paths": "^4.2.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", "storybook": "^9.0.15", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-OOAywn5x2Ged3LD84+TMwpjZUelFg7Wb8eHkgHE2SzM20XiZrhoKvreqxlzbfey3weBl+bKNhsiWF9BluT8YHg=="],
|
||||
|
||||
"@tailwindcss/node": ["@tailwindcss/node@4.1.11", "", { "dependencies": { "@ampproject/remapping": "^2.3.0", "enhanced-resolve": "^5.18.1", "jiti": "^2.4.2", "lightningcss": "1.30.1", "magic-string": "^0.30.17", "source-map-js": "^1.2.1", "tailwindcss": "4.1.11" } }, "sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q=="],
|
||||
|
||||
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.11", "", { "dependencies": { "detect-libc": "^2.0.4", "tar": "^7.4.3" }, "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.11", "@tailwindcss/oxide-darwin-arm64": "4.1.11", "@tailwindcss/oxide-darwin-x64": "4.1.11", "@tailwindcss/oxide-freebsd-x64": "4.1.11", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.11", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.11", "@tailwindcss/oxide-linux-arm64-musl": "4.1.11", "@tailwindcss/oxide-linux-x64-gnu": "4.1.11", "@tailwindcss/oxide-linux-x64-musl": "4.1.11", "@tailwindcss/oxide-wasm32-wasi": "4.1.11", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.11", "@tailwindcss/oxide-win32-x64-msvc": "4.1.11" } }, "sha512-Q69XzrtAhuyfHo+5/HMgr1lAiPP/G40OMFAnws7xcFEYqcypZmdW8eGXaOUIeOl1dzPJBPENXgbjsOyhg2nkrg=="],
|
||||
|
||||
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.11", "", { "os": "android", "cpu": "arm64" }, "sha512-3IfFuATVRUMZZprEIx9OGDjG3Ou3jG4xQzNTvjDoKmU9JdmoCohQJ83MYd0GPnQIu89YoJqvMM0G3uqLRFtetg=="],
|
||||
|
||||
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ESgStEOEsyg8J5YcMb1xl8WFOXfeBmrhAwGsFxxB2CxY9evy63+AtpbDLAyRkJnxLy2WsD1qF13E97uQyP1lfQ=="],
|
||||
|
||||
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-EgnK8kRchgmgzG6jE10UQNaH9Mwi2n+yw1jWmof9Vyg2lpKNX2ioe7CJdf9M5f8V9uaQxInenZkOxnTVL3fhAw=="],
|
||||
|
||||
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.11", "", { "os": "freebsd", "cpu": "x64" }, "sha512-xdqKtbpHs7pQhIKmqVpxStnY1skuNh4CtbcyOHeX1YBE0hArj2romsFGb6yUmzkq/6M24nkxDqU8GYrKrz+UcA=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.11", "", { "os": "linux", "cpu": "arm" }, "sha512-ryHQK2eyDYYMwB5wZL46uoxz2zzDZsFBwfjssgB7pzytAeCCa6glsiJGjhTEddq/4OsIjsLNMAiMlHNYnkEEeg=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-mYwqheq4BXF83j/w75ewkPJmPZIqqP1nhoghS9D57CLjsh3Nfq0m4ftTotRYtGnZd3eCztgbSPJ9QhfC91gDZQ=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-m/NVRFNGlEHJrNVk3O6I9ggVuNjXHIPoD6bqay/pubtYC9QIdAMpS+cswZQPBLvVvEF6GtSNONbDkZrjWZXYNQ=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.11", "", { "os": "linux", "cpu": "x64" }, "sha512-YW6sblI7xukSD2TdbbaeQVDysIm/UPJtObHJHKxDEcW2exAtY47j52f8jZXkqE1krdnkhCMGqP3dbniu1Te2Fg=="],
|
||||
|
||||
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.11", "", { "os": "linux", "cpu": "x64" }, "sha512-e3C/RRhGunWYNC3aSF7exsQkdXzQ/M+aYuZHKnw4U7KQwTJotnWsGOIVih0s2qQzmEzOFIJ3+xt7iq67K/p56Q=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.11", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@emnapi/wasi-threads": "^1.0.2", "@napi-rs/wasm-runtime": "^0.2.11", "@tybys/wasm-util": "^0.9.0", "tslib": "^2.8.0" }, "cpu": "none" }, "sha512-Xo1+/GU0JEN/C/dvcammKHzeM6NqKovG+6921MR6oadee5XPBaKOumrJCXvopJ/Qb5TH7LX/UAywbqrP4lax0g=="],
|
||||
|
||||
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.11", "", { "os": "win32", "cpu": "arm64" }, "sha512-UgKYx5PwEKrac3GPNPf6HVMNhUIGuUh4wlDFR2jYYdkX6pL/rn73zTq/4pzUm8fOjAn5L8zDeHp9iXmUGOXZ+w=="],
|
||||
|
||||
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.11", "", { "os": "win32", "cpu": "x64" }, "sha512-YfHoggn1j0LK7wR82TOucWc5LDCguHnoS879idHekmmiR7g9HUtMw9MI0NHatS28u/Xlkfi9w5RJWgz2Dl+5Qg=="],
|
||||
|
||||
"@tailwindcss/vite": ["@tailwindcss/vite@4.1.11", "", { "dependencies": { "@tailwindcss/node": "4.1.11", "@tailwindcss/oxide": "4.1.11", "tailwindcss": "4.1.11" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7" } }, "sha512-RHYhrR3hku0MJFRV+fN2gNbDNEh3dwKvY8XJvTxCSXeMOsCRSr+uKvDWQcbizrHgjML6ZmTE5OwMrl5wKcujCw=="],
|
||||
|
||||
"@testing-library/dom": ["@testing-library/dom@10.4.0", "", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", "chalk": "^4.1.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "pretty-format": "^27.0.2" } }, "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ=="],
|
||||
|
||||
"@testing-library/jest-dom": ["@testing-library/jest-dom@6.6.3", "", { "dependencies": { "@adobe/css-tools": "^4.4.0", "aria-query": "^5.0.0", "chalk": "^3.0.0", "css.escape": "^1.5.1", "dom-accessibility-api": "^0.6.3", "lodash": "^4.17.21", "redent": "^3.0.0" } }, "sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA=="],
|
||||
|
||||
"@testing-library/user-event": ["@testing-library/user-event@14.6.1", "", { "peerDependencies": { "@testing-library/dom": ">=7.21.4" } }, "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw=="],
|
||||
|
||||
"@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="],
|
||||
|
||||
"@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
|
||||
|
||||
"@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="],
|
||||
|
||||
"@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="],
|
||||
|
||||
"@types/babel__traverse": ["@types/babel__traverse@7.20.7", "", { "dependencies": { "@babel/types": "^7.20.7" } }, "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.2.17", "", { "dependencies": { "bun-types": "1.2.17" } }, "sha512-l/BYs/JYt+cXA/0+wUhulYJB6a6p//GTPiJ7nV+QHa8iiId4HZmnu/3J/SowP5g0rTiERY2kfGKXEK5Ehltx4Q=="],
|
||||
|
||||
"@types/chai": ["@types/chai@5.2.2", "", { "dependencies": { "@types/deep-eql": "*" } }, "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg=="],
|
||||
|
||||
"@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="],
|
||||
|
||||
"@types/doctrine": ["@types/doctrine@0.0.9", "", {}, "sha512-eOIHzCUSH7SMfonMG1LsC2f8vxBFtho6NGBznK41R84YzPuvSBzrhEps33IsQiOW9+VL6NQ9DbjQJznk/S4uRA=="],
|
||||
|
||||
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
||||
|
||||
"@types/mdx": ["@types/mdx@2.0.13", "", {}, "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw=="],
|
||||
|
||||
"@types/node": ["@types/node@24.0.10", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA=="],
|
||||
|
||||
"@types/prismjs": ["@types/prismjs@1.26.5", "", {}, "sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ=="],
|
||||
|
||||
"@types/react": ["@types/react@19.1.8", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g=="],
|
||||
|
||||
"@types/resolve": ["@types/resolve@1.20.6", "", {}, "sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ=="],
|
||||
|
||||
"@vitejs/plugin-react": ["@vitejs/plugin-react@4.6.0", "", { "dependencies": { "@babel/core": "^7.27.4", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.19", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" } }, "sha512-5Kgff+m8e2PB+9j51eGHEpn5kUzRKH2Ry0qGoe8ItJg7pqnkPrYPkDQZGgGmTa0EGarHrkjLvOdU3b1fzI8otQ=="],
|
||||
|
||||
"@vitest/browser": ["@vitest/browser@3.2.4", "", { "dependencies": { "@testing-library/dom": "^10.4.0", "@testing-library/user-event": "^14.6.1", "@vitest/mocker": "3.2.4", "@vitest/utils": "3.2.4", "magic-string": "^0.30.17", "sirv": "^3.0.1", "tinyrainbow": "^2.0.0", "ws": "^8.18.2" }, "peerDependencies": { "playwright": "*", "vitest": "3.2.4", "webdriverio": "^7.0.0 || ^8.0.0 || ^9.0.0" }, "optionalPeers": ["playwright", "webdriverio"] }, "sha512-tJxiPrWmzH8a+w9nLKlQMzAKX/7VjFs50MWgcAj7p9XQ7AQ9/35fByFYptgPELyLw+0aixTnC4pUWV+APcZ/kw=="],
|
||||
|
||||
"@vitest/coverage-v8": ["@vitest/coverage-v8@3.2.4", "", { "dependencies": { "@ampproject/remapping": "^2.3.0", "@bcoe/v8-coverage": "^1.0.2", "ast-v8-to-istanbul": "^0.3.3", "debug": "^4.4.1", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-lib-source-maps": "^5.0.6", "istanbul-reports": "^3.1.7", "magic-string": "^0.30.17", "magicast": "^0.3.5", "std-env": "^3.9.0", "test-exclude": "^7.0.1", "tinyrainbow": "^2.0.0" }, "peerDependencies": { "@vitest/browser": "3.2.4", "vitest": "3.2.4" }, "optionalPeers": ["@vitest/browser"] }, "sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ=="],
|
||||
|
||||
"@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="],
|
||||
|
||||
"@vitest/mocker": ["@vitest/mocker@3.2.4", "", { "dependencies": { "@vitest/spy": "3.2.4", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ=="],
|
||||
|
||||
"@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="],
|
||||
|
||||
"@vitest/runner": ["@vitest/runner@3.2.4", "", { "dependencies": { "@vitest/utils": "3.2.4", "pathe": "^2.0.3", "strip-literal": "^3.0.0" } }, "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ=="],
|
||||
|
||||
"@vitest/snapshot": ["@vitest/snapshot@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "magic-string": "^0.30.17", "pathe": "^2.0.3" } }, "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ=="],
|
||||
|
||||
"@vitest/spy": ["@vitest/spy@3.2.4", "", { "dependencies": { "tinyspy": "^4.0.3" } }, "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw=="],
|
||||
|
||||
"@vitest/utils": ["@vitest/utils@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="],
|
||||
|
||||
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
|
||||
|
||||
"ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
|
||||
|
||||
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||
|
||||
"aria-query": ["aria-query@5.3.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A=="],
|
||||
|
||||
"assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="],
|
||||
|
||||
"ast-types": ["ast-types@0.16.1", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg=="],
|
||||
|
||||
"ast-v8-to-istanbul": ["ast-v8-to-istanbul@0.3.3", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "estree-walker": "^3.0.3", "js-tokens": "^9.0.1" } }, "sha512-MuXMrSLVVoA6sYN/6Hke18vMzrT4TZNbZIj/hvh0fnYFpO+/kFXcLIaiPwXXWaQUPg4yJD8fj+lfJ7/1EBconw=="],
|
||||
|
||||
"axe-core": ["axe-core@4.10.3", "", {}, "sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg=="],
|
||||
|
||||
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||
|
||||
"better-opn": ["better-opn@3.0.2", "", { "dependencies": { "open": "^8.0.4" } }, "sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ=="],
|
||||
|
||||
"brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
||||
|
||||
"browserslist": ["browserslist@4.25.1", "", { "dependencies": { "caniuse-lite": "^1.0.30001726", "electron-to-chromium": "^1.5.173", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw=="],
|
||||
|
||||
"bun-types": ["bun-types@1.2.17", "", { "dependencies": { "@types/node": "*" } }, "sha512-ElC7ItwT3SCQwYZDYoAH+q6KT4Fxjl8DtZ6qDulUFBmXA8YB4xo+l54J9ZJN+k2pphfn9vk7kfubeSd5QfTVJQ=="],
|
||||
|
||||
"cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="],
|
||||
|
||||
"caniuse-lite": ["caniuse-lite@1.0.30001726", "", {}, "sha512-VQAUIUzBiZ/UnlM28fSp2CRF3ivUn1BWEvxMcVTNwpw91Py1pGbPIyIKtd+tzct9C3ouceCVdGAXxZOpZAsgdw=="],
|
||||
|
||||
"chai": ["chai@5.2.0", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw=="],
|
||||
|
||||
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
||||
|
||||
"check-error": ["check-error@2.1.1", "", {}, "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw=="],
|
||||
|
||||
"chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="],
|
||||
|
||||
"chromatic": ["chromatic@12.2.0", "", { "peerDependencies": { "@chromatic-com/cypress": "^0.*.* || ^1.0.0", "@chromatic-com/playwright": "^0.*.* || ^1.0.0" }, "optionalPeers": ["@chromatic-com/cypress", "@chromatic-com/playwright"], "bin": { "chroma": "dist/bin.js", "chromatic": "dist/bin.js", "chromatic-cli": "dist/bin.js" } }, "sha512-GswmBW9ZptAoTns1BMyjbm55Z7EsIJnUvYKdQqXIBZIKbGErmpA+p4c0BYA+nzw5B0M+rb3Iqp1IaH8TFwIQew=="],
|
||||
|
||||
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
||||
|
||||
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
||||
|
||||
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
||||
|
||||
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
|
||||
|
||||
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||
|
||||
"css.escape": ["css.escape@1.5.1", "", {}, "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg=="],
|
||||
|
||||
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
||||
|
||||
"debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
|
||||
|
||||
"deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="],
|
||||
|
||||
"define-lazy-prop": ["define-lazy-prop@2.0.0", "", {}, "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og=="],
|
||||
|
||||
"dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
|
||||
|
||||
"detect-libc": ["detect-libc@2.0.4", "", {}, "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="],
|
||||
|
||||
"doctrine": ["doctrine@3.0.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w=="],
|
||||
|
||||
"dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="],
|
||||
|
||||
"eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="],
|
||||
|
||||
"electron-to-chromium": ["electron-to-chromium@1.5.179", "", {}, "sha512-UWKi/EbBopgfFsc5k61wFpV7WrnnSlSzW/e2XcBmS6qKYTivZlLtoll5/rdqRTxGglGHkmkW0j0pFNJG10EUIQ=="],
|
||||
|
||||
"emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
|
||||
|
||||
"enhanced-resolve": ["enhanced-resolve@5.18.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ=="],
|
||||
|
||||
"es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="],
|
||||
|
||||
"esbuild": ["esbuild@0.25.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.5", "@esbuild/android-arm": "0.25.5", "@esbuild/android-arm64": "0.25.5", "@esbuild/android-x64": "0.25.5", "@esbuild/darwin-arm64": "0.25.5", "@esbuild/darwin-x64": "0.25.5", "@esbuild/freebsd-arm64": "0.25.5", "@esbuild/freebsd-x64": "0.25.5", "@esbuild/linux-arm": "0.25.5", "@esbuild/linux-arm64": "0.25.5", "@esbuild/linux-ia32": "0.25.5", "@esbuild/linux-loong64": "0.25.5", "@esbuild/linux-mips64el": "0.25.5", "@esbuild/linux-ppc64": "0.25.5", "@esbuild/linux-riscv64": "0.25.5", "@esbuild/linux-s390x": "0.25.5", "@esbuild/linux-x64": "0.25.5", "@esbuild/netbsd-arm64": "0.25.5", "@esbuild/netbsd-x64": "0.25.5", "@esbuild/openbsd-arm64": "0.25.5", "@esbuild/openbsd-x64": "0.25.5", "@esbuild/sunos-x64": "0.25.5", "@esbuild/win32-arm64": "0.25.5", "@esbuild/win32-ia32": "0.25.5", "@esbuild/win32-x64": "0.25.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ=="],
|
||||
|
||||
"esbuild-register": ["esbuild-register@3.6.0", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "esbuild": ">=0.12 <1" } }, "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg=="],
|
||||
|
||||
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
||||
|
||||
"esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="],
|
||||
|
||||
"estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
|
||||
|
||||
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
|
||||
|
||||
"expect-type": ["expect-type@1.2.1", "", {}, "sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw=="],
|
||||
|
||||
"fdir": ["fdir@6.4.6", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w=="],
|
||||
|
||||
"filesize": ["filesize@10.1.6", "", {}, "sha512-sJslQKU2uM33qH5nqewAwVB2QgR6w1aMNsYUp3aN5rMRyXEwJGmZvaWzeJFNTOXWlHQyBFCWrdj3fV/fsTOX8w=="],
|
||||
|
||||
"find-up": ["find-up@7.0.0", "", { "dependencies": { "locate-path": "^7.2.0", "path-exists": "^5.0.0", "unicorn-magic": "^0.1.0" } }, "sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g=="],
|
||||
|
||||
"foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="],
|
||||
|
||||
"fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="],
|
||||
|
||||
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||
|
||||
"gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
|
||||
|
||||
"glob": ["glob@10.4.5", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg=="],
|
||||
|
||||
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||
|
||||
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
|
||||
|
||||
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
||||
|
||||
"html-escaper": ["html-escaper@2.0.2", "", {}, "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg=="],
|
||||
|
||||
"indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="],
|
||||
|
||||
"is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="],
|
||||
|
||||
"is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="],
|
||||
|
||||
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
|
||||
|
||||
"is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="],
|
||||
|
||||
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||
|
||||
"istanbul-lib-coverage": ["istanbul-lib-coverage@3.2.2", "", {}, "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg=="],
|
||||
|
||||
"istanbul-lib-report": ["istanbul-lib-report@3.0.1", "", { "dependencies": { "istanbul-lib-coverage": "^3.0.0", "make-dir": "^4.0.0", "supports-color": "^7.1.0" } }, "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw=="],
|
||||
|
||||
"istanbul-lib-source-maps": ["istanbul-lib-source-maps@5.0.6", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.23", "debug": "^4.1.1", "istanbul-lib-coverage": "^3.0.0" } }, "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A=="],
|
||||
|
||||
"istanbul-reports": ["istanbul-reports@3.1.7", "", { "dependencies": { "html-escaper": "^2.0.0", "istanbul-lib-report": "^3.0.0" } }, "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g=="],
|
||||
|
||||
"jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
|
||||
|
||||
"jiti": ["jiti@2.4.2", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A=="],
|
||||
|
||||
"js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="],
|
||||
|
||||
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
|
||||
|
||||
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
|
||||
|
||||
"jsonfile": ["jsonfile@6.1.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ=="],
|
||||
|
||||
"kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="],
|
||||
|
||||
"lightningcss": ["lightningcss@1.30.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.30.1", "lightningcss-darwin-x64": "1.30.1", "lightningcss-freebsd-x64": "1.30.1", "lightningcss-linux-arm-gnueabihf": "1.30.1", "lightningcss-linux-arm64-gnu": "1.30.1", "lightningcss-linux-arm64-musl": "1.30.1", "lightningcss-linux-x64-gnu": "1.30.1", "lightningcss-linux-x64-musl": "1.30.1", "lightningcss-win32-arm64-msvc": "1.30.1", "lightningcss-win32-x64-msvc": "1.30.1" } }, "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg=="],
|
||||
|
||||
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ=="],
|
||||
|
||||
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA=="],
|
||||
|
||||
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig=="],
|
||||
|
||||
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.1", "", { "os": "linux", "cpu": "arm" }, "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q=="],
|
||||
|
||||
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw=="],
|
||||
|
||||
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ=="],
|
||||
|
||||
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.1", "", { "os": "linux", "cpu": "x64" }, "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw=="],
|
||||
|
||||
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.1", "", { "os": "linux", "cpu": "x64" }, "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ=="],
|
||||
|
||||
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA=="],
|
||||
|
||||
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.1", "", { "os": "win32", "cpu": "x64" }, "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg=="],
|
||||
|
||||
"locate-path": ["locate-path@7.2.0", "", { "dependencies": { "p-locate": "^6.0.0" } }, "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA=="],
|
||||
|
||||
"lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
|
||||
|
||||
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
|
||||
|
||||
"loupe": ["loupe@3.1.4", "", {}, "sha512-wJzkKwJrheKtknCOKNEtDK4iqg/MxmZheEMtSTYvnzRdEYaZzmgH976nenp8WdJRdx5Vc1X/9MO0Oszl6ezeXg=="],
|
||||
|
||||
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
||||
|
||||
"lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="],
|
||||
|
||||
"magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="],
|
||||
|
||||
"magicast": ["magicast@0.3.5", "", { "dependencies": { "@babel/parser": "^7.25.4", "@babel/types": "^7.25.4", "source-map-js": "^1.2.0" } }, "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ=="],
|
||||
|
||||
"make-dir": ["make-dir@4.0.0", "", { "dependencies": { "semver": "^7.5.3" } }, "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw=="],
|
||||
|
||||
"min-indent": ["min-indent@1.0.1", "", {}, "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg=="],
|
||||
|
||||
"minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
||||
|
||||
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
|
||||
|
||||
"minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],
|
||||
|
||||
"minizlib": ["minizlib@3.0.2", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA=="],
|
||||
|
||||
"mkdirp": ["mkdirp@3.0.1", "", { "bin": { "mkdirp": "dist/cjs/src/bin.js" } }, "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg=="],
|
||||
|
||||
"mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||
|
||||
"node-releases": ["node-releases@2.0.19", "", {}, "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw=="],
|
||||
|
||||
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
||||
|
||||
"open": ["open@8.4.2", "", { "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", "is-wsl": "^2.2.0" } }, "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ=="],
|
||||
|
||||
"p-limit": ["p-limit@4.0.0", "", { "dependencies": { "yocto-queue": "^1.0.0" } }, "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ=="],
|
||||
|
||||
"p-locate": ["p-locate@6.0.0", "", { "dependencies": { "p-limit": "^4.0.0" } }, "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw=="],
|
||||
|
||||
"package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="],
|
||||
|
||||
"path-exists": ["path-exists@5.0.0", "", {}, "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ=="],
|
||||
|
||||
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
||||
|
||||
"path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="],
|
||||
|
||||
"path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
|
||||
|
||||
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
||||
|
||||
"pathval": ["pathval@2.0.1", "", {}, "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ=="],
|
||||
|
||||
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
|
||||
"picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="],
|
||||
|
||||
"playwright": ["playwright@1.53.2", "", { "dependencies": { "playwright-core": "1.53.2" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-6K/qQxVFuVQhRQhFsVZ9fGeatxirtrpPgxzBYWyZLEXJzqYwuL4fuNmfOfD5et1tJE4GScKyPNeLhZeRwuTU3A=="],
|
||||
|
||||
"playwright-core": ["playwright-core@1.53.2", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-ox/OytMy+2w1jcYEYlOo1Hhp8hZkLCximMTUTMBXjGUA1KoFfiSZ+DU+3a739jsPY0yoKH2TFy9S2fsJas8yAw=="],
|
||||
|
||||
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
|
||||
|
||||
"pretty-format": ["pretty-format@27.5.1", "", { "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ=="],
|
||||
|
||||
"prism-react-renderer": ["prism-react-renderer@2.4.1", "", { "dependencies": { "@types/prismjs": "^1.26.0", "clsx": "^2.0.0" }, "peerDependencies": { "react": ">=16.0.0" } }, "sha512-ey8Ls/+Di31eqzUxC46h8MksNuGx/n0AAC8uKpwFau4RPDYLuE3EXTp8N8G2vX2N7UC/+IXeNUnlWBGGcAG+Ig=="],
|
||||
|
||||
"prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="],
|
||||
|
||||
"prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="],
|
||||
|
||||
"react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="],
|
||||
|
||||
"react-bootstrap-icons": ["react-bootstrap-icons@1.11.6", "", { "dependencies": { "prop-types": "^15.7.2" }, "peerDependencies": { "react": ">=16.8.6" } }, "sha512-ycXiyeSyzbS1C4+MlPTYe0riB+UlZ7LV7YZQYqlERV2cxDiKtntI0huHmP/3VVvzPt4tGxqK0K+Y6g7We3U6tQ=="],
|
||||
|
||||
"react-docgen": ["react-docgen@8.0.0", "", { "dependencies": { "@babel/core": "^7.18.9", "@babel/traverse": "^7.18.9", "@babel/types": "^7.18.9", "@types/babel__core": "^7.18.0", "@types/babel__traverse": "^7.18.0", "@types/doctrine": "^0.0.9", "@types/resolve": "^1.20.2", "doctrine": "^3.0.0", "resolve": "^1.22.1", "strip-indent": "^4.0.0" } }, "sha512-kmob/FOTwep7DUWf9KjuenKX0vyvChr3oTdvvPt09V60Iz75FJp+T/0ZeHMbAfJj2WaVWqAPP5Hmm3PYzSPPKg=="],
|
||||
|
||||
"react-docgen-typescript": ["react-docgen-typescript@2.4.0", "", { "peerDependencies": { "typescript": ">= 4.3.x" } }, "sha512-ZtAp5XTO5HRzQctjPU0ybY0RRCQO19X/8fxn3w7y2VVTUbGHDKULPTL4ky3vB05euSgG5NpALhEhDPvQ56wvXg=="],
|
||||
|
||||
"react-dom": ["react-dom@19.1.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g=="],
|
||||
|
||||
"react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
|
||||
|
||||
"react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="],
|
||||
|
||||
"recast": ["recast@0.23.11", "", { "dependencies": { "ast-types": "^0.16.1", "esprima": "~4.0.0", "source-map": "~0.6.1", "tiny-invariant": "^1.3.3", "tslib": "^2.0.1" } }, "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA=="],
|
||||
|
||||
"redent": ["redent@3.0.0", "", { "dependencies": { "indent-string": "^4.0.0", "strip-indent": "^3.0.0" } }, "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg=="],
|
||||
|
||||
"resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="],
|
||||
|
||||
"rollup": ["rollup@4.44.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.44.1", "@rollup/rollup-android-arm64": "4.44.1", "@rollup/rollup-darwin-arm64": "4.44.1", "@rollup/rollup-darwin-x64": "4.44.1", "@rollup/rollup-freebsd-arm64": "4.44.1", "@rollup/rollup-freebsd-x64": "4.44.1", "@rollup/rollup-linux-arm-gnueabihf": "4.44.1", "@rollup/rollup-linux-arm-musleabihf": "4.44.1", "@rollup/rollup-linux-arm64-gnu": "4.44.1", "@rollup/rollup-linux-arm64-musl": "4.44.1", "@rollup/rollup-linux-loongarch64-gnu": "4.44.1", "@rollup/rollup-linux-powerpc64le-gnu": "4.44.1", "@rollup/rollup-linux-riscv64-gnu": "4.44.1", "@rollup/rollup-linux-riscv64-musl": "4.44.1", "@rollup/rollup-linux-s390x-gnu": "4.44.1", "@rollup/rollup-linux-x64-gnu": "4.44.1", "@rollup/rollup-linux-x64-musl": "4.44.1", "@rollup/rollup-win32-arm64-msvc": "4.44.1", "@rollup/rollup-win32-ia32-msvc": "4.44.1", "@rollup/rollup-win32-x64-msvc": "4.44.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-x8H8aPvD+xbl0Do8oez5f5o8eMS3trfCghc4HhLAnCkj7Vl0d1JWGs0UF/D886zLW2rOj2QymV/JcSSsw+XDNg=="],
|
||||
|
||||
"scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="],
|
||||
|
||||
"semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
|
||||
|
||||
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
|
||||
|
||||
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
|
||||
|
||||
"siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="],
|
||||
|
||||
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
|
||||
|
||||
"sirv": ["sirv@3.0.1", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A=="],
|
||||
|
||||
"sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="],
|
||||
|
||||
"source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
||||
|
||||
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||
|
||||
"stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="],
|
||||
|
||||
"std-env": ["std-env@3.9.0", "", {}, "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw=="],
|
||||
|
||||
"storybook": ["storybook@9.0.15", "", { "dependencies": { "@storybook/global": "^5.0.0", "@testing-library/jest-dom": "^6.6.3", "@testing-library/user-event": "^14.6.1", "@vitest/expect": "3.2.4", "@vitest/spy": "3.2.4", "better-opn": "^3.0.2", "esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0", "esbuild-register": "^3.5.0", "recast": "^0.23.5", "semver": "^7.6.2", "ws": "^8.18.0" }, "peerDependencies": { "prettier": "^2 || ^3" }, "optionalPeers": ["prettier"], "bin": "./bin/index.cjs" }, "sha512-r9hwcSMM3dq7dkMveaWFTosrmyHCL2FRrV3JOwVnVWraF6GtCgp2k+r4hsYtyp1bY3zdmK9e4KYzXsGs5q1h/Q=="],
|
||||
|
||||
"string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
|
||||
|
||||
"string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||
|
||||
"strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="],
|
||||
|
||||
"strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="],
|
||||
|
||||
"strip-indent": ["strip-indent@4.0.0", "", { "dependencies": { "min-indent": "^1.0.1" } }, "sha512-mnVSV2l+Zv6BLpSD/8V87CW/y9EmmbYzGCIavsnsI6/nwn26DwffM/yztm30Z/I2DY9wdS3vXVCMnHDgZaVNoA=="],
|
||||
|
||||
"strip-literal": ["strip-literal@3.0.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA=="],
|
||||
|
||||
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
||||
|
||||
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
|
||||
|
||||
"tabbable": ["tabbable@6.2.0", "", {}, "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew=="],
|
||||
|
||||
"tailwind-merge": ["tailwind-merge@3.3.1", "", {}, "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g=="],
|
||||
|
||||
"tailwind-scrollbar": ["tailwind-scrollbar@4.0.2", "", { "dependencies": { "prism-react-renderer": "^2.4.1" }, "peerDependencies": { "tailwindcss": "4.x" } }, "sha512-wAQiIxAPqk0MNTPptVe/xoyWi27y+NRGnTwvn4PQnbvB9kp8QUBiGl/wsfoVBHnQxTmhXJSNt9NHTmcz9EivFA=="],
|
||||
|
||||
"tailwindcss": ["tailwindcss@4.1.11", "", {}, "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA=="],
|
||||
|
||||
"tapable": ["tapable@2.2.2", "", {}, "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg=="],
|
||||
|
||||
"tar": ["tar@7.4.3", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.0.1", "mkdirp": "^3.0.1", "yallist": "^5.0.0" } }, "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw=="],
|
||||
|
||||
"test-exclude": ["test-exclude@7.0.1", "", { "dependencies": { "@istanbuljs/schema": "^0.1.2", "glob": "^10.4.1", "minimatch": "^9.0.4" } }, "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg=="],
|
||||
|
||||
"tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],
|
||||
|
||||
"tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="],
|
||||
|
||||
"tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="],
|
||||
|
||||
"tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="],
|
||||
|
||||
"tinypool": ["tinypool@1.1.1", "", {}, "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg=="],
|
||||
|
||||
"tinyrainbow": ["tinyrainbow@2.0.0", "", {}, "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw=="],
|
||||
|
||||
"tinyspy": ["tinyspy@4.0.3", "", {}, "sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A=="],
|
||||
|
||||
"totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="],
|
||||
|
||||
"ts-dedent": ["ts-dedent@2.2.0", "", {}, "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ=="],
|
||||
|
||||
"tsconfig-paths": ["tsconfig-paths@4.2.0", "", { "dependencies": { "json5": "^2.2.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg=="],
|
||||
|
||||
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
|
||||
|
||||
"undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],
|
||||
|
||||
"unicorn-magic": ["unicorn-magic@0.1.0", "", {}, "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ=="],
|
||||
|
||||
"universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="],
|
||||
|
||||
"unplugin": ["unplugin@1.16.1", "", { "dependencies": { "acorn": "^8.14.0", "webpack-virtual-modules": "^0.6.2" } }, "sha512-4/u/j4FrCKdi17jaxuJA0jClGxB1AvU2hw/IuayPc4ay1XGaJs/rbb4v5WKwAjNifjmXK9PIFyuPiaK8azyR9w=="],
|
||||
|
||||
"update-browserslist-db": ["update-browserslist-db@1.1.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw=="],
|
||||
|
||||
"vite": ["vite@7.0.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.6", "picomatch": "^4.0.2", "postcss": "^8.5.6", "rollup": "^4.40.0", "tinyglobby": "^0.2.14" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-BiKOQoW5HGR30E6JDeNsati6HnSPMVEKbkIWbCiol+xKeu3g5owrjy7kbk/QEMuzCV87dSUTvycYKmlcfGKq3Q=="],
|
||||
|
||||
"vite-node": ["vite-node@3.2.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="],
|
||||
|
||||
"vitest": ["vitest@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", "@vitest/mocker": "3.2.4", "@vitest/pretty-format": "^3.2.4", "@vitest/runner": "3.2.4", "@vitest/snapshot": "3.2.4", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "debug": "^4.4.1", "expect-type": "^1.2.1", "magic-string": "^0.30.17", "pathe": "^2.0.3", "picomatch": "^4.0.2", "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.14", "tinypool": "^1.1.1", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", "vite-node": "3.2.4", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@vitest/browser": "3.2.4", "@vitest/ui": "3.2.4", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A=="],
|
||||
|
||||
"webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="],
|
||||
|
||||
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||
|
||||
"why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="],
|
||||
|
||||
"wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="],
|
||||
|
||||
"wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
||||
|
||||
"ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="],
|
||||
|
||||
"yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],
|
||||
|
||||
"yocto-queue": ["yocto-queue@1.2.1", "", {}, "sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg=="],
|
||||
|
||||
"@babel/code-frame/js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||
|
||||
"@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
|
||||
"@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.4.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.4.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.0.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.11", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.9.0" }, "bundled": true }, "sha512-9DPkXtvHydrcOsopiYpUgPHpmj0HWZKMUnL2dZqpvC42lsratuBG06V5ipyno0fUek5VlFsNQ+AcFATSrJXgMA=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.9.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw=="],
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"@testing-library/jest-dom/chalk": ["chalk@3.0.0", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg=="],
|
||||
|
||||
"@testing-library/jest-dom/dom-accessibility-api": ["dom-accessibility-api@0.6.3", "", {}, "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w=="],
|
||||
|
||||
"@vitest/mocker/estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
|
||||
|
||||
"ast-v8-to-istanbul/estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
|
||||
|
||||
"loose-envify/js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||
|
||||
"lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
|
||||
|
||||
"path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
|
||||
|
||||
"pretty-format/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="],
|
||||
|
||||
"pretty-format/react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="],
|
||||
|
||||
"redent/strip-indent": ["strip-indent@3.0.0", "", { "dependencies": { "min-indent": "^1.0.0" } }, "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ=="],
|
||||
|
||||
"rollup/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||
|
||||
"string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
|
||||
"string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"strip-ansi-cjs/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"vite/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||
|
||||
"wrap-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="],
|
||||
|
||||
"wrap-ansi-cjs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||
|
||||
"wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||
|
||||
"string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
|
||||
"wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
|
||||
"wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||
}
|
||||
}
|
||||
|
||||
122
openhands-ui/components/button/Button.stories.tsx
Normal file
122
openhands-ui/components/button/Button.stories.tsx
Normal file
@@ -0,0 +1,122 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
|
||||
import { Button, type ButtonProps } from "./Button";
|
||||
import { Icon } from "../icon/Icon";
|
||||
|
||||
const meta = {
|
||||
title: "Components/Button",
|
||||
component: Button,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
argTypes: {},
|
||||
} satisfies Meta<typeof Button>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
const WithIconsComponent = (props: ButtonProps) => {
|
||||
return (
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<Button
|
||||
variant="primary"
|
||||
size="small"
|
||||
{...props}
|
||||
start={<Icon icon="ChevronLeft" />}
|
||||
end={<Icon icon="ChevronRight" />}
|
||||
>
|
||||
Click me
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="small"
|
||||
{...props}
|
||||
start={<Icon icon="ChevronLeft" />}
|
||||
end={<Icon icon="ChevronRight" />}
|
||||
>
|
||||
Click me
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="tertiary"
|
||||
size="small"
|
||||
{...props}
|
||||
start={<Icon icon="ChevronLeft" />}
|
||||
end={<Icon icon="ChevronRight" />}
|
||||
>
|
||||
Click me
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const LargeComponent = (props: ButtonProps) => {
|
||||
return (
|
||||
<div className="flex flex-col gap-y-2">
|
||||
<Button
|
||||
variant="primary"
|
||||
size="large"
|
||||
{...props}
|
||||
start={<Icon icon="ChevronLeft" />}
|
||||
end={<Icon icon="ChevronRight" />}
|
||||
>
|
||||
Click me
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="large"
|
||||
{...props}
|
||||
start={<Icon icon="HeartFill" />}
|
||||
end={<Icon icon="HeartFill" />}
|
||||
>
|
||||
Click me
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="tertiary"
|
||||
size="large"
|
||||
{...props}
|
||||
start={<Icon icon="HeartFill" />}
|
||||
end={<Icon icon="HeartFill" />}
|
||||
>
|
||||
Click me
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Primary: Story = {
|
||||
args: {
|
||||
variant: "primary",
|
||||
children: "Click me",
|
||||
disabled: false,
|
||||
},
|
||||
};
|
||||
export const Secondary: Story = {
|
||||
args: {
|
||||
variant: "secondary",
|
||||
children: "Click me",
|
||||
disabled: false,
|
||||
},
|
||||
};
|
||||
export const Tertiary: Story = {
|
||||
args: {
|
||||
variant: "tertiary",
|
||||
children: "Click me",
|
||||
disabled: false,
|
||||
},
|
||||
};
|
||||
export const Large: Story = {
|
||||
args: {
|
||||
disabled: false,
|
||||
},
|
||||
render: LargeComponent,
|
||||
};
|
||||
|
||||
export const WithIcons: Story = {
|
||||
args: {
|
||||
disabled: false,
|
||||
},
|
||||
render: WithIconsComponent,
|
||||
};
|
||||
61
openhands-ui/components/button/Button.tsx
Normal file
61
openhands-ui/components/button/Button.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import type { PropsWithChildren, ReactElement } from "react";
|
||||
import type { ComponentVariant, HTMLProps } from "../../shared/types";
|
||||
import { cn } from "../../shared/utils/cn";
|
||||
import { invariant } from "../../shared/utils/invariant";
|
||||
import { buttonStyles } from "./utils";
|
||||
import { Typography } from "../typography/Typography";
|
||||
import { cloneIcon } from "../../shared/utils/clone-icon";
|
||||
|
||||
export type ButtonProps = Omit<HTMLProps<"button">, "aria-disabled"> & {
|
||||
size?: "small" | "large";
|
||||
variant?: ComponentVariant;
|
||||
start?: ReactElement<HTMLProps<"svg">>;
|
||||
end?: ReactElement<HTMLProps<"svg">>;
|
||||
};
|
||||
|
||||
export const Button = ({
|
||||
size = "small",
|
||||
variant = "primary",
|
||||
className,
|
||||
children,
|
||||
start,
|
||||
end,
|
||||
...props
|
||||
}: PropsWithChildren<ButtonProps>) => {
|
||||
invariant(typeof children === "string", "Children must be string");
|
||||
const buttonClassNames = buttonStyles[variant];
|
||||
|
||||
const iconCss = "w-6 h-6";
|
||||
const hasIcons = start || end;
|
||||
|
||||
return (
|
||||
<button
|
||||
{...props}
|
||||
aria-disabled={props.disabled ? "true" : "false"}
|
||||
className={cn(
|
||||
size === "small" ? "px-3 py-1.5 min-w-32" : "px-3 py-3 min-w-64",
|
||||
"flex flex-row items-center gap-x-8",
|
||||
hasIcons ? " justify-between" : " justify-center",
|
||||
"group enabled:cursor-pointer focus:outline-0",
|
||||
buttonClassNames.button
|
||||
)}
|
||||
>
|
||||
{cloneIcon(start, {
|
||||
className: cn(iconCss, buttonClassNames.icon),
|
||||
})}
|
||||
|
||||
<Typography.Text
|
||||
fontSize="l"
|
||||
className={cn(
|
||||
"text-center font-size-l font-normal",
|
||||
buttonClassNames.text
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</Typography.Text>
|
||||
{cloneIcon(end, {
|
||||
className: cn(iconCss, buttonClassNames.icon),
|
||||
})}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
60
openhands-ui/components/button/utils.ts
Normal file
60
openhands-ui/components/button/utils.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import type { ComponentVariant } from "../../shared/types";
|
||||
import { cn } from "../../shared/utils/cn";
|
||||
|
||||
type ButtonStyle = {
|
||||
button: string;
|
||||
icon: string;
|
||||
text: string;
|
||||
};
|
||||
|
||||
export const buttonStyles: Record<ComponentVariant, ButtonStyle> = {
|
||||
primary: {
|
||||
button: cn([
|
||||
"ring-1 ring-solid ring-primary-500 rounded-full bg-grey-950",
|
||||
// hover modifier
|
||||
"enabled:hover:bg-grey-900 enabled:hover:ring-[1.5px]",
|
||||
// focus modifier
|
||||
"enabled:hover:bg-grey-900 enabled:focus:ring-[1.5px]",
|
||||
// active modifier
|
||||
"enabled:active:ring-1",
|
||||
// disabled modifier
|
||||
"disabled:opacity-50",
|
||||
]),
|
||||
icon: cn(["text-primary-500"]),
|
||||
text: cn(["text-primary-500"]),
|
||||
},
|
||||
secondary: {
|
||||
button: cn([
|
||||
"ring-1 ring-solid ring-light-neutral-300 rounded-full bg-light-neutral-950",
|
||||
// hover modifier
|
||||
"enabled:hover:bg-light-neutral-900 enabled:hover:ring-[1.5px]",
|
||||
// focus modifier
|
||||
"enabled:focus:bg-light-neutral-900 enabled:focus:ring-[1.5px]",
|
||||
// active modifier
|
||||
"enabled:active:ring-1",
|
||||
// disabled modifier
|
||||
"disabled:opacity-50",
|
||||
]),
|
||||
icon: cn(["text-light-neutral-300"]),
|
||||
text: cn(["text-light-neutral-300"]),
|
||||
},
|
||||
tertiary: {
|
||||
button: cn([
|
||||
"rounded-full",
|
||||
// hover modifier
|
||||
"enabled:hover:bg-grey-900",
|
||||
// focus modifier
|
||||
"enabled:focus:bg-grey-900",
|
||||
// active modifier
|
||||
"enabled:active:bg-grey-970",
|
||||
// disabled modifier
|
||||
"disabled:opacity-50",
|
||||
]),
|
||||
icon: cn(["text-primary-500"]),
|
||||
text: cn([
|
||||
"text-primary-500 underline",
|
||||
// disabled modifier
|
||||
"group-disabled:no-underline",
|
||||
]),
|
||||
},
|
||||
};
|
||||
44
openhands-ui/components/checkbox/Checkbox.stories.tsx
Normal file
44
openhands-ui/components/checkbox/Checkbox.stories.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { Checkbox, type CheckboxProps } from "./Checkbox";
|
||||
import { useState } from "react";
|
||||
|
||||
const meta = {
|
||||
title: "Components/Checkbox",
|
||||
component: Checkbox,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
} satisfies Meta<typeof Checkbox>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
const CheckboxComponent = (props: CheckboxProps) => {
|
||||
const [checked, setChecked] = useState(false);
|
||||
return (
|
||||
<Checkbox
|
||||
{...props}
|
||||
checked={checked}
|
||||
onChange={(e) => setChecked(e.target.checked)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const Enabled: Story = {
|
||||
args: {
|
||||
label:
|
||||
"Lorem Ipsum is simply dummy text of the printing and typesetting industry",
|
||||
},
|
||||
render: CheckboxComponent,
|
||||
};
|
||||
|
||||
export const Disabled: Story = {
|
||||
args: {
|
||||
disabled: true,
|
||||
label:
|
||||
"Lorem Ipsum is simply dummy text of the printing and typesetting industry",
|
||||
},
|
||||
render: CheckboxComponent,
|
||||
};
|
||||
80
openhands-ui/components/checkbox/Checkbox.tsx
Normal file
80
openhands-ui/components/checkbox/Checkbox.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import { useId } from "react";
|
||||
import type { HTMLProps } from "../../shared/types";
|
||||
import { cn } from "../../shared/utils/cn";
|
||||
import { Typography } from "../typography/Typography";
|
||||
import { Icon } from "../icon/Icon";
|
||||
|
||||
export type CheckboxProps = HTMLProps<"input"> & {
|
||||
label: React.ReactNode;
|
||||
labelClassName?: string;
|
||||
};
|
||||
|
||||
export const Checkbox = ({
|
||||
className,
|
||||
label,
|
||||
labelClassName,
|
||||
id: propId,
|
||||
disabled,
|
||||
checked,
|
||||
onChange,
|
||||
...props
|
||||
}: CheckboxProps) => {
|
||||
const generatedId = useId();
|
||||
const id = propId ?? generatedId;
|
||||
return (
|
||||
<label
|
||||
htmlFor={id}
|
||||
className={cn(
|
||||
"flex items-center gap-2 cursor-pointer",
|
||||
disabled && "cursor-not-allowed"
|
||||
)}
|
||||
>
|
||||
<input
|
||||
id={id}
|
||||
type="checkbox"
|
||||
checked={checked}
|
||||
onChange={onChange}
|
||||
disabled={disabled}
|
||||
className="sr-only peer"
|
||||
{...props}
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
"group",
|
||||
"w-5 h-5 border-1 border-grey-985 rounded-sm transition",
|
||||
"flex items-center justify-center",
|
||||
// base
|
||||
"border-light-neutral-500 bg-light-neutral-950",
|
||||
// disabled modifier
|
||||
"peer-disabled:opacity-50",
|
||||
// checked
|
||||
"peer-checked:bg-light-neutral-985",
|
||||
"peer-checked:border-primary-500",
|
||||
|
||||
// hover modifier
|
||||
"peer-hover:peer-enabled:bg-light-neutral-900",
|
||||
"peer-hover:peer-enabled:border-light-neutral-200",
|
||||
"peer-hover:peer-checked:peer-enabled:bg-light-neutral-985",
|
||||
"peer-hover:peer-checked:peer-enabled:border-primary-500",
|
||||
|
||||
// focus modifier
|
||||
"peer-focus:peer-enabled:bg-light-neutral-900",
|
||||
"peer-focus:peer-enabled:border-light-neutral-200",
|
||||
"peer-focus:peer-checked:peer-enabled:bg-light-neutral-985",
|
||||
"peer-focus:peer-checked:peer-enabled:border-primary-500"
|
||||
)}
|
||||
>
|
||||
{checked && (
|
||||
<Icon icon="CheckLg" className={cn("text-primary-500 w-6 h-6")} />
|
||||
)}
|
||||
</div>
|
||||
<Typography.Text
|
||||
fontSize="xxs"
|
||||
fontWeight={500}
|
||||
className={cn(labelClassName, disabled && "opacity-50")}
|
||||
>
|
||||
{label}
|
||||
</Typography.Text>
|
||||
</label>
|
||||
);
|
||||
};
|
||||
37
openhands-ui/components/chip/Chip.stories.tsx
Normal file
37
openhands-ui/components/chip/Chip.stories.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { Chip, type ChipProps } from "./Chip";
|
||||
|
||||
const colors = ["primaryDark", "primaryLight", "green", "red", "aqua", "gray"];
|
||||
|
||||
const meta = {
|
||||
title: "Components/Chip",
|
||||
component: Chip,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
argTypes: {
|
||||
color: {
|
||||
control: {
|
||||
type: "select",
|
||||
},
|
||||
options: colors,
|
||||
},
|
||||
},
|
||||
} satisfies Meta<typeof Chip>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
const ChipComponent = (props: ChipProps) => {
|
||||
return <Chip {...props}>Hello</Chip>;
|
||||
};
|
||||
|
||||
export const Main: Story = {
|
||||
args: {
|
||||
color: "aqua",
|
||||
variant: "pill",
|
||||
},
|
||||
render: ChipComponent,
|
||||
};
|
||||
38
openhands-ui/components/chip/Chip.tsx
Normal file
38
openhands-ui/components/chip/Chip.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { type PropsWithChildren } from "react";
|
||||
import type { HTMLProps } from "../../shared/types";
|
||||
import { cn } from "../../shared/utils/cn";
|
||||
import { Typography } from "../typography/Typography";
|
||||
import { chipStyles, type ChipColor, type ChipVariant } from "./utils";
|
||||
import { invariant } from "../../shared/utils/invariant";
|
||||
|
||||
export type ChipProps = Omit<HTMLProps<"div">, "label"> & {
|
||||
color?: ChipColor;
|
||||
variant?: ChipVariant;
|
||||
};
|
||||
|
||||
export const Chip = ({
|
||||
className,
|
||||
color = "gray",
|
||||
variant = "pill",
|
||||
children,
|
||||
...props
|
||||
}: PropsWithChildren<ChipProps>) => {
|
||||
invariant(typeof children === "string", "Children must be string");
|
||||
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
className={cn(
|
||||
"flex flex-row items-center px-1.5 py-1",
|
||||
variant === "pill" ? "rounded-full" : "rounded-lg",
|
||||
"border-1",
|
||||
chipStyles[color],
|
||||
className
|
||||
)}
|
||||
>
|
||||
<Typography.Text fontSize="xs" fontWeight={500} className="text-inherit">
|
||||
{children}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
17
openhands-ui/components/chip/utils.ts
Normal file
17
openhands-ui/components/chip/utils.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
export type ChipVariant = "pill" | "corner";
|
||||
export type ChipColor =
|
||||
| "primaryDark"
|
||||
| "primaryLight"
|
||||
| "green"
|
||||
| "red"
|
||||
| "aqua"
|
||||
| "gray";
|
||||
|
||||
export const chipStyles: Record<ChipColor, string> = {
|
||||
aqua: "border-aqua-500 text-aqua-500",
|
||||
gray: "border-light-neutral-400 text-light-neutral-400",
|
||||
green: "border-green-500 text-green-500",
|
||||
red: "border-red-400 text-red-400",
|
||||
primaryDark: "border-primary-400 text-primary-400",
|
||||
primaryLight: "border-primary-200 text-primary-200",
|
||||
};
|
||||
34
openhands-ui/components/divider/Divider.stories.tsx
Normal file
34
openhands-ui/components/divider/Divider.stories.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { Divider } from "./Divider";
|
||||
|
||||
const meta = {
|
||||
title: "Components/Divider",
|
||||
component: Divider,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
argTypes: {
|
||||
type: {
|
||||
control: {
|
||||
type: "select",
|
||||
},
|
||||
options: ["vertical", "horizontal"],
|
||||
},
|
||||
},
|
||||
} satisfies Meta<typeof Divider>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Main: Story = {
|
||||
args: {
|
||||
type: "vertical",
|
||||
},
|
||||
render: ({ type }) => (
|
||||
<div className="flex justify-center items-center h-64 w-64">
|
||||
<Divider type={type} />
|
||||
</div>
|
||||
),
|
||||
};
|
||||
28
openhands-ui/components/divider/Divider.tsx
Normal file
28
openhands-ui/components/divider/Divider.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import type { HTMLProps } from "../../shared/types";
|
||||
import { cn } from "../../shared/utils/cn";
|
||||
|
||||
export type DividerProps = Omit<
|
||||
HTMLProps<"div">,
|
||||
"role" | "aria-orientation"
|
||||
> & {
|
||||
type?: "horizontal" | "vertical";
|
||||
};
|
||||
|
||||
export const Divider = ({
|
||||
type = "horizontal",
|
||||
className,
|
||||
...props
|
||||
}: DividerProps) => {
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
className={cn(
|
||||
"bg-grey-900",
|
||||
type === "vertical" ? "w-px h-full" : "h-px w-full",
|
||||
className
|
||||
)}
|
||||
role="separator"
|
||||
aria-orientation={type}
|
||||
/>
|
||||
);
|
||||
};
|
||||
38
openhands-ui/components/icon/Icon.stories.tsx
Normal file
38
openhands-ui/components/icon/Icon.stories.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { Icon } from "./Icon";
|
||||
import * as icons from "react-bootstrap-icons";
|
||||
import { Typography } from "../typography/Typography";
|
||||
|
||||
const iconNames = Object.keys(icons);
|
||||
|
||||
const IconComponent = () => {
|
||||
return (
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 xl:grid-cols-8 gap-4">
|
||||
{iconNames.map((icon) => (
|
||||
<div key={icon} className="flex flex-col gap-y-1 items-center">
|
||||
<Icon icon={icon as any} className="w-6 h-6 text-white" />
|
||||
<Typography.Text fontSize="xs" className="text-white">
|
||||
{icon}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const meta = {
|
||||
title: "Components/Icon",
|
||||
component: IconComponent,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
} satisfies Meta;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Main: Story = {
|
||||
render: IconComponent,
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user