From 0d3e25c899dd75783ef05315e4e33b8cea015e94 Mon Sep 17 00:00:00 2001
From: Kaiohz
Date: Mon, 6 Apr 2026 07:37:19 +0200
Subject: [PATCH 1/9] feat: init composable-ui React frontend
React + TypeScript + Vite frontend for composable-agents API.
Features:
- Agent management (CRUD with YAML upload)
- Real-time chat with SSE streaming
- HITL (Human-in-the-loop) support
- Thread history grouped by agent
Stack: React 18, Tailwind CSS, shadcn/ui, Zustand, React Query
Architecture: Hexagonal (domain/infrastructure/application)
CI/CD: Build + tests + Trivy + SonarQube / Docker + Flux
---
.dockerignore | 14 +
.env.example | 2 +
.github/workflows/cd.yaml | 127 ++
.github/workflows/ci.yaml | 60 +
.gitignore | 35 +
.trivyignore | 0
Dockerfile | 33 +
Makefile | 13 +
README.md | 151 +++
bun.lock | 1153 +++++++++++++++++
components.json | 20 +
eslint.config.js | 29 +
index.html | 15 +
nginx.conf | 48 +
package.json | 81 ++
postcss.config.js | 6 +
public/favicon.svg | 1 +
public/icons.svg | 24 +
sonar-project.properties | 10 +
src/application/App.tsx | 15 +
src/application/components/agent/.gitkeep | 0
.../components/agent/AgentCard.tsx | 77 ++
.../components/agent/AgentConfigViewer.tsx | 259 ++++
.../components/agent/AgentGrid.tsx | 58 +
.../components/agent/CreateAgentDialog.tsx | 132 ++
src/application/components/chat/.gitkeep | 0
src/application/components/chat/ChatInput.tsx | 68 +
.../components/chat/ChatMessage.tsx | 117 ++
.../components/chat/HITLReviewPanel.tsx | 141 ++
.../components/chat/MessageList.tsx | 87 ++
src/application/components/layout/.gitkeep | 0
.../components/layout/MainLayout.tsx | 25 +
.../components/layout/ThreadSidebar.tsx | 117 ++
src/application/components/layout/TopNav.tsx | 60 +
src/application/components/shared/.gitkeep | 0
.../components/shared/StatusBadge.tsx | 35 +
src/application/components/shared/ToolTag.tsx | 11 +
src/application/components/ui/.gitkeep | 0
src/application/hooks/agent/.gitkeep | 0
src/application/hooks/agent/useAgentConfig.ts | 10 +
src/application/hooks/agent/useAgents.ts | 9 +
src/application/hooks/agent/useCreateAgent.ts | 14 +
src/application/hooks/agent/useDeleteAgent.ts | 13 +
src/application/hooks/agent/useUpdateAgent.ts | 15 +
src/application/hooks/chat/.gitkeep | 0
src/application/hooks/chat/useCreateThread.ts | 13 +
src/application/hooks/chat/useDeleteThread.ts | 13 +
src/application/hooks/chat/useMessages.ts | 10 +
src/application/hooks/chat/useSendMessage.ts | 20 +
src/application/hooks/chat/useStreamChat.ts | 54 +
src/application/hooks/chat/useThreads.ts | 9 +
src/application/index.css | 93 ++
src/application/lib/utils.ts | 6 +
src/application/main.tsx | 27 +
src/application/pages/.gitkeep | 0
src/application/pages/AgentsPage.tsx | 62 +
src/application/pages/ChatPage.tsx | 49 +
src/application/stores/.gitkeep | 0
src/application/stores/useChatStore.ts | 22 +
src/application/vite-env.d.ts | 1 +
src/domain/entities/agent/agentConfig.ts | 56 +
.../entities/agent/agentConfigMetadata.ts | 8 +
src/domain/entities/agent/mcpServerConfig.ts | 15 +
src/domain/entities/chat/chatRequest.ts | 9 +
src/domain/entities/chat/message.ts | 27 +
src/domain/entities/chat/thread.ts | 9 +
src/domain/lib/.gitkeep | 0
src/domain/ports/agent/.gitkeep | 0
src/domain/ports/agent/agentPort.ts | 10 +
src/domain/ports/chat/.gitkeep | 0
src/domain/ports/chat/chatPort.ts | 19 +
src/infrastructure/api/agent/.gitkeep | 0
src/infrastructure/api/agent/agentApi.ts | 52 +
src/infrastructure/api/axiosInstance.ts | 21 +
src/infrastructure/api/chat/.gitkeep | 0
src/infrastructure/api/chat/chatApi.ts | 76 ++
src/infrastructure/config/.gitkeep | 0
src/infrastructure/config/envConfig.ts | 4 +
src/infrastructure/ws/.gitkeep | 0
tailwind.config.ts | 74 ++
tests/fixtures/external.ts | 44 +
tests/setup.ts | 90 ++
.../unit/components/agent/AgentCard.test.tsx | 45 +
.../unit/components/agent/AgentGrid.test.tsx | 79 ++
tests/unit/components/chat/ChatInput.test.tsx | 60 +
.../unit/components/chat/ChatMessage.test.tsx | 64 +
.../components/shared/StatusBadge.test.tsx | 46 +
tests/unit/components/shared/ToolTag.test.tsx | 12 +
.../unit/domain/entities/agentConfig.test.ts | 30 +
tests/unit/domain/entities/message.test.ts | 18 +
.../infrastructure/api/agent/agentApi.test.ts | 101 ++
.../infrastructure/api/chat/chatApi.test.ts | 102 ++
tests/unit/pages/AgentsPage.test.tsx | 44 +
tests/unit/pages/ChatPage.test.tsx | 79 ++
tests/unit/smoke.test.ts | 7 +
tests/utils/render.tsx | 42 +
trivy.yaml | 11 +
tsconfig.app.json | 28 +
tsconfig.json | 18 +
tsconfig.node.json | 20 +
vite.config.ts | 26 +
vitest.config.ts | 37 +
102 files changed, 4847 insertions(+)
create mode 100644 .dockerignore
create mode 100644 .env.example
create mode 100644 .github/workflows/cd.yaml
create mode 100644 .github/workflows/ci.yaml
create mode 100644 .gitignore
create mode 100644 .trivyignore
create mode 100644 Dockerfile
create mode 100644 Makefile
create mode 100644 README.md
create mode 100644 bun.lock
create mode 100644 components.json
create mode 100644 eslint.config.js
create mode 100644 index.html
create mode 100644 nginx.conf
create mode 100644 package.json
create mode 100644 postcss.config.js
create mode 100644 public/favicon.svg
create mode 100644 public/icons.svg
create mode 100644 sonar-project.properties
create mode 100644 src/application/App.tsx
create mode 100644 src/application/components/agent/.gitkeep
create mode 100644 src/application/components/agent/AgentCard.tsx
create mode 100644 src/application/components/agent/AgentConfigViewer.tsx
create mode 100644 src/application/components/agent/AgentGrid.tsx
create mode 100644 src/application/components/agent/CreateAgentDialog.tsx
create mode 100644 src/application/components/chat/.gitkeep
create mode 100644 src/application/components/chat/ChatInput.tsx
create mode 100644 src/application/components/chat/ChatMessage.tsx
create mode 100644 src/application/components/chat/HITLReviewPanel.tsx
create mode 100644 src/application/components/chat/MessageList.tsx
create mode 100644 src/application/components/layout/.gitkeep
create mode 100644 src/application/components/layout/MainLayout.tsx
create mode 100644 src/application/components/layout/ThreadSidebar.tsx
create mode 100644 src/application/components/layout/TopNav.tsx
create mode 100644 src/application/components/shared/.gitkeep
create mode 100644 src/application/components/shared/StatusBadge.tsx
create mode 100644 src/application/components/shared/ToolTag.tsx
create mode 100644 src/application/components/ui/.gitkeep
create mode 100644 src/application/hooks/agent/.gitkeep
create mode 100644 src/application/hooks/agent/useAgentConfig.ts
create mode 100644 src/application/hooks/agent/useAgents.ts
create mode 100644 src/application/hooks/agent/useCreateAgent.ts
create mode 100644 src/application/hooks/agent/useDeleteAgent.ts
create mode 100644 src/application/hooks/agent/useUpdateAgent.ts
create mode 100644 src/application/hooks/chat/.gitkeep
create mode 100644 src/application/hooks/chat/useCreateThread.ts
create mode 100644 src/application/hooks/chat/useDeleteThread.ts
create mode 100644 src/application/hooks/chat/useMessages.ts
create mode 100644 src/application/hooks/chat/useSendMessage.ts
create mode 100644 src/application/hooks/chat/useStreamChat.ts
create mode 100644 src/application/hooks/chat/useThreads.ts
create mode 100644 src/application/index.css
create mode 100644 src/application/lib/utils.ts
create mode 100644 src/application/main.tsx
create mode 100644 src/application/pages/.gitkeep
create mode 100644 src/application/pages/AgentsPage.tsx
create mode 100644 src/application/pages/ChatPage.tsx
create mode 100644 src/application/stores/.gitkeep
create mode 100644 src/application/stores/useChatStore.ts
create mode 100644 src/application/vite-env.d.ts
create mode 100644 src/domain/entities/agent/agentConfig.ts
create mode 100644 src/domain/entities/agent/agentConfigMetadata.ts
create mode 100644 src/domain/entities/agent/mcpServerConfig.ts
create mode 100644 src/domain/entities/chat/chatRequest.ts
create mode 100644 src/domain/entities/chat/message.ts
create mode 100644 src/domain/entities/chat/thread.ts
create mode 100644 src/domain/lib/.gitkeep
create mode 100644 src/domain/ports/agent/.gitkeep
create mode 100644 src/domain/ports/agent/agentPort.ts
create mode 100644 src/domain/ports/chat/.gitkeep
create mode 100644 src/domain/ports/chat/chatPort.ts
create mode 100644 src/infrastructure/api/agent/.gitkeep
create mode 100644 src/infrastructure/api/agent/agentApi.ts
create mode 100644 src/infrastructure/api/axiosInstance.ts
create mode 100644 src/infrastructure/api/chat/.gitkeep
create mode 100644 src/infrastructure/api/chat/chatApi.ts
create mode 100644 src/infrastructure/config/.gitkeep
create mode 100644 src/infrastructure/config/envConfig.ts
create mode 100644 src/infrastructure/ws/.gitkeep
create mode 100644 tailwind.config.ts
create mode 100644 tests/fixtures/external.ts
create mode 100644 tests/setup.ts
create mode 100644 tests/unit/components/agent/AgentCard.test.tsx
create mode 100644 tests/unit/components/agent/AgentGrid.test.tsx
create mode 100644 tests/unit/components/chat/ChatInput.test.tsx
create mode 100644 tests/unit/components/chat/ChatMessage.test.tsx
create mode 100644 tests/unit/components/shared/StatusBadge.test.tsx
create mode 100644 tests/unit/components/shared/ToolTag.test.tsx
create mode 100644 tests/unit/domain/entities/agentConfig.test.ts
create mode 100644 tests/unit/domain/entities/message.test.ts
create mode 100644 tests/unit/infrastructure/api/agent/agentApi.test.ts
create mode 100644 tests/unit/infrastructure/api/chat/chatApi.test.ts
create mode 100644 tests/unit/pages/AgentsPage.test.tsx
create mode 100644 tests/unit/pages/ChatPage.test.tsx
create mode 100644 tests/unit/smoke.test.ts
create mode 100644 tests/utils/render.tsx
create mode 100644 trivy.yaml
create mode 100644 tsconfig.app.json
create mode 100644 tsconfig.json
create mode 100644 tsconfig.node.json
create mode 100644 vite.config.ts
create mode 100644 vitest.config.ts
diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..55d08a8
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,14 @@
+node_modules
+dist
+coverage
+.git
+.github
+.vscode
+.idea
+*.md
+.env*
+.scannerwork
+.trivyignore
+trivy-report*.json
+sonar-project.properties
+*.tsbuildinfo
diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..d312d6c
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,2 @@
+VITE_API_BASE_URL=http://localhost:8010
+VITE_WS_BASE_URL=ws://localhost:8010
diff --git a/.github/workflows/cd.yaml b/.github/workflows/cd.yaml
new file mode 100644
index 0000000..b2861bd
--- /dev/null
+++ b/.github/workflows/cd.yaml
@@ -0,0 +1,127 @@
+name: CD
+
+on:
+ pull_request:
+ types: [closed]
+ issue_comment:
+ types: [created]
+
+permissions:
+ contents: read
+ pull-requests: write
+
+jobs:
+ build-and-push:
+ if: >
+ (github.event_name == 'pull_request' && github.event.pull_request.merged == true) ||
+ (github.event_name == 'issue_comment' && github.event.issue.pull_request && contains(github.event.comment.body, '/deploy'))
+ runs-on: ubuntu-latest
+ steps:
+ - name: Get deploy SHA
+ id: sha
+ uses: actions/github-script@v7
+ with:
+ script: |
+ if (context.eventName === 'issue_comment') {
+ const pr = await github.rest.pulls.get({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ pull_number: context.issue.number,
+ });
+ return pr.data.head.sha;
+ }
+ return context.sha;
+ result-encoding: string
+
+ - name: Add deploy reaction
+ if: github.event_name == 'issue_comment'
+ uses: actions/github-script@v7
+ with:
+ script: |
+ await github.rest.reactions.createForIssueComment({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ comment_id: context.payload.comment.id,
+ content: 'rocket',
+ });
+
+ - name: Checkout code
+ uses: actions/checkout@v4
+ with:
+ ref: ${{ steps.sha.outputs.result }}
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+
+ - name: Log in to Docker Hub
+ uses: docker/login-action@v3
+ with:
+ username: ${{ secrets.DOCKERHUB_USERNAME }}
+ password: ${{ secrets.DOCKERHUB_TOKEN }}
+
+ - name: Build Docker image for scanning (amd64)
+ uses: docker/build-push-action@v5
+ with:
+ context: .
+ file: ./Dockerfile
+ load: true
+ platforms: linux/amd64
+ tags: kaiohz/pickpro:composable-ui-scan
+ cache-from: type=gha
+ cache-to: type=gha,mode=max
+
+ - name: Trivy Image Scan (report)
+ uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1
+ with:
+ image-ref: 'kaiohz/pickpro:composable-ui-scan'
+ format: 'table'
+ severity: 'CRITICAL,HIGH,MEDIUM'
+ exit-code: '0'
+ trivy-config: trivy.yaml
+ trivy-version: 'v0.69.3'
+
+ - name: Trivy Image Scan (CRITICAL gate)
+ uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1
+ with:
+ image-ref: 'kaiohz/pickpro:composable-ui-scan'
+ format: 'table'
+ severity: 'CRITICAL'
+ exit-code: '1'
+ trivy-config: trivy.yaml
+ trivy-version: 'v0.69.3'
+
+ - name: Build and push Docker image (multi-platform)
+ uses: docker/build-push-action@v5
+ with:
+ context: .
+ file: ./Dockerfile
+ push: true
+ platforms: linux/amd64,linux/arm64
+ tags: |
+ kaiohz/pickpro:composable-ui-${{ steps.sha.outputs.result }}
+ kaiohz/pickpro:composable-ui-latest
+ cache-from: type=gha
+ cache-to: type=gha,mode=max
+
+ - name: Checkout flux repository
+ run: |
+ git clone https://x-access-token:${{ secrets.FLUX_REPO_TOKEN }}@github.com/SoluDevTech/flux.git flux-repo
+
+ - name: Update deployment image tag
+ run: |
+ DEPLOYMENT_FILE="flux-repo/dev/bricks/composable-ui/deployment.yaml"
+ if [ -f "$DEPLOYMENT_FILE" ]; then
+ sed -i 's|image: kaiohz/pickpro:composable-ui-.*|image: kaiohz/pickpro:composable-ui-${{ steps.sha.outputs.result }}|g' "$DEPLOYMENT_FILE"
+ else
+ echo "Error: Deployment file not found at $DEPLOYMENT_FILE"
+ exit 1
+ fi
+
+ - name: Commit and push changes
+ run: |
+ cd flux-repo
+ git config user.name "github-actions[bot]"
+ git config user.email "github-actions[bot]@users.noreply.github.com"
+ git add dev/bricks/composable-ui/deployment.yaml
+ git commit -m "Update composable-ui image to ${{ steps.sha.outputs.result }}" || echo "No changes to commit"
+ git push https://x-access-token:${{ secrets.FLUX_REPO_TOKEN }}@github.com/SoluDevTech/flux.git main
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
new file mode 100644
index 0000000..6e832fb
--- /dev/null
+++ b/.github/workflows/ci.yaml
@@ -0,0 +1,60 @@
+name: CI
+
+on:
+ pull_request:
+ types: [opened, synchronize, reopened]
+
+permissions:
+ contents: read
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Setup Bun
+ uses: oven-sh/setup-bun@v1
+
+ - name: Install dependencies
+ run: bun install --frozen-lockfile || bun install
+
+ - name: Run build check
+ run: bun run build
+
+ - name: Run tests with coverage
+ run: bun run test --coverage
+
+ - name: Trivy FS Scan (report)
+ uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1
+ with:
+ scan-type: 'fs'
+ scan-ref: '.'
+ format: 'table'
+ severity: 'CRITICAL,HIGH,MEDIUM'
+ exit-code: '0'
+ trivy-config: trivy.yaml
+ trivy-version: 'v0.69.3'
+
+ - name: Trivy FS Scan (CRITICAL gate)
+ uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1
+ with:
+ scan-type: 'fs'
+ scan-ref: '.'
+ format: 'table'
+ severity: 'CRITICAL'
+ exit-code: '1'
+ trivy-config: trivy.yaml
+ trivy-version: 'v0.69.3'
+
+ - name: SonarQube Scan
+ uses: SonarSource/sonarqube-scan-action@v5
+ env:
+ SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+ SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
+ with:
+ args: >
+ -Dsonar.qualitygate.wait=false
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..8bb6865
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,35 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+
+# Testing
+coverage
+
+# TypeScript build artifacts
+*.tsbuildinfo
+
+# Environment
+.env
+.env.local
+.env.*.local
diff --git a/.trivyignore b/.trivyignore
new file mode 100644
index 0000000..e69de29
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..565d924
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,33 @@
+FROM oven/bun:1 AS deps
+
+WORKDIR /app
+
+COPY package.json bun.lock ./
+
+RUN bun install --frozen-lockfile || bun install
+
+FROM deps AS builder
+
+COPY . .
+
+RUN bun run build
+
+FROM nginx:alpine AS production
+
+RUN apk update && apk upgrade --no-cache && apk add --no-cache dumb-init
+
+RUN rm -rf /usr/share/nginx/html/* && \
+ rm /etc/nginx/conf.d/default.conf
+
+COPY --from=builder /app/dist /usr/share/nginx/html
+
+COPY nginx.conf /etc/nginx/conf.d/default.conf
+
+EXPOSE 80
+
+HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
+ CMD wget --no-verbose --tries=1 --spider http://127.0.0.1:80/ || exit 1
+
+ENTRYPOINT ["dumb-init", "--"]
+
+CMD ["nginx", "-g", "daemon off;"]
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..ec915df
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,13 @@
+.PHONY: trivy-fs trivy-image trivy-fs-critical trivy-image-critical
+
+trivy-fs:
+ trivy fs --severity CRITICAL,HIGH,MEDIUM --config trivy.yaml .
+
+trivy-image:
+ trivy image --severity CRITICAL,HIGH,MEDIUM --config trivy.yaml kaiohz/pickpro:composable-ui-latest
+
+trivy-fs-critical:
+ trivy fs --severity CRITICAL --exit-code 1 --config trivy.yaml .
+
+trivy-image-critical:
+ trivy image --severity CRITICAL --exit-code 1 --config trivy.yaml kaiohz/pickpro:composable-ui-latest
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..0dffaac
--- /dev/null
+++ b/README.md
@@ -0,0 +1,151 @@
+# Composable UI
+
+React frontend for interacting with the [composable-agents](https://github.com/soludev/bricks/composable-agents) API. Provides a chat interface for AI agents with real-time streaming, human-in-the-loop (HITL) tool validation, and full agent management.
+
+## Features
+
+- **Agent management** -- Create, view, configure, and delete agents via YAML file upload
+- **Real-time chat** -- Stream AI responses via SSE (Server-Sent Events)
+- **Human-in-the-loop** -- Review and approve/reject tool calls before execution
+- **Thread history** -- Conversation threads grouped by agent in a sidebar
+- **Material Design 3** -- Inspired design system with shadcn/ui components
+
+## Tech Stack
+
+- **Runtime**: Bun
+- **Framework**: React 19 + TypeScript
+- **Build**: Vite 8
+- **Styling**: Tailwind CSS 4 + shadcn/ui + Framer Motion
+- **State**: Zustand (local) + TanStack React Query (server)
+- **Forms**: React Hook Form + Zod validation
+- **Testing**: Vitest + Testing Library
+- **Linting**: ESLint + Prettier
+
+## Prerequisites
+
+- [Bun](https://bun.sh/) >= 1.0
+- [composable-agents](https://github.com/soludev/bricks/composable-agents) API running on port 8010
+
+## Installation
+
+```bash
+bun install
+```
+
+## Configuration
+
+| Variable | Default | Description |
+|---|---|---|
+| `VITE_API_BASE_URL` | `http://localhost:8010` | composable-agents API URL |
+| `VITE_WS_BASE_URL` | `ws://localhost:8010` | WebSocket URL for streaming |
+
+The Vite dev server proxies `/api` requests to the API automatically (see `vite.config.ts`).
+
+## Running
+
+```bash
+# Development (port 8030)
+bun run dev
+
+# Production build
+bun run build
+
+# Preview production build
+bun run preview
+```
+
+## Testing
+
+```bash
+bun run test # Run all tests
+bun run test:watch # Watch mode
+bun run test:ui # Vitest UI
+bun run test:coverage # With coverage report
+```
+
+## Linting and Formatting
+
+```bash
+bun run lint # Run ESLint
+bun run lint:fix # Auto-fix lint issues
+bun run format # Format with Prettier
+bun run format:check # Check formatting
+```
+
+## Docker
+
+Multi-stage build: `bun` (install + build) then `nginx:alpine` (serve).
+
+```bash
+# Build image
+docker build -t kaiohz/pickpro:composable-ui-latest .
+
+# Run container (port 8030 -> nginx on 80)
+docker run -p 8030:80 kaiohz/pickpro:composable-ui-latest
+```
+
+For the full stack, use the Docker Compose setup in `soludev-compose-apps/bricks`.
+
+## Security Scans
+
+```bash
+make trivy-fs # Trivy filesystem scan (CRITICAL/HIGH/MEDIUM)
+make trivy-fs-critical # Trivy filesystem scan (CRITICAL only, exit code 1)
+make trivy-image # Trivy image scan
+make trivy-image-critical # Trivy image scan (CRITICAL only, exit code 1)
+```
+
+## Project Structure
+
+```
+src/
+ domain/ # Business entities and port interfaces
+ entities/
+ agent/ # AgentConfig, AgentConfigMetadata, McpServerConfig
+ chat/ # Message, Thread, ChatRequest
+ ports/
+ agent/agentPort.ts # Agent repository interface
+ chat/chatPort.ts # Chat repository interface
+ infrastructure/ # External adapters (API clients, config)
+ api/
+ agent/agentApi.ts # Agent API adapter (axios)
+ chat/chatApi.ts # Chat API adapter (axios + SSE)
+ axiosInstance.ts # Shared axios instance
+ config/envConfig.ts # Environment variables
+ application/ # React UI layer
+ components/
+ agent/ # AgentCard, AgentGrid, CreateAgentDialog, AgentConfigViewer
+ chat/ # ChatInput, ChatMessage, HITLReviewPanel, MessageList
+ layout/ # MainLayout, ThreadSidebar, TopNav
+ shared/ # StatusBadge, ToolTag
+ ui/ # shadcn/ui primitives
+ hooks/
+ agent/ # useAgents, useCreateAgent, useDeleteAgent, useUpdateAgent, useAgentConfig
+ chat/ # useThreads, useCreateThread, useDeleteThread, useMessages, useSendMessage, useStreamChat
+ pages/
+ AgentsPage.tsx # /agents route
+ ChatPage.tsx # /chat/:threadId? route
+ stores/
+ useChatStore.ts # Zustand store for chat state
+tests/
+ unit/ # Mirrors src/ structure
+ fixtures/ # Test data
+ utils/ # Test helpers (custom render)
+```
+
+## Routes
+
+| Path | Page | Description |
+|---|---|---|
+| `/` | -- | Redirects to `/chat` |
+| `/agents` | AgentsPage | List, create, view, and delete agents |
+| `/chat/:threadId?` | ChatPage | Chat with agents, streaming responses, HITL validation |
+
+## CI/CD
+
+- **CI** (on PR): Build + tests + Trivy FS scan + SonarQube analysis
+- **CD** (on merge to main): Docker build + push to DockerHub (`kaiohz/pickpro:composable-ui-*`) + Flux image update
+
+## License
+
+Private -- internal use only.
diff --git a/bun.lock b/bun.lock
new file mode 100644
index 0000000..70fcfef
--- /dev/null
+++ b/bun.lock
@@ -0,0 +1,1153 @@
+{
+ "lockfileVersion": 1,
+ "workspaces": {
+ "": {
+ "name": "composable-ui",
+ "dependencies": {
+ "@hookform/resolvers": "^5.2.2",
+ "@microsoft/fetch-event-source": "^2.0.1",
+ "@radix-ui/react-alert-dialog": "^1.1.15",
+ "@radix-ui/react-avatar": "^1.1.11",
+ "@radix-ui/react-dialog": "^1.1.15",
+ "@radix-ui/react-dropdown-menu": "^2.1.16",
+ "@radix-ui/react-label": "^2.1.8",
+ "@radix-ui/react-popover": "^1.1.15",
+ "@radix-ui/react-scroll-area": "^1.2.10",
+ "@radix-ui/react-select": "^2.2.6",
+ "@radix-ui/react-separator": "^1.1.8",
+ "@radix-ui/react-slot": "^1.2.4",
+ "@radix-ui/react-tabs": "^1.1.13",
+ "@radix-ui/react-toggle": "^1.1.10",
+ "@radix-ui/react-toggle-group": "^1.1.11",
+ "@radix-ui/react-tooltip": "^1.2.8",
+ "@tanstack/react-query": "^5.96.2",
+ "axios": "^1.14.0",
+ "class-variance-authority": "^0.7.1",
+ "clsx": "^2.1.1",
+ "cmdk": "^1.1.1",
+ "framer-motion": "^12.38.0",
+ "lucide-react": "^1.7.0",
+ "react": "^19.2.4",
+ "react-dom": "^19.2.4",
+ "react-hook-form": "^7.72.1",
+ "react-markdown": "^10.1.0",
+ "react-router-dom": "^7.14.0",
+ "remark-gfm": "^4.0.1",
+ "sonner": "^2.0.7",
+ "tailwind-merge": "^3.5.0",
+ "tailwindcss-animate": "^1.0.7",
+ "zod": "^4.3.6",
+ "zustand": "^5.0.12",
+ },
+ "devDependencies": {
+ "@eslint/js": "^10.0.1",
+ "@tailwindcss/postcss": "^4.2.2",
+ "@testing-library/jest-dom": "^6.9.1",
+ "@testing-library/react": "^16.3.2",
+ "@testing-library/user-event": "^14.6.1",
+ "@types/node": "^25.5.2",
+ "@types/react": "^19.2.14",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react-swc": "^4.3.0",
+ "@vitest/coverage-v8": "^4.1.2",
+ "@vitest/ui": "^4.1.2",
+ "autoprefixer": "^10.4.27",
+ "eslint": "^10.2.0",
+ "eslint-plugin-react-hooks": "^7.0.1",
+ "eslint-plugin-react-refresh": "^0.5.2",
+ "globals": "^17.4.0",
+ "jsdom": "25.0.1",
+ "postcss": "^8.5.8",
+ "prettier": "^3.8.1",
+ "tailwindcss": "^4.2.2",
+ "typescript": "^6.0.2",
+ "typescript-eslint": "^8.58.0",
+ "vite": "^8.0.1",
+ "vitest": "^4.1.2",
+ },
+ },
+ },
+ "packages": {
+ "@adobe/css-tools": ["@adobe/css-tools@4.4.4", "", {}, "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg=="],
+
+ "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
+
+ "@asamuzakjp/css-color": ["@asamuzakjp/css-color@3.2.0", "", { "dependencies": { "@csstools/css-calc": "^2.1.3", "@csstools/css-color-parser": "^3.0.9", "@csstools/css-parser-algorithms": "^3.0.4", "@csstools/css-tokenizer": "^3.0.3", "lru-cache": "^10.4.3" } }, "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw=="],
+
+ "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
+
+ "@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="],
+
+ "@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "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-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA=="],
+
+ "@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="],
+
+ "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="],
+
+ "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
+
+ "@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="],
+
+ "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="],
+
+ "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
+
+ "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
+
+ "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
+
+ "@babel/helpers": ["@babel/helpers@7.29.2", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.29.0" } }, "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw=="],
+
+ "@babel/parser": ["@babel/parser@7.29.2", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA=="],
+
+ "@babel/runtime": ["@babel/runtime@7.29.2", "", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="],
+
+ "@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="],
+
+ "@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="],
+
+ "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
+
+ "@bcoe/v8-coverage": ["@bcoe/v8-coverage@1.0.2", "", {}, "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA=="],
+
+ "@csstools/color-helpers": ["@csstools/color-helpers@5.1.0", "", {}, "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA=="],
+
+ "@csstools/css-calc": ["@csstools/css-calc@2.1.4", "", { "peerDependencies": { "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ=="],
+
+ "@csstools/css-color-parser": ["@csstools/css-color-parser@3.1.0", "", { "dependencies": { "@csstools/color-helpers": "^5.1.0", "@csstools/css-calc": "^2.1.4" }, "peerDependencies": { "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA=="],
+
+ "@csstools/css-parser-algorithms": ["@csstools/css-parser-algorithms@3.0.5", "", { "peerDependencies": { "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ=="],
+
+ "@csstools/css-tokenizer": ["@csstools/css-tokenizer@3.0.4", "", {}, "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw=="],
+
+ "@emnapi/core": ["@emnapi/core@1.9.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA=="],
+
+ "@emnapi/runtime": ["@emnapi/runtime@1.9.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw=="],
+
+ "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="],
+
+ "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="],
+
+ "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="],
+
+ "@eslint/config-array": ["@eslint/config-array@0.23.4", "", { "dependencies": { "@eslint/object-schema": "^3.0.4", "debug": "^4.3.1", "minimatch": "^10.2.4" } }, "sha512-lf19F24LSMfF8weXvW5QEtnLqW70u7kgit5e9PSx0MsHAFclGd1T9ynvWEMDT1w5J4Qt54tomGeAhdoAku1Xow=="],
+
+ "@eslint/config-helpers": ["@eslint/config-helpers@0.5.4", "", { "dependencies": { "@eslint/core": "^1.2.0" } }, "sha512-jJhqiY3wPMlWWO3370M86CPJ7pt8GmEwSLglMfQhjXal07RCvhmU0as4IuUEW5SJeunfItiEetHmSxCCe9lDBg=="],
+
+ "@eslint/core": ["@eslint/core@1.2.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-8FTGbNzTvmSlc4cZBaShkC6YvFMG0riksYWRFKXztqVdXaQbcZLXlFbSpC05s70sGEsXAw0qwhx69JiW7hQS7A=="],
+
+ "@eslint/js": ["@eslint/js@10.0.1", "", { "peerDependencies": { "eslint": "^10.0.0" }, "optionalPeers": ["eslint"] }, "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA=="],
+
+ "@eslint/object-schema": ["@eslint/object-schema@3.0.4", "", {}, "sha512-55lO/7+Yp0ISKRP0PsPtNTeNGapXaO085aELZmWCVc5SH3jfrqpuU6YgOdIxMS99ZHkQN1cXKE+cdIqwww9ptw=="],
+
+ "@eslint/plugin-kit": ["@eslint/plugin-kit@0.7.0", "", { "dependencies": { "@eslint/core": "^1.2.0", "levn": "^0.4.1" } }, "sha512-ejvBr8MQCbVsWNZnCwDXjUKq40MDmHalq7cJ6e9s/qzTUFIIo/afzt1Vui9T97FM/V/pN4YsFVoed5NIa96RDg=="],
+
+ "@floating-ui/core": ["@floating-ui/core@1.7.5", "", { "dependencies": { "@floating-ui/utils": "^0.2.11" } }, "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ=="],
+
+ "@floating-ui/dom": ["@floating-ui/dom@1.7.6", "", { "dependencies": { "@floating-ui/core": "^1.7.5", "@floating-ui/utils": "^0.2.11" } }, "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ=="],
+
+ "@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.8", "", { "dependencies": { "@floating-ui/dom": "^1.7.6" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A=="],
+
+ "@floating-ui/utils": ["@floating-ui/utils@0.2.11", "", {}, "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg=="],
+
+ "@hookform/resolvers": ["@hookform/resolvers@5.2.2", "", { "dependencies": { "@standard-schema/utils": "^0.3.0" }, "peerDependencies": { "react-hook-form": "^7.55.0" } }, "sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA=="],
+
+ "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
+
+ "@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="],
+
+ "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="],
+
+ "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="],
+
+ "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
+
+ "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
+
+ "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
+
+ "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
+
+ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
+
+ "@microsoft/fetch-event-source": ["@microsoft/fetch-event-source@2.0.1", "", {}, "sha512-W6CLUJ2eBMw3Rec70qrsEW0jOm/3twwJv21mrmj2yORiaVmVYGS4sSS5yUwvQc1ZlDLYGPnClVWmUUMagKNsfA=="],
+
+ "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.2", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw=="],
+
+ "@oxc-project/types": ["@oxc-project/types@0.122.0", "", {}, "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA=="],
+
+ "@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="],
+
+ "@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="],
+
+ "@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="],
+
+ "@radix-ui/react-alert-dialog": ["@radix-ui/react-alert-dialog@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dialog": "1.1.15", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw=="],
+
+ "@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w=="],
+
+ "@radix-ui/react-avatar": ["@radix-ui/react-avatar@1.1.11", "", { "dependencies": { "@radix-ui/react-context": "1.1.3", "@radix-ui/react-primitive": "2.1.4", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0Qk603AHGV28BOBO34p7IgD5m+V5Sg/YovfayABkoDDBM5d3NCx0Mp4gGrjzLGes1jV5eNOE1r3itqOR33VC6Q=="],
+
+ "@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw=="],
+
+ "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
+
+ "@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
+
+ "@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw=="],
+
+ "@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="],
+
+ "@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg=="],
+
+ "@radix-ui/react-dropdown-menu": ["@radix-ui/react-dropdown-menu@2.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw=="],
+
+ "@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw=="],
+
+ "@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="],
+
+ "@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="],
+
+ "@radix-ui/react-label": ["@radix-ui/react-label@2.1.8", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A=="],
+
+ "@radix-ui/react-menu": ["@radix-ui/react-menu@2.1.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg=="],
+
+ "@radix-ui/react-popover": ["@radix-ui/react-popover@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA=="],
+
+ "@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.8", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw=="],
+
+ "@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ=="],
+
+ "@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="],
+
+ "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
+
+ "@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA=="],
+
+ "@radix-ui/react-scroll-area": ["@radix-ui/react-scroll-area@1.2.10", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A=="],
+
+ "@radix-ui/react-select": ["@radix-ui/react-select@2.2.6", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ=="],
+
+ "@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.8", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g=="],
+
+ "@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="],
+
+ "@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.13", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A=="],
+
+ "@radix-ui/react-toggle": ["@radix-ui/react-toggle@1.1.10", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ=="],
+
+ "@radix-ui/react-toggle-group": ["@radix-ui/react-toggle-group@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-toggle": "1.1.10", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q=="],
+
+ "@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.2.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg=="],
+
+ "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="],
+
+ "@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="],
+
+ "@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA=="],
+
+ "@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.1", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g=="],
+
+ "@radix-ui/react-use-is-hydrated": ["@radix-ui/react-use-is-hydrated@0.1.0", "", { "dependencies": { "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA=="],
+
+ "@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="],
+
+ "@radix-ui/react-use-previous": ["@radix-ui/react-use-previous@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ=="],
+
+ "@radix-ui/react-use-rect": ["@radix-ui/react-use-rect@1.1.1", "", { "dependencies": { "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w=="],
+
+ "@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ=="],
+
+ "@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.2.3", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug=="],
+
+ "@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="],
+
+ "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.12", "", { "os": "android", "cpu": "arm64" }, "sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA=="],
+
+ "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-rc.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg=="],
+
+ "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-rc.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw=="],
+
+ "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-rc.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q=="],
+
+ "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.12", "", { "os": "linux", "cpu": "arm" }, "sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q=="],
+
+ "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-rc.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg=="],
+
+ "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-rc.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw=="],
+
+ "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g=="],
+
+ "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.0-rc.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og=="],
+
+ "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-rc.12", "", { "os": "linux", "cpu": "x64" }, "sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg=="],
+
+ "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-rc.12", "", { "os": "linux", "cpu": "x64" }, "sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig=="],
+
+ "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-rc.12", "", { "os": "none", "cpu": "arm64" }, "sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA=="],
+
+ "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-rc.12", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.1.1" }, "cpu": "none" }, "sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg=="],
+
+ "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-rc.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q=="],
+
+ "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-rc.12", "", { "os": "win32", "cpu": "x64" }, "sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw=="],
+
+ "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.7", "", {}, "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA=="],
+
+ "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
+
+ "@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="],
+
+ "@swc/core": ["@swc/core@1.15.24", "", { "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.26" }, "optionalDependencies": { "@swc/core-darwin-arm64": "1.15.24", "@swc/core-darwin-x64": "1.15.24", "@swc/core-linux-arm-gnueabihf": "1.15.24", "@swc/core-linux-arm64-gnu": "1.15.24", "@swc/core-linux-arm64-musl": "1.15.24", "@swc/core-linux-ppc64-gnu": "1.15.24", "@swc/core-linux-s390x-gnu": "1.15.24", "@swc/core-linux-x64-gnu": "1.15.24", "@swc/core-linux-x64-musl": "1.15.24", "@swc/core-win32-arm64-msvc": "1.15.24", "@swc/core-win32-ia32-msvc": "1.15.24", "@swc/core-win32-x64-msvc": "1.15.24" }, "peerDependencies": { "@swc/helpers": ">=0.5.17" }, "optionalPeers": ["@swc/helpers"] }, "sha512-5Hj8aNasue7yusUt8LGCUe/AjM7RMAce8ZoyDyiFwx7Al+GbYKL+yE7g4sJk8vEr1dKIkTRARkNIJENc4CjkBQ=="],
+
+ "@swc/core-darwin-arm64": ["@swc/core-darwin-arm64@1.15.24", "", { "os": "darwin", "cpu": "arm64" }, "sha512-uM5ZGfFXjtvtJ+fe448PVBEbn/CSxS3UAyLj3O9xOqKIWy3S6hPTXSPbszxkSsGDYKi+YFhzAsR4r/eXLxEQ0g=="],
+
+ "@swc/core-darwin-x64": ["@swc/core-darwin-x64@1.15.24", "", { "os": "darwin", "cpu": "x64" }, "sha512-fMIb/Zfn929pw25VMBhV7Ji2Dl+lCWtUPNdYJQYOke+00E5fcQ9ynxtP8+qhUo/HZc+mYQb1gJxwHM9vty+lXg=="],
+
+ "@swc/core-linux-arm-gnueabihf": ["@swc/core-linux-arm-gnueabihf@1.15.24", "", { "os": "linux", "cpu": "arm" }, "sha512-vOkjsyjjxnoYx3hMEWcGxQrMgnNrRm6WAegBXrN8foHtDAR+zpdhpGF5a4lj1bNPgXAvmysjui8cM1ov/Clkaw=="],
+
+ "@swc/core-linux-arm64-gnu": ["@swc/core-linux-arm64-gnu@1.15.24", "", { "os": "linux", "cpu": "arm64" }, "sha512-h/oNu+upkXJ6Cicnq7YGVj9PkdfarLCdQa8l/FlHYvfv8CEiMaeeTnpLU7gSBH/rGxosM6Qkfa/J9mThGF9CLA=="],
+
+ "@swc/core-linux-arm64-musl": ["@swc/core-linux-arm64-musl@1.15.24", "", { "os": "linux", "cpu": "arm64" }, "sha512-ZpF/pRe1guk6sKzQI9D1jAORtjTdNlyeXn9GDz8ophof/w2WhojRblvSDJaGe7rJjcPN8AaOkhwdRUh7q8oYIg=="],
+
+ "@swc/core-linux-ppc64-gnu": ["@swc/core-linux-ppc64-gnu@1.15.24", "", { "os": "linux", "cpu": "ppc64" }, "sha512-QZEsZfisHTSJlmyChgDFNmKPb3W6Lhbfo/O76HhIngfEdnQNmukS38/VSe1feho+xkV5A5hETyCbx3sALBZKAQ=="],
+
+ "@swc/core-linux-s390x-gnu": ["@swc/core-linux-s390x-gnu@1.15.24", "", { "os": "linux", "cpu": "s390x" }, "sha512-DLdJKVsJgglqQrJBuoUYNmzm3leI7kUZhLbZGHv42onfKsGf6JDS3+bzCUQfte/XOqDjh/tmmn1DR/CF/tCJFw=="],
+
+ "@swc/core-linux-x64-gnu": ["@swc/core-linux-x64-gnu@1.15.24", "", { "os": "linux", "cpu": "x64" }, "sha512-IpLYfposPA/XLxYOKpRfeccl1p5dDa3+okZDHHTchBkXEaVCnq5MADPmIWwIYj1tudt7hORsEHccG5no6IUQRw=="],
+
+ "@swc/core-linux-x64-musl": ["@swc/core-linux-x64-musl@1.15.24", "", { "os": "linux", "cpu": "x64" }, "sha512-JHy3fMSc0t/EPWgo74+OK5TGr51aElnzqfUPaiRf2qJ/BfX5CUCfMiWVBuhI7qmVMBnk1jTRnL/xZnOSHDPLYg=="],
+
+ "@swc/core-win32-arm64-msvc": ["@swc/core-win32-arm64-msvc@1.15.24", "", { "os": "win32", "cpu": "arm64" }, "sha512-Txj+qUH1z2bUd1P3JvwByfjKFti3cptlAxhWgmunBUUxy/IW3CXLZ6l6Gk4liANadKkU71nIU1X30Z5vpMT3BA=="],
+
+ "@swc/core-win32-ia32-msvc": ["@swc/core-win32-ia32-msvc@1.15.24", "", { "os": "win32", "cpu": "ia32" }, "sha512-15D/nl3XwrhFpMv+MADFOiVwv3FvH9j8c6Rf8EXBT3Q5LoMh8YnDnSgPYqw1JzPnksvsBX6QPXLiPqmcR/Z4qQ=="],
+
+ "@swc/core-win32-x64-msvc": ["@swc/core-win32-x64-msvc@1.15.24", "", { "os": "win32", "cpu": "x64" }, "sha512-PR0PlTlPra2JbaDphrOAzm6s0v9rA0F17YzB+XbWD95B4g2cWcZY9LAeTa4xll70VLw9Jr7xBrlohqlQmelMFQ=="],
+
+ "@swc/counter": ["@swc/counter@0.1.3", "", {}, "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ=="],
+
+ "@swc/types": ["@swc/types@0.1.26", "", { "dependencies": { "@swc/counter": "^0.1.3" } }, "sha512-lyMwd7WGgG79RS7EERZV3T8wMdmPq3xwyg+1nmAM64kIhx5yl+juO2PYIHb7vTiPgPCj8LYjsNV2T5wiQHUEaw=="],
+
+ "@tailwindcss/node": ["@tailwindcss/node@4.2.2", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.2.2" } }, "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA=="],
+
+ "@tailwindcss/oxide": ["@tailwindcss/oxide@4.2.2", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.2.2", "@tailwindcss/oxide-darwin-arm64": "4.2.2", "@tailwindcss/oxide-darwin-x64": "4.2.2", "@tailwindcss/oxide-freebsd-x64": "4.2.2", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.2", "@tailwindcss/oxide-linux-arm64-gnu": "4.2.2", "@tailwindcss/oxide-linux-arm64-musl": "4.2.2", "@tailwindcss/oxide-linux-x64-gnu": "4.2.2", "@tailwindcss/oxide-linux-x64-musl": "4.2.2", "@tailwindcss/oxide-wasm32-wasi": "4.2.2", "@tailwindcss/oxide-win32-arm64-msvc": "4.2.2", "@tailwindcss/oxide-win32-x64-msvc": "4.2.2" } }, "sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg=="],
+
+ "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.2.2", "", { "os": "android", "cpu": "arm64" }, "sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg=="],
+
+ "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.2.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg=="],
+
+ "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.2.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw=="],
+
+ "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.2.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ=="],
+
+ "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.2.2", "", { "os": "linux", "cpu": "arm" }, "sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ=="],
+
+ "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.2.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw=="],
+
+ "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.2.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag=="],
+
+ "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.2.2", "", { "os": "linux", "cpu": "x64" }, "sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg=="],
+
+ "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.2.2", "", { "os": "linux", "cpu": "x64" }, "sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ=="],
+
+ "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.2.2", "", { "dependencies": { "@emnapi/core": "^1.8.1", "@emnapi/runtime": "^1.8.1", "@emnapi/wasi-threads": "^1.1.0", "@napi-rs/wasm-runtime": "^1.1.1", "@tybys/wasm-util": "^0.10.1", "tslib": "^2.8.1" }, "cpu": "none" }, "sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q=="],
+
+ "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.2.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ=="],
+
+ "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.2.2", "", { "os": "win32", "cpu": "x64" }, "sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA=="],
+
+ "@tailwindcss/postcss": ["@tailwindcss/postcss@4.2.2", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.2.2", "@tailwindcss/oxide": "4.2.2", "postcss": "^8.5.6", "tailwindcss": "4.2.2" } }, "sha512-n4goKQbW8RVXIbNKRB/45LzyUqN451deQK0nzIeauVEqjlI49slUlgKYJM2QyUzap/PcpnS7kzSUmPb1sCRvYQ=="],
+
+ "@tanstack/query-core": ["@tanstack/query-core@5.96.2", "", {}, "sha512-hzI6cTVh4KNRk8UtoIBS7Lv9g6BnJPXvBKsvYH1aGWvv0347jT3BnSvztOE+kD76XGvZnRC/t6qdW1CaIfwCeA=="],
+
+ "@tanstack/react-query": ["@tanstack/react-query@5.96.2", "", { "dependencies": { "@tanstack/query-core": "5.96.2" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-sYyzzJT4G0g02azzJ8o55VFFV31XvFpdUpG+unxS0vSaYsJnSPKGoI6WdPwUucJL1wpgGfwfmntNX/Ub1uOViA=="],
+
+ "@testing-library/dom": ["@testing-library/dom@10.4.1", "", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "picocolors": "1.1.1", "pretty-format": "^27.0.2" } }, "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg=="],
+
+ "@testing-library/jest-dom": ["@testing-library/jest-dom@6.9.1", "", { "dependencies": { "@adobe/css-tools": "^4.4.0", "aria-query": "^5.0.0", "css.escape": "^1.5.1", "dom-accessibility-api": "^0.6.3", "picocolors": "^1.1.1", "redent": "^3.0.0" } }, "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA=="],
+
+ "@testing-library/react": ["@testing-library/react@16.3.2", "", { "dependencies": { "@babel/runtime": "^7.12.5" }, "peerDependencies": { "@testing-library/dom": "^10.0.0", "@types/react": "^18.0.0 || ^19.0.0", "@types/react-dom": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g=="],
+
+ "@testing-library/user-event": ["@testing-library/user-event@14.6.1", "", { "peerDependencies": { "@testing-library/dom": ">=7.21.4" } }, "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw=="],
+
+ "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
+
+ "@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="],
+
+ "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="],
+
+ "@types/debug": ["@types/debug@4.1.13", "", { "dependencies": { "@types/ms": "*" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="],
+
+ "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="],
+
+ "@types/esrecurse": ["@types/esrecurse@4.3.1", "", {}, "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw=="],
+
+ "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
+
+ "@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="],
+
+ "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="],
+
+ "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
+
+ "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],
+
+ "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
+
+ "@types/node": ["@types/node@25.5.2", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg=="],
+
+ "@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="],
+
+ "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
+
+ "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
+
+ "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.58.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.58.0", "@typescript-eslint/type-utils": "8.58.0", "@typescript-eslint/utils": "8.58.0", "@typescript-eslint/visitor-keys": "8.58.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.58.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-RLkVSiNuUP1C2ROIWfqX+YcUfLaSnxGE/8M+Y57lopVwg9VTYYfhuz15Yf1IzCKgZj6/rIbYTmJCUSqr76r0Wg=="],
+
+ "@typescript-eslint/parser": ["@typescript-eslint/parser@8.58.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.58.0", "@typescript-eslint/types": "8.58.0", "@typescript-eslint/typescript-estree": "8.58.0", "@typescript-eslint/visitor-keys": "8.58.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-rLoGZIf9afaRBYsPUMtvkDWykwXwUPL60HebR4JgTI8mxfFe2cQTu3AGitANp4b9B2QlVru6WzjgB2IzJKiCSA=="],
+
+ "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.58.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.58.0", "@typescript-eslint/types": "^8.58.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-8Q/wBPWLQP1j16NxoPNIKpDZFMaxl7yWIoqXWYeWO+Bbd2mjgvoF0dxP2jKZg5+x49rgKdf7Ck473M8PC3V9lg=="],
+
+ "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.58.0", "", { "dependencies": { "@typescript-eslint/types": "8.58.0", "@typescript-eslint/visitor-keys": "8.58.0" } }, "sha512-W1Lur1oF50FxSnNdGp3Vs6P+yBRSmZiw4IIjEeYxd8UQJwhUF0gDgDD/W/Tgmh73mxgEU3qX0Bzdl/NGuSPEpQ=="],
+
+ "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.58.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-doNSZEVJsWEu4htiVC+PR6NpM+pa+a4ClH9INRWOWCUzMst/VA9c4gXq92F8GUD1rwhNvRLkgjfYtFXegXQF7A=="],
+
+ "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.58.0", "", { "dependencies": { "@typescript-eslint/types": "8.58.0", "@typescript-eslint/typescript-estree": "8.58.0", "@typescript-eslint/utils": "8.58.0", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-aGsCQImkDIqMyx1u4PrVlbi/krmDsQUs4zAcCV6M7yPcPev+RqVlndsJy9kJ8TLihW9TZ0kbDAzctpLn5o+lOg=="],
+
+ "@typescript-eslint/types": ["@typescript-eslint/types@8.58.0", "", {}, "sha512-O9CjxypDT89fbHxRfETNoAnHj/i6IpRK0CvbVN3qibxlLdo5p5hcLmUuCCrHMpxiWSwKyI8mCP7qRNYuOJ0Uww=="],
+
+ "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.58.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.58.0", "@typescript-eslint/tsconfig-utils": "8.58.0", "@typescript-eslint/types": "8.58.0", "@typescript-eslint/visitor-keys": "8.58.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-7vv5UWbHqew/dvs+D3e1RvLv1v2eeZ9txRHPnEEBUgSNLx5ghdzjHa0sgLWYVKssH+lYmV0JaWdoubo0ncGYLA=="],
+
+ "@typescript-eslint/utils": ["@typescript-eslint/utils@8.58.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.58.0", "@typescript-eslint/types": "8.58.0", "@typescript-eslint/typescript-estree": "8.58.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-RfeSqcFeHMHlAWzt4TBjWOAtoW9lnsAGiP3GbaX9uVgTYYrMbVnGONEfUCiSss+xMHFl+eHZiipmA8WkQ7FuNA=="],
+
+ "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.58.0", "", { "dependencies": { "@typescript-eslint/types": "8.58.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-XJ9UD9+bbDo4a4epraTwG3TsNPeiB9aShrUneAVXy8q4LuwowN+qu89/6ByLMINqvIMeI9H9hOHQtg/ijrYXzQ=="],
+
+ "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
+
+ "@vitejs/plugin-react-swc": ["@vitejs/plugin-react-swc@4.3.0", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-rc.7", "@swc/core": "^1.15.11" }, "peerDependencies": { "vite": "^4 || ^5 || ^6 || ^7 || ^8" } }, "sha512-mOkXCII839dHyAt/gpoSlm28JIVDwhZ6tnG6wJxUy2bmOx7UaPjvOyIDf3SFv5s7Eo7HVaq6kRcu6YMEzt5Z7w=="],
+
+ "@vitest/coverage-v8": ["@vitest/coverage-v8@4.1.2", "", { "dependencies": { "@bcoe/v8-coverage": "^1.0.2", "@vitest/utils": "4.1.2", "ast-v8-to-istanbul": "^1.0.0", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-reports": "^3.2.0", "magicast": "^0.5.2", "obug": "^2.1.1", "std-env": "^4.0.0-rc.1", "tinyrainbow": "^3.1.0" }, "peerDependencies": { "@vitest/browser": "4.1.2", "vitest": "4.1.2" }, "optionalPeers": ["@vitest/browser"] }, "sha512-sPK//PHO+kAkScb8XITeB1bf7fsk85Km7+rt4eeuRR3VS1/crD47cmV5wicisJmjNdfeokTZwjMk4Mj2d58Mgg=="],
+
+ "@vitest/expect": ["@vitest/expect@4.1.2", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.2", "@vitest/utils": "4.1.2", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-gbu+7B0YgUJ2nkdsRJrFFW6X7NTP44WlhiclHniUhxADQJH5Szt9mZ9hWnJPJ8YwOK5zUOSSlSvyzRf0u1DSBQ=="],
+
+ "@vitest/mocker": ["@vitest/mocker@4.1.2", "", { "dependencies": { "@vitest/spy": "4.1.2", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-Ize4iQtEALHDttPRCmN+FKqOl2vxTiNUhzobQFFt/BM1lRUTG7zRCLOykG/6Vo4E4hnUdfVLo5/eqKPukcWW7Q=="],
+
+ "@vitest/pretty-format": ["@vitest/pretty-format@4.1.2", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-dwQga8aejqeuB+TvXCMzSQemvV9hNEtDDpgUKDzOmNQayl2OG241PSWeJwKRH3CiC+sESrmoFd49rfnq7T4RnA=="],
+
+ "@vitest/runner": ["@vitest/runner@4.1.2", "", { "dependencies": { "@vitest/utils": "4.1.2", "pathe": "^2.0.3" } }, "sha512-Gr+FQan34CdiYAwpGJmQG8PgkyFVmARK8/xSijia3eTFgVfpcpztWLuP6FttGNfPLJhaZVP/euvujeNYar36OQ=="],
+
+ "@vitest/snapshot": ["@vitest/snapshot@4.1.2", "", { "dependencies": { "@vitest/pretty-format": "4.1.2", "@vitest/utils": "4.1.2", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-g7yfUmxYS4mNxk31qbOYsSt2F4m1E02LFqO53Xpzg3zKMhLAPZAjjfyl9e6z7HrW6LvUdTwAQR3HHfLjpko16A=="],
+
+ "@vitest/spy": ["@vitest/spy@4.1.2", "", {}, "sha512-DU4fBnbVCJGNBwVA6xSToNXrkZNSiw59H8tcuUspVMsBDBST4nfvsPsEHDHGtWRRnqBERBQu7TrTKskmjqTXKA=="],
+
+ "@vitest/ui": ["@vitest/ui@4.1.2", "", { "dependencies": { "@vitest/utils": "4.1.2", "fflate": "^0.8.2", "flatted": "^3.4.2", "pathe": "^2.0.3", "sirv": "^3.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0" }, "peerDependencies": { "vitest": "4.1.2" } }, "sha512-/irhyeAcKS2u6Zokagf9tqZJ0t8S6kMZq4ZG9BHZv7I+fkRrYfQX4w7geYeC2r6obThz39PDxvXQzZX+qXqGeg=="],
+
+ "@vitest/utils": ["@vitest/utils@4.1.2", "", { "dependencies": { "@vitest/pretty-format": "4.1.2", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-xw2/TiX82lQHA06cgbqRKFb5lCAy3axQ4H4SoUFhUsg+wztiet+co86IAMDtF6Vm1hc7J6j09oh/rgDn+JdKIQ=="],
+
+ "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
+
+ "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
+
+ "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
+
+ "ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="],
+
+ "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
+
+ "ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="],
+
+ "aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="],
+
+ "aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="],
+
+ "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="],
+
+ "ast-v8-to-istanbul": ["ast-v8-to-istanbul@1.0.0", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.31", "estree-walker": "^3.0.3", "js-tokens": "^10.0.0" } }, "sha512-1fSfIwuDICFA4LKkCzRPO7F0hzFf0B7+Xqrl27ynQaa+Rh0e1Es0v6kWHPott3lU10AyAr7oKHa65OppjLn3Rg=="],
+
+ "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
+
+ "autoprefixer": ["autoprefixer@10.4.27", "", { "dependencies": { "browserslist": "^4.28.1", "caniuse-lite": "^1.0.30001774", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA=="],
+
+ "axios": ["axios@1.14.0", "", { "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", "proxy-from-env": "^2.1.0" } }, "sha512-3Y8yrqLSwjuzpXuZ0oIYZ/XGgLwUIBU3uLvbcpb0pidD9ctpShJd43KSlEEkVQg6DS0G9NKyzOvBfUtDKEyHvQ=="],
+
+ "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="],
+
+ "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
+
+ "baseline-browser-mapping": ["baseline-browser-mapping@2.10.15", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-1nfKCq9wuAZFTkA2ey/3OXXx7GzFjLdkTiFVNwlJ9WqdI706CZRIhEqjuwanjMIja+84jDLa9rcyZDPDiVkASQ=="],
+
+ "brace-expansion": ["brace-expansion@5.0.5", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="],
+
+ "browserslist": ["browserslist@4.28.2", "", { "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", "electron-to-chromium": "^1.5.328", "node-releases": "^2.0.36", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="],
+
+ "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
+
+ "caniuse-lite": ["caniuse-lite@1.0.30001785", "", {}, "sha512-blhOL/WNR+Km1RI/LCVAvA73xplXA7ZbjzI4YkMK9pa6T/P3F2GxjNpEkyw5repTw9IvkyrjyHpwjnhZ5FOvYQ=="],
+
+ "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
+
+ "chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="],
+
+ "character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="],
+
+ "character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="],
+
+ "character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="],
+
+ "character-reference-invalid": ["character-reference-invalid@2.0.1", "", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="],
+
+ "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="],
+
+ "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
+
+ "cmdk": ["cmdk@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "^1.1.1", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-id": "^1.1.0", "@radix-ui/react-primitive": "^2.0.2" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg=="],
+
+ "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
+
+ "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="],
+
+ "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
+
+ "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
+
+ "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=="],
+
+ "cssstyle": ["cssstyle@4.6.0", "", { "dependencies": { "@asamuzakjp/css-color": "^3.2.0", "rrweb-cssom": "^0.8.0" } }, "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg=="],
+
+ "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
+
+ "data-urls": ["data-urls@5.0.0", "", { "dependencies": { "whatwg-mimetype": "^4.0.0", "whatwg-url": "^14.0.0" } }, "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg=="],
+
+ "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
+
+ "decimal.js": ["decimal.js@10.6.0", "", {}, "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg=="],
+
+ "decode-named-character-reference": ["decode-named-character-reference@1.3.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q=="],
+
+ "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
+
+ "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
+
+ "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
+
+ "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
+
+ "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="],
+
+ "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="],
+
+ "dom-accessibility-api": ["dom-accessibility-api@0.6.3", "", {}, "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w=="],
+
+ "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
+
+ "electron-to-chromium": ["electron-to-chromium@1.5.331", "", {}, "sha512-IbxXrsTlD3hRodkLnbxAPP4OuJYdWCeM3IOdT+CpcMoIwIoDfCmRpEtSPfwBXxVkg9xmBeY7Lz2Eo2TDn/HC3Q=="],
+
+ "enhanced-resolve": ["enhanced-resolve@5.20.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA=="],
+
+ "entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
+
+ "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
+
+ "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
+
+ "es-module-lexer": ["es-module-lexer@2.0.0", "", {}, "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw=="],
+
+ "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
+
+ "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
+
+ "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
+
+ "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
+
+ "eslint": ["eslint@10.2.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.4", "@eslint/config-helpers": "^0.5.4", "@eslint/core": "^1.2.0", "@eslint/plugin-kit": "^0.7.0", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.2.4", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-+L0vBFYGIpSNIt/KWTpFonPrqYvgKw1eUI5Vn7mEogrQcWtWYtNQ7dNqC+px/J0idT3BAkiWrhfS7k+Tum8TUA=="],
+
+ "eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@7.0.1", "", { "dependencies": { "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", "hermes-parser": "^0.25.1", "zod": "^3.25.0 || ^4.0.0", "zod-validation-error": "^3.5.0 || ^4.0.0" }, "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA=="],
+
+ "eslint-plugin-react-refresh": ["eslint-plugin-react-refresh@0.5.2", "", { "peerDependencies": { "eslint": "^9 || ^10" } }, "sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA=="],
+
+ "eslint-scope": ["eslint-scope@9.1.2", "", { "dependencies": { "@types/esrecurse": "^4.3.1", "@types/estree": "^1.0.8", "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ=="],
+
+ "eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="],
+
+ "espree": ["espree@11.2.0", "", { "dependencies": { "acorn": "^8.16.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^5.0.1" } }, "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw=="],
+
+ "esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="],
+
+ "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="],
+
+ "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
+
+ "estree-util-is-identifier-name": ["estree-util-is-identifier-name@3.0.0", "", {}, "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg=="],
+
+ "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
+
+ "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
+
+ "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="],
+
+ "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
+
+ "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
+
+ "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
+
+ "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
+
+ "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
+
+ "fflate": ["fflate@0.8.2", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="],
+
+ "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
+
+ "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
+
+ "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="],
+
+ "flatted": ["flatted@3.4.2", "", {}, "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA=="],
+
+ "follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="],
+
+ "form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="],
+
+ "fraction.js": ["fraction.js@5.3.4", "", {}, "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ=="],
+
+ "framer-motion": ["framer-motion@12.38.0", "", { "dependencies": { "motion-dom": "^12.38.0", "motion-utils": "^12.36.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g=="],
+
+ "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
+
+ "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
+
+ "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
+
+ "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
+
+ "get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="],
+
+ "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
+
+ "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
+
+ "globals": ["globals@17.4.0", "", {}, "sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw=="],
+
+ "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
+
+ "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=="],
+
+ "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
+
+ "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
+
+ "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
+
+ "hast-util-to-jsx-runtime": ["hast-util-to-jsx-runtime@2.3.6", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "vfile-message": "^4.0.0" } }, "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg=="],
+
+ "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="],
+
+ "hermes-estree": ["hermes-estree@0.25.1", "", {}, "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw=="],
+
+ "hermes-parser": ["hermes-parser@0.25.1", "", { "dependencies": { "hermes-estree": "0.25.1" } }, "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA=="],
+
+ "html-encoding-sniffer": ["html-encoding-sniffer@4.0.0", "", { "dependencies": { "whatwg-encoding": "^3.1.1" } }, "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ=="],
+
+ "html-escaper": ["html-escaper@2.0.2", "", {}, "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg=="],
+
+ "html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="],
+
+ "http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="],
+
+ "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
+
+ "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
+
+ "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
+
+ "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
+
+ "indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="],
+
+ "inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="],
+
+ "is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="],
+
+ "is-alphanumerical": ["is-alphanumerical@2.0.1", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw=="],
+
+ "is-decimal": ["is-decimal@2.0.1", "", {}, "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A=="],
+
+ "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
+
+ "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
+
+ "is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="],
+
+ "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
+
+ "is-potential-custom-element-name": ["is-potential-custom-element-name@1.0.1", "", {}, "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="],
+
+ "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-reports": ["istanbul-reports@3.2.0", "", { "dependencies": { "html-escaper": "^2.0.0", "istanbul-lib-report": "^3.0.0" } }, "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA=="],
+
+ "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
+
+ "js-tokens": ["js-tokens@10.0.0", "", {}, "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q=="],
+
+ "jsdom": ["jsdom@25.0.1", "", { "dependencies": { "cssstyle": "^4.1.0", "data-urls": "^5.0.0", "decimal.js": "^10.4.3", "form-data": "^4.0.0", "html-encoding-sniffer": "^4.0.0", "http-proxy-agent": "^7.0.2", "https-proxy-agent": "^7.0.5", "is-potential-custom-element-name": "^1.0.1", "nwsapi": "^2.2.12", "parse5": "^7.1.2", "rrweb-cssom": "^0.7.1", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^5.0.0", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^7.0.0", "whatwg-encoding": "^3.1.1", "whatwg-mimetype": "^4.0.0", "whatwg-url": "^14.0.0", "ws": "^8.18.0", "xml-name-validator": "^5.0.0" }, "peerDependencies": { "canvas": "^2.11.2" }, "optionalPeers": ["canvas"] }, "sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw=="],
+
+ "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
+
+ "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
+
+ "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
+
+ "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
+
+ "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
+
+ "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
+
+ "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
+
+ "lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
+
+ "lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
+
+ "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="],
+
+ "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="],
+
+ "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="],
+
+ "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="],
+
+ "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="],
+
+ "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="],
+
+ "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="],
+
+ "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="],
+
+ "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="],
+
+ "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
+
+ "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
+
+ "longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="],
+
+ "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
+
+ "lucide-react": ["lucide-react@1.7.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-yI7BeItCLZJTXikmK4KNUGCKoGzSvbKlfCvw44bU4fXAL6v3gYS4uHD1jzsLkfwODYwI6Drw5Tu9Z5ulDe0TSg=="],
+
+ "lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="],
+
+ "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
+
+ "magicast": ["magicast@0.5.2", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "source-map-js": "^1.2.1" } }, "sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ=="],
+
+ "make-dir": ["make-dir@4.0.0", "", { "dependencies": { "semver": "^7.5.3" } }, "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw=="],
+
+ "markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="],
+
+ "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
+
+ "mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="],
+
+ "mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.3", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q=="],
+
+ "mdast-util-gfm": ["mdast-util-gfm@3.1.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ=="],
+
+ "mdast-util-gfm-autolink-literal": ["mdast-util-gfm-autolink-literal@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "ccount": "^2.0.0", "devlop": "^1.0.0", "mdast-util-find-and-replace": "^3.0.0", "micromark-util-character": "^2.0.0" } }, "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ=="],
+
+ "mdast-util-gfm-footnote": ["mdast-util-gfm-footnote@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0" } }, "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ=="],
+
+ "mdast-util-gfm-strikethrough": ["mdast-util-gfm-strikethrough@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg=="],
+
+ "mdast-util-gfm-table": ["mdast-util-gfm-table@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "markdown-table": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg=="],
+
+ "mdast-util-gfm-task-list-item": ["mdast-util-gfm-task-list-item@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ=="],
+
+ "mdast-util-mdx-expression": ["mdast-util-mdx-expression@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ=="],
+
+ "mdast-util-mdx-jsx": ["mdast-util-mdx-jsx@3.2.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", "unist-util-stringify-position": "^4.0.0", "vfile-message": "^4.0.0" } }, "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q=="],
+
+ "mdast-util-mdxjs-esm": ["mdast-util-mdxjs-esm@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg=="],
+
+ "mdast-util-phrasing": ["mdast-util-phrasing@4.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "unist-util-is": "^6.0.0" } }, "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w=="],
+
+ "mdast-util-to-hast": ["mdast-util-to-hast@13.2.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA=="],
+
+ "mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA=="],
+
+ "mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="],
+
+ "micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="],
+
+ "micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="],
+
+ "micromark-extension-gfm": ["micromark-extension-gfm@3.0.0", "", { "dependencies": { "micromark-extension-gfm-autolink-literal": "^2.0.0", "micromark-extension-gfm-footnote": "^2.0.0", "micromark-extension-gfm-strikethrough": "^2.0.0", "micromark-extension-gfm-table": "^2.0.0", "micromark-extension-gfm-tagfilter": "^2.0.0", "micromark-extension-gfm-task-list-item": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w=="],
+
+ "micromark-extension-gfm-autolink-literal": ["micromark-extension-gfm-autolink-literal@2.1.0", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw=="],
+
+ "micromark-extension-gfm-footnote": ["micromark-extension-gfm-footnote@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw=="],
+
+ "micromark-extension-gfm-strikethrough": ["micromark-extension-gfm-strikethrough@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw=="],
+
+ "micromark-extension-gfm-table": ["micromark-extension-gfm-table@2.1.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg=="],
+
+ "micromark-extension-gfm-tagfilter": ["micromark-extension-gfm-tagfilter@2.0.0", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg=="],
+
+ "micromark-extension-gfm-task-list-item": ["micromark-extension-gfm-task-list-item@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw=="],
+
+ "micromark-factory-destination": ["micromark-factory-destination@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA=="],
+
+ "micromark-factory-label": ["micromark-factory-label@2.0.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg=="],
+
+ "micromark-factory-space": ["micromark-factory-space@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg=="],
+
+ "micromark-factory-title": ["micromark-factory-title@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw=="],
+
+ "micromark-factory-whitespace": ["micromark-factory-whitespace@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ=="],
+
+ "micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="],
+
+ "micromark-util-chunked": ["micromark-util-chunked@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA=="],
+
+ "micromark-util-classify-character": ["micromark-util-classify-character@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q=="],
+
+ "micromark-util-combine-extensions": ["micromark-util-combine-extensions@2.0.1", "", { "dependencies": { "micromark-util-chunked": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg=="],
+
+ "micromark-util-decode-numeric-character-reference": ["micromark-util-decode-numeric-character-reference@2.0.2", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw=="],
+
+ "micromark-util-decode-string": ["micromark-util-decode-string@2.0.1", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ=="],
+
+ "micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="],
+
+ "micromark-util-html-tag-name": ["micromark-util-html-tag-name@2.0.1", "", {}, "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA=="],
+
+ "micromark-util-normalize-identifier": ["micromark-util-normalize-identifier@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q=="],
+
+ "micromark-util-resolve-all": ["micromark-util-resolve-all@2.0.1", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg=="],
+
+ "micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="],
+
+ "micromark-util-subtokenize": ["micromark-util-subtokenize@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA=="],
+
+ "micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="],
+
+ "micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="],
+
+ "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
+
+ "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
+
+ "min-indent": ["min-indent@1.0.1", "", {}, "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg=="],
+
+ "minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="],
+
+ "motion-dom": ["motion-dom@12.38.0", "", { "dependencies": { "motion-utils": "^12.36.0" } }, "sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA=="],
+
+ "motion-utils": ["motion-utils@12.36.0", "", {}, "sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg=="],
+
+ "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=="],
+
+ "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
+
+ "node-releases": ["node-releases@2.0.37", "", {}, "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg=="],
+
+ "nwsapi": ["nwsapi@2.2.23", "", {}, "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ=="],
+
+ "obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="],
+
+ "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
+
+ "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
+
+ "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
+
+ "parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="],
+
+ "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="],
+
+ "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
+
+ "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
+
+ "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
+
+ "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
+
+ "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="],
+
+ "postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="],
+
+ "postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="],
+
+ "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
+
+ "prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="],
+
+ "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=="],
+
+ "property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="],
+
+ "proxy-from-env": ["proxy-from-env@2.1.0", "", {}, "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA=="],
+
+ "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
+
+ "react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="],
+
+ "react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="],
+
+ "react-hook-form": ["react-hook-form@7.72.1", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-RhwBoy2ygeVZje+C+bwJ8g0NjTdBmDlJvAUHTxRjTmSUKPYsKfMphkS2sgEMotsY03bP358yEYlnUeZy//D9Ig=="],
+
+ "react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="],
+
+ "react-markdown": ["react-markdown@10.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "html-url-attributes": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "unified": "^11.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" }, "peerDependencies": { "@types/react": ">=18", "react": ">=18" } }, "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ=="],
+
+ "react-remove-scroll": ["react-remove-scroll@2.7.2", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q=="],
+
+ "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="],
+
+ "react-router": ["react-router@7.14.0", "", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-m/xR9N4LQLmAS0ZhkY2nkPA1N7gQ5TUVa5n8TgANuDTARbn1gt+zLPXEm7W0XDTbrQ2AJSJKhoa6yx1D8BcpxQ=="],
+
+ "react-router-dom": ["react-router-dom@7.14.0", "", { "dependencies": { "react-router": "7.14.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, "sha512-2G3ajSVSZMEtmTjIklRWlNvo8wICEpLihfD/0YMDxbWK2UyP5EGfnoIn9AIQGnF3G/FX0MRbHXdFcD+rL1ZreQ=="],
+
+ "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="],
+
+ "redent": ["redent@3.0.0", "", { "dependencies": { "indent-string": "^4.0.0", "strip-indent": "^3.0.0" } }, "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg=="],
+
+ "remark-gfm": ["remark-gfm@4.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="],
+
+ "remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="],
+
+ "remark-rehype": ["remark-rehype@11.1.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "mdast-util-to-hast": "^13.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw=="],
+
+ "remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="],
+
+ "rolldown": ["rolldown@1.0.0-rc.12", "", { "dependencies": { "@oxc-project/types": "=0.122.0", "@rolldown/pluginutils": "1.0.0-rc.12" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.12", "@rolldown/binding-darwin-arm64": "1.0.0-rc.12", "@rolldown/binding-darwin-x64": "1.0.0-rc.12", "@rolldown/binding-freebsd-x64": "1.0.0-rc.12", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.12", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.12", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.12", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.12", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.12", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.12", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.12", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.12", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.12", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.12", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.12" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A=="],
+
+ "rrweb-cssom": ["rrweb-cssom@0.7.1", "", {}, "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg=="],
+
+ "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
+
+ "saxes": ["saxes@6.0.0", "", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA=="],
+
+ "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
+
+ "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
+
+ "set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="],
+
+ "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=="],
+
+ "sirv": ["sirv@3.0.2", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g=="],
+
+ "sonner": ["sonner@2.0.7", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w=="],
+
+ "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
+
+ "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="],
+
+ "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="],
+
+ "std-env": ["std-env@4.0.0", "", {}, "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ=="],
+
+ "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],
+
+ "strip-indent": ["strip-indent@3.0.0", "", { "dependencies": { "min-indent": "^1.0.0" } }, "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ=="],
+
+ "style-to-js": ["style-to-js@1.1.21", "", { "dependencies": { "style-to-object": "1.0.14" } }, "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ=="],
+
+ "style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="],
+
+ "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
+
+ "symbol-tree": ["symbol-tree@3.2.4", "", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="],
+
+ "tailwind-merge": ["tailwind-merge@3.5.0", "", {}, "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A=="],
+
+ "tailwindcss": ["tailwindcss@4.2.2", "", {}, "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q=="],
+
+ "tailwindcss-animate": ["tailwindcss-animate@1.0.7", "", { "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders" } }, "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA=="],
+
+ "tapable": ["tapable@2.3.2", "", {}, "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA=="],
+
+ "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="],
+
+ "tinyexec": ["tinyexec@1.0.4", "", {}, "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw=="],
+
+ "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
+
+ "tinyrainbow": ["tinyrainbow@3.1.0", "", {}, "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw=="],
+
+ "tldts": ["tldts@6.1.86", "", { "dependencies": { "tldts-core": "^6.1.86" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ=="],
+
+ "tldts-core": ["tldts-core@6.1.86", "", {}, "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA=="],
+
+ "totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="],
+
+ "tough-cookie": ["tough-cookie@5.1.2", "", { "dependencies": { "tldts": "^6.1.32" } }, "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A=="],
+
+ "tr46": ["tr46@5.1.1", "", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw=="],
+
+ "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
+
+ "trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="],
+
+ "ts-api-utils": ["ts-api-utils@2.5.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA=="],
+
+ "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
+
+ "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
+
+ "typescript": ["typescript@6.0.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ=="],
+
+ "typescript-eslint": ["typescript-eslint@8.58.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.58.0", "@typescript-eslint/parser": "8.58.0", "@typescript-eslint/typescript-estree": "8.58.0", "@typescript-eslint/utils": "8.58.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-e2TQzKfaI85fO+F3QywtX+tCTsu/D3WW5LVU6nz8hTFKFZ8yBJ6mSYRpXqdR3mFjPWmO0eWsTa5f+UpAOe/FMA=="],
+
+ "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
+
+ "unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="],
+
+ "unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="],
+
+ "unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="],
+
+ "unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="],
+
+ "unist-util-visit": ["unist-util-visit@5.1.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg=="],
+
+ "unist-util-visit-parents": ["unist-util-visit-parents@6.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ=="],
+
+ "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
+
+ "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
+
+ "use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="],
+
+ "use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="],
+
+ "use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="],
+
+ "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
+
+ "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="],
+
+ "vite": ["vite@8.0.3", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.8", "rolldown": "1.0.0-rc.12", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.0", "esbuild": "^0.27.0", "jiti": ">=1.21.0", "less": "^4.0.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", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ=="],
+
+ "vitest": ["vitest@4.1.2", "", { "dependencies": { "@vitest/expect": "4.1.2", "@vitest/mocker": "4.1.2", "@vitest/pretty-format": "4.1.2", "@vitest/runner": "4.1.2", "@vitest/snapshot": "4.1.2", "@vitest/spy": "4.1.2", "@vitest/utils": "4.1.2", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.2", "@vitest/browser-preview": "4.1.2", "@vitest/browser-webdriverio": "4.1.2", "@vitest/ui": "4.1.2", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-xjR1dMTVHlFLh98JE3i/f/WePqJsah4A0FK9cc8Ehp9Udk0AZk6ccpIZhh1qJ/yxVWRZ+Q54ocnD8TXmkhspGg=="],
+
+ "w3c-xmlserializer": ["w3c-xmlserializer@5.0.0", "", { "dependencies": { "xml-name-validator": "^5.0.0" } }, "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA=="],
+
+ "webidl-conversions": ["webidl-conversions@7.0.0", "", {}, "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="],
+
+ "whatwg-encoding": ["whatwg-encoding@3.1.1", "", { "dependencies": { "iconv-lite": "0.6.3" } }, "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ=="],
+
+ "whatwg-mimetype": ["whatwg-mimetype@4.0.0", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="],
+
+ "whatwg-url": ["whatwg-url@14.2.0", "", { "dependencies": { "tr46": "^5.1.0", "webidl-conversions": "^7.0.0" } }, "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw=="],
+
+ "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=="],
+
+ "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
+
+ "ws": ["ws@8.20.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA=="],
+
+ "xml-name-validator": ["xml-name-validator@5.0.0", "", {}, "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg=="],
+
+ "xmlchars": ["xmlchars@2.2.0", "", {}, "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="],
+
+ "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
+
+ "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
+
+ "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
+
+ "zod-validation-error": ["zod-validation-error@4.0.2", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ=="],
+
+ "zustand": ["zustand@5.0.12", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-i77ae3aZq4dhMlRhJVCYgMLKuSiZAaUPAct2AksxQ+gOtimhGMdXljRT21P5BNpeT4kXlLIckvkPM029OljD7g=="],
+
+ "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
+
+ "@asamuzakjp/css-color/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
+
+ "@babel/code-frame/js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
+
+ "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
+
+ "@radix-ui/react-alert-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
+
+ "@radix-ui/react-avatar/@radix-ui/react-context": ["@radix-ui/react-context@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ieIFACdMpYfMEjF0rEf5KLvfVyIkOz6PDGyNnP+u+4xQ6jny3VCgA4OgXOwNx2aUkxn8zx9fiVcM8CfFYv9Lxw=="],
+
+ "@radix-ui/react-avatar/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="],
+
+ "@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
+
+ "@radix-ui/react-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
+
+ "@radix-ui/react-label/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="],
+
+ "@radix-ui/react-menu/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
+
+ "@radix-ui/react-popover/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
+
+ "@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
+
+ "@radix-ui/react-select/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
+
+ "@radix-ui/react-separator/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="],
+
+ "@radix-ui/react-tooltip/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.9.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" }, "bundled": true }, "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.9.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.2", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" }, "bundled": true }, "sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
+
+ "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
+
+ "@testing-library/dom/aria-query": ["aria-query@5.3.0", "", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A=="],
+
+ "@testing-library/dom/dom-accessibility-api": ["dom-accessibility-api@0.5.16", "", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="],
+
+ "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
+
+ "@typescript-eslint/typescript-estree/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
+
+ "cmdk/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="],
+
+ "cssstyle/rrweb-cssom": ["rrweb-cssom@0.8.0", "", {}, "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw=="],
+
+ "make-dir/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
+
+ "mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
+
+ "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
+
+ "rolldown/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.12", "", {}, "sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw=="],
+ }
+}
diff --git a/components.json b/components.json
new file mode 100644
index 0000000..a468ddc
--- /dev/null
+++ b/components.json
@@ -0,0 +1,20 @@
+{
+ "$schema": "https://ui.shadcn.com/schema.json",
+ "style": "default",
+ "rsc": false,
+ "tsx": true,
+ "tailwind": {
+ "config": "tailwind.config.ts",
+ "css": "src/application/index.css",
+ "baseColor": "slate",
+ "cssVariables": true,
+ "prefix": ""
+ },
+ "aliases": {
+ "components": "@/application/components",
+ "utils": "@/application/lib/utils",
+ "ui": "@/application/components/ui",
+ "lib": "@/application/lib",
+ "hooks": "@/application/hooks"
+ }
+}
diff --git a/eslint.config.js b/eslint.config.js
new file mode 100644
index 0000000..e74a771
--- /dev/null
+++ b/eslint.config.js
@@ -0,0 +1,29 @@
+import js from "@eslint/js";
+import globals from "globals";
+import reactHooks from "eslint-plugin-react-hooks";
+import reactRefresh from "eslint-plugin-react-refresh";
+import tseslint from "typescript-eslint";
+
+export default tseslint.config(
+ { ignores: ["dist", "coverage"] },
+ {
+ extends: [js.configs.recommended, ...tseslint.configs.recommended],
+ files: ["**/*.{ts,tsx}"],
+ languageOptions: {
+ ecmaVersion: 2020,
+ globals: globals.browser,
+ },
+ plugins: {
+ "react-hooks": reactHooks,
+ "react-refresh": reactRefresh,
+ },
+ rules: {
+ ...reactHooks.configs.recommended.rules,
+ "react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
+ "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_", varsIgnorePattern: "^_", destructuredArrayIgnorePattern: "^_" }],
+ "@typescript-eslint/no-explicit-any": "warn",
+ "no-console": ["warn", { allow: ["warn", "error"] }],
+ "no-debugger": "warn",
+ },
+ },
+);
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..a04c3db
--- /dev/null
+++ b/index.html
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+ Composable UI
+
+
+
+
+
+
diff --git a/nginx.conf b/nginx.conf
new file mode 100644
index 0000000..e102925
--- /dev/null
+++ b/nginx.conf
@@ -0,0 +1,48 @@
+server {
+ listen 80;
+ server_name _;
+ root /usr/share/nginx/html;
+ index index.html;
+
+ # Security headers
+ add_header X-Frame-Options "SAMEORIGIN" always;
+ add_header X-Content-Type-Options "nosniff" always;
+ add_header X-XSS-Protection "1; mode=block" always;
+ add_header Referrer-Policy "strict-origin-when-cross-origin" always;
+ add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
+
+ # Gzip compression
+ gzip on;
+ gzip_vary on;
+ gzip_min_length 1024;
+ gzip_proxied any;
+ gzip_comp_level 6;
+ gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml;
+
+ # Cache static assets
+ location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
+ expires 1y;
+ add_header Cache-Control "public, immutable";
+ try_files $uri =404;
+ }
+
+ # SPA routing - serve index.html for all routes
+ location / {
+ try_files $uri $uri/ /index.html;
+ add_header Cache-Control "no-store, no-cache, must-revalidate";
+ }
+
+ # Health check endpoint
+ location /health {
+ access_log off;
+ return 200 "OK";
+ add_header Content-Type text/plain;
+ }
+
+ # Block hidden files
+ location ~ /\. {
+ deny all;
+ access_log off;
+ log_not_found off;
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..5d0698c
--- /dev/null
+++ b/package.json
@@ -0,0 +1,81 @@
+{
+ "name": "composable-ui",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc -b --clean && tsc -b && vite build",
+ "lint": "eslint .",
+ "lint:fix": "eslint . --fix",
+ "format": "prettier --write \"src/**/*.{ts,tsx}\" \"tests/**/*.{ts,tsx}\"",
+ "format:check": "prettier --check \"src/**/*.{ts,tsx}\" \"tests/**/*.{ts,tsx}\"",
+ "test": "vitest run",
+ "test:watch": "vitest",
+ "test:ui": "vitest --ui",
+ "test:coverage": "vitest run --coverage",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@hookform/resolvers": "^5.2.2",
+ "@microsoft/fetch-event-source": "^2.0.1",
+ "@radix-ui/react-alert-dialog": "^1.1.15",
+ "@radix-ui/react-avatar": "^1.1.11",
+ "@radix-ui/react-dialog": "^1.1.15",
+ "@radix-ui/react-dropdown-menu": "^2.1.16",
+ "@radix-ui/react-label": "^2.1.8",
+ "@radix-ui/react-popover": "^1.1.15",
+ "@radix-ui/react-scroll-area": "^1.2.10",
+ "@radix-ui/react-select": "^2.2.6",
+ "@radix-ui/react-separator": "^1.1.8",
+ "@radix-ui/react-slot": "^1.2.4",
+ "@radix-ui/react-tabs": "^1.1.13",
+ "@radix-ui/react-toggle": "^1.1.10",
+ "@radix-ui/react-toggle-group": "^1.1.11",
+ "@radix-ui/react-tooltip": "^1.2.8",
+ "@tanstack/react-query": "^5.96.2",
+ "axios": "^1.14.0",
+ "class-variance-authority": "^0.7.1",
+ "clsx": "^2.1.1",
+ "cmdk": "^1.1.1",
+ "framer-motion": "^12.38.0",
+ "lucide-react": "^1.7.0",
+ "react": "^19.2.4",
+ "react-dom": "^19.2.4",
+ "react-hook-form": "^7.72.1",
+ "react-markdown": "^10.1.0",
+ "react-router-dom": "^7.14.0",
+ "remark-gfm": "^4.0.1",
+ "sonner": "^2.0.7",
+ "tailwind-merge": "^3.5.0",
+ "tailwindcss-animate": "^1.0.7",
+ "zod": "^4.3.6",
+ "zustand": "^5.0.12"
+ },
+ "devDependencies": {
+ "@eslint/js": "^10.0.1",
+ "@tailwindcss/postcss": "^4.2.2",
+ "@testing-library/jest-dom": "^6.9.1",
+ "@testing-library/react": "^16.3.2",
+ "@testing-library/user-event": "^14.6.1",
+ "@types/node": "^25.5.2",
+ "@types/react": "^19.2.14",
+ "@types/react-dom": "^19.2.3",
+ "@vitejs/plugin-react-swc": "^4.3.0",
+ "@vitest/coverage-v8": "^4.1.2",
+ "@vitest/ui": "^4.1.2",
+ "autoprefixer": "^10.4.27",
+ "eslint": "^10.2.0",
+ "eslint-plugin-react-hooks": "^7.0.1",
+ "eslint-plugin-react-refresh": "^0.5.2",
+ "globals": "^17.4.0",
+ "jsdom": "25.0.1",
+ "postcss": "^8.5.8",
+ "prettier": "^3.8.1",
+ "tailwindcss": "^4.2.2",
+ "typescript": "^6.0.2",
+ "typescript-eslint": "^8.58.0",
+ "vite": "^8.0.1",
+ "vitest": "^4.1.2"
+ }
+}
diff --git a/postcss.config.js b/postcss.config.js
new file mode 100644
index 0000000..f69c5d4
--- /dev/null
+++ b/postcss.config.js
@@ -0,0 +1,6 @@
+export default {
+ plugins: {
+ "@tailwindcss/postcss": {},
+ autoprefixer: {},
+ },
+};
diff --git a/public/favicon.svg b/public/favicon.svg
new file mode 100644
index 0000000..6893eb1
--- /dev/null
+++ b/public/favicon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/icons.svg b/public/icons.svg
new file mode 100644
index 0000000..e952219
--- /dev/null
+++ b/public/icons.svg
@@ -0,0 +1,24 @@
+
diff --git a/sonar-project.properties b/sonar-project.properties
new file mode 100644
index 0000000..f1b55a6
--- /dev/null
+++ b/sonar-project.properties
@@ -0,0 +1,10 @@
+sonar.projectKey=composable-ui
+sonar.projectName=Composable UI
+sonar.sources=src
+sonar.tests=src
+sonar.test.inclusions=**/*.test.ts,**/*.test.tsx
+sonar.sourceEncoding=UTF-8
+sonar.exclusions=**/node_modules/**,**/dist/**,**/coverage/**,**/*.test.ts,**/*.test.tsx,**/test/**,**/*.d.ts,**/main.tsx
+sonar.javascript.lcov.reportPaths=coverage/lcov.info
+sonar.typescript.tsconfigPath=tsconfig.json
+sonar.coverage.exclusions=**/main.tsx,**/components/ui/**
diff --git a/src/application/App.tsx b/src/application/App.tsx
new file mode 100644
index 0000000..790e1f0
--- /dev/null
+++ b/src/application/App.tsx
@@ -0,0 +1,15 @@
+import { Routes, Route, Navigate } from "react-router-dom";
+import AgentsPage from "@/application/pages/AgentsPage";
+import ChatPage from "@/application/pages/ChatPage";
+
+function App() {
+ return (
+
+ } />
+ } />
+ } />
+
+ );
+}
+
+export default App;
diff --git a/src/application/components/agent/.gitkeep b/src/application/components/agent/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/src/application/components/agent/AgentCard.tsx b/src/application/components/agent/AgentCard.tsx
new file mode 100644
index 0000000..10e39c4
--- /dev/null
+++ b/src/application/components/agent/AgentCard.tsx
@@ -0,0 +1,77 @@
+import type { AgentConfigMetadata } from "@/domain/entities/agent/agentConfigMetadata";
+import StatusBadge from "@/application/components/shared/StatusBadge";
+
+interface AgentCardProps {
+ agent: AgentConfigMetadata;
+ onConfigure: (name: string) => void;
+}
+
+const AGENT_ICONS: Record = {
+ a: "smart_toy",
+ b: "psychology",
+ c: "code",
+ d: "data_object",
+ e: "engineering",
+ f: "functions",
+ g: "generating_tokens",
+ h: "hub",
+ i: "integration_instructions",
+ j: "join",
+ k: "key",
+ l: "lightbulb",
+ m: "model_training",
+ n: "neurology",
+ o: "offline_bolt",
+ p: "precision_manufacturing",
+ q: "query_stats",
+ r: "robot_2",
+ s: "schema",
+ t: "terminal",
+ u: "upgrade",
+ v: "verified",
+ w: "workspaces",
+ x: "extension",
+ y: "yield",
+ z: "zoom_in",
+};
+
+function getAgentIcon(name: string): string {
+ const firstLetter = name.charAt(0).toLowerCase();
+ return AGENT_ICONS[firstLetter] ?? "smart_toy";
+}
+
+export default function AgentCard({ agent, onConfigure }: AgentCardProps) {
+ return (
+
+ {/* Header: icon + status */}
+
+
+
+ {getAgentIcon(agent.name)}
+
+
+
+
+
+ {/* Name */}
+
+ {agent.name}
+
+
+ {/* Model */}
+
{agent.model}
+
+ {/* Configure link */}
+
+
+ );
+}
diff --git a/src/application/components/agent/AgentConfigViewer.tsx b/src/application/components/agent/AgentConfigViewer.tsx
new file mode 100644
index 0000000..0a56c50
--- /dev/null
+++ b/src/application/components/agent/AgentConfigViewer.tsx
@@ -0,0 +1,259 @@
+import { useState } from "react";
+import { toast } from "sonner";
+import { useAgentConfig } from "@/application/hooks/agent/useAgentConfig";
+import { useDeleteAgent } from "@/application/hooks/agent/useDeleteAgent";
+import StatusBadge from "@/application/components/shared/StatusBadge";
+import ToolTag from "@/application/components/shared/ToolTag";
+
+interface AgentConfigViewerProps {
+ agentName: string;
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+}
+
+export default function AgentConfigViewer({
+ agentName,
+ open,
+ onOpenChange,
+}: AgentConfigViewerProps) {
+ const { data: config, isLoading } = useAgentConfig(open ? agentName : null);
+ const deleteAgent = useDeleteAgent();
+ const [promptExpanded, setPromptExpanded] = useState(false);
+ const [confirmDelete, setConfirmDelete] = useState(false);
+
+ if (!open) return null;
+
+ function handleDelete() {
+ if (!confirmDelete) {
+ setConfirmDelete(true);
+ return;
+ }
+
+ deleteAgent.mutate(agentName, {
+ onSuccess: () => {
+ toast.success(`Agent "${agentName}" deleted`);
+ onOpenChange(false);
+ setConfirmDelete(false);
+ },
+ onError: (error) => {
+ toast.error(error.message);
+ setConfirmDelete(false);
+ },
+ });
+ }
+
+ function handleBackdropClick(e: React.MouseEvent) {
+ if (e.target === e.currentTarget) {
+ onOpenChange(false);
+ }
+ }
+
+ return (
+
+
+ {/* Header */}
+
+
+ {agentName}
+
+
+
+
+ {isLoading && (
+
+ Loading configuration...
+
+ )}
+
+ {config && (
+
+ {/* Model & Debug */}
+
+
+
Model
+
{config.model}
+
+
+ Debug
+
+
+
+
+ {/* System Prompt */}
+ {config.system_prompt && (
+
+
System Prompt
+
+ {promptExpanded
+ ? config.system_prompt
+ : config.system_prompt.slice(0, 200)}
+ {config.system_prompt.length > 200 && (
+
+ )}
+
+
+ )}
+
+ {/* Tools */}
+ {config.tools.length > 0 && (
+
+
Tools ({config.tools.length})
+
+ {config.tools.map((tool) => (
+
+ ))}
+
+
+ )}
+
+ {/* Middleware */}
+ {config.middleware.length > 0 && (
+
+
+ Middleware ({config.middleware.length})
+
+
+ {config.middleware.map((mw) => (
+
+ ))}
+
+
+ )}
+
+ {/* Backend */}
+
+
Backend
+
+ Type: {config.backend.type}
+ {config.backend.root_dir && (
+ <>
+ {" "}
+ · Root:{" "}
+ {config.backend.root_dir}
+ >
+ )}
+
+
+
+ {/* HITL Rules */}
+ {Object.keys(config.hitl.rules).length > 0 && (
+
+
HITL Rules
+
+ {Object.entries(config.hitl.rules).map(([key, value]) => (
+
+ {key}
+
+ {typeof value === "boolean"
+ ? value
+ ? "Enabled"
+ : "Disabled"
+ : JSON.stringify(value)}
+
+
+ ))}
+
+
+ )}
+
+ {/* MCP Servers */}
+ {config.mcp_servers.length > 0 && (
+
+
+ MCP Servers ({config.mcp_servers.length})
+
+
+ {config.mcp_servers.map((server) => (
+
+
+ {server.name}
+
+
+
+ ))}
+
+
+ )}
+
+ {/* Subagents */}
+ {config.subagents.length > 0 && (
+
+
+ Subagents ({config.subagents.length})
+
+
+ {config.subagents.map((sub) => (
+
+
+ {sub.name}
+
+
+ {sub.description}
+
+
+ ))}
+
+
+ )}
+
+ )}
+
+ {/* Actions */}
+
+
+
+
+
+
+ );
+}
+
+function SectionLabel({ children }: { children: React.ReactNode }) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/src/application/components/agent/AgentGrid.tsx b/src/application/components/agent/AgentGrid.tsx
new file mode 100644
index 0000000..116bbce
--- /dev/null
+++ b/src/application/components/agent/AgentGrid.tsx
@@ -0,0 +1,58 @@
+import { useAgents } from "@/application/hooks/agent/useAgents";
+import AgentCard from "@/application/components/agent/AgentCard";
+
+interface AgentGridProps {
+ onCreateNew: () => void;
+ onConfigure: (name: string) => void;
+}
+
+export default function AgentGrid({
+ onCreateNew,
+ onConfigure,
+}: AgentGridProps) {
+ const { data: agents, isLoading, error } = useAgents();
+
+ if (isLoading) {
+ return (
+
+
+ Loading agents...
+
+
+ );
+ }
+
+ if (error) {
+ return (
+
+
+ Failed to load agents: {error.message}
+
+
+ );
+ }
+
+ return (
+
+ {agents?.map((agent) => (
+
+ ))}
+
+ {/* New Agent template card */}
+
+
+ );
+}
diff --git a/src/application/components/agent/CreateAgentDialog.tsx b/src/application/components/agent/CreateAgentDialog.tsx
new file mode 100644
index 0000000..a674cae
--- /dev/null
+++ b/src/application/components/agent/CreateAgentDialog.tsx
@@ -0,0 +1,132 @@
+import { useState, useRef, type FormEvent } from "react";
+import { toast } from "sonner";
+import { useCreateAgent } from "@/application/hooks/agent/useCreateAgent";
+
+interface CreateAgentDialogProps {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+}
+
+export default function CreateAgentDialog({
+ open,
+ onOpenChange,
+}: CreateAgentDialogProps) {
+ const [name, setName] = useState("");
+ const fileInputRef = useRef(null);
+ const createAgent = useCreateAgent();
+
+ if (!open) return null;
+
+ function handleSubmit(e: FormEvent) {
+ e.preventDefault();
+
+ const file = fileInputRef.current?.files?.[0];
+ if (!name.trim()) {
+ toast.error("Agent name is required");
+ return;
+ }
+ if (!file) {
+ toast.error("YAML configuration file is required");
+ return;
+ }
+
+ createAgent.mutate(
+ { name: name.trim(), yamlFile: file },
+ {
+ onSuccess: () => {
+ toast.success("Agent created successfully");
+ setName("");
+ if (fileInputRef.current) fileInputRef.current.value = "";
+ onOpenChange(false);
+ },
+ onError: (error) => {
+ toast.error(error.message);
+ },
+ },
+ );
+ }
+
+ function handleBackdropClick(e: React.MouseEvent) {
+ if (e.target === e.currentTarget) {
+ onOpenChange(false);
+ }
+ }
+
+ return (
+
+
+ {/* Header */}
+
+
+ Create Agent
+
+
+
+
+ {/* Form */}
+
+
+
+ );
+}
diff --git a/src/application/components/chat/.gitkeep b/src/application/components/chat/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/src/application/components/chat/ChatInput.tsx b/src/application/components/chat/ChatInput.tsx
new file mode 100644
index 0000000..62ff352
--- /dev/null
+++ b/src/application/components/chat/ChatInput.tsx
@@ -0,0 +1,68 @@
+import { useState, type FormEvent, type KeyboardEvent } from "react";
+import { cn } from "@/application/lib/utils";
+import { useStreamChat } from "@/application/hooks/chat/useStreamChat";
+import { useChatStore } from "@/application/stores/useChatStore";
+
+interface ChatInputProps {
+ threadId: string;
+}
+
+export default function ChatInput({ threadId }: ChatInputProps) {
+ const [input, setInput] = useState("");
+ const { stream } = useStreamChat(threadId);
+ const isStreaming = useChatStore((s) => s.isStreaming);
+
+ function handleSubmit(e?: FormEvent) {
+ e?.preventDefault();
+ const trimmed = input.trim();
+ if (!trimmed || isStreaming) return;
+
+ stream({ message: trimmed });
+ setInput("");
+ }
+
+ function handleKeyDown(e: KeyboardEvent) {
+ if (e.key === "Enter" && !e.shiftKey) {
+ e.preventDefault();
+ handleSubmit();
+ }
+ }
+
+ return (
+
+
+
+ Composable Agents v0.1
+
+
+ );
+}
diff --git a/src/application/components/chat/ChatMessage.tsx b/src/application/components/chat/ChatMessage.tsx
new file mode 100644
index 0000000..65fcb70
--- /dev/null
+++ b/src/application/components/chat/ChatMessage.tsx
@@ -0,0 +1,117 @@
+import ReactMarkdown from "react-markdown";
+import remarkGfm from "remark-gfm";
+import { cn } from "@/application/lib/utils";
+import {
+ MessageRole,
+ MessageStatus,
+ type Message,
+} from "@/domain/entities/chat/message";
+import StatusBadge from "@/application/components/shared/StatusBadge";
+import HITLReviewPanel from "@/application/components/chat/HITLReviewPanel";
+
+interface ChatMessageProps {
+ message: Message;
+ agentName: string;
+ threadId?: string;
+}
+
+function formatTimestamp(ts: string): string {
+ const date = new Date(ts);
+ return date.toLocaleTimeString("en-US", {
+ hour: "2-digit",
+ minute: "2-digit",
+ });
+}
+
+export default function ChatMessage({
+ message,
+ agentName,
+ threadId,
+}: ChatMessageProps) {
+ const isHuman = message.role === MessageRole.HUMAN;
+ const isAi = message.role === MessageRole.AI;
+ const isAwaitingHitl = message.status === MessageStatus.AWAITING_HITL;
+
+ if (isHuman) {
+ return (
+
+
+
+
+ {message.content}
+
+
+
+ {formatTimestamp(message.timestamp)}
+
+
+
+ );
+ }
+
+ if (isAi) {
+ return (
+
+ {/* Avatar */}
+
+
+ hub
+
+
+
+
+ {/* Agent name + status */}
+
+
+ {agentName}
+
+ {message.status && }
+
+
+ {/* Content bubble */}
+
+
+
+ {message.content}
+
+
+
+
+ {/* HITL Review Panel */}
+ {isAwaitingHitl &&
+ message.tool_calls &&
+ message.tool_calls.length > 0 &&
+ threadId && (
+
+ )}
+
+ {/* Timestamp */}
+
+ {formatTimestamp(message.timestamp)}
+
+
+
+ );
+ }
+
+ // System / Tool messages: compact, centered
+ return (
+
+
+ {message.content}
+
+
+ );
+}
diff --git a/src/application/components/chat/HITLReviewPanel.tsx b/src/application/components/chat/HITLReviewPanel.tsx
new file mode 100644
index 0000000..6859f27
--- /dev/null
+++ b/src/application/components/chat/HITLReviewPanel.tsx
@@ -0,0 +1,141 @@
+import { useState } from "react";
+import { cn } from "@/application/lib/utils";
+import { useSendMessage } from "@/application/hooks/chat/useSendMessage";
+import type { ToolCall } from "@/domain/entities/chat/message";
+
+interface HITLReviewPanelProps {
+ toolCalls: ToolCall[];
+ threadId: string;
+}
+
+type ReviewState = "idle" | "reviewing" | "rejecting";
+
+export default function HITLReviewPanel({
+ toolCalls,
+ threadId,
+}: HITLReviewPanelProps) {
+ const [reviewState, setReviewState] = useState("idle");
+ const [rejectReason, setRejectReason] = useState("");
+ const sendMessage = useSendMessage(threadId);
+
+ const toolName = toolCalls[0]?.name ?? "Unknown tool";
+ const toolCallId = toolCalls[0]?.id ?? "";
+
+ function handleApprove() {
+ sendMessage.mutate({
+ tool_call_id: toolCallId,
+ action: "approve",
+ });
+ }
+
+ function handleReject() {
+ if (reviewState !== "rejecting") {
+ setReviewState("rejecting");
+ return;
+ }
+
+ sendMessage.mutate(
+ {
+ tool_call_id: toolCallId,
+ action: "reject",
+ reason: rejectReason || "Rejected by user",
+ },
+ {
+ onSuccess: () => {
+ setReviewState("idle");
+ setRejectReason("");
+ },
+ },
+ );
+ }
+
+ if (reviewState === "idle") {
+ return (
+
+
+
+ warning
+
+
+ Validation required: {toolName}
+
+
+
+
+ );
+ }
+
+ return (
+
+ {/* Tool call details */}
+
+
+ warning
+
+
+ Review: {toolName}
+
+
+
+ {/* Args display */}
+ {toolCalls[0]?.args && (
+
+ {JSON.stringify(toolCalls[0].args, null, 2)}
+
+ )}
+
+ {/* Reject reason input */}
+ {reviewState === "rejecting" && (
+
setRejectReason(e.target.value)}
+ placeholder="Reason for rejection (optional)"
+ className="w-full px-4 py-2 rounded-xl bg-surface-container-lowest border border-outline-variant/30 text-sm text-on-surface focus:outline-none focus:ring-2 focus:ring-secondary-brand/40"
+ />
+ )}
+
+ {/* Action buttons */}
+
+
+
+
+
+
+ );
+}
diff --git a/src/application/components/chat/MessageList.tsx b/src/application/components/chat/MessageList.tsx
new file mode 100644
index 0000000..006c605
--- /dev/null
+++ b/src/application/components/chat/MessageList.tsx
@@ -0,0 +1,87 @@
+import { useEffect, useRef } from "react";
+import { useMessages } from "@/application/hooks/chat/useMessages";
+import { useChatStore } from "@/application/stores/useChatStore";
+import { MessageRole, type Message } from "@/domain/entities/chat/message";
+import ChatMessage from "@/application/components/chat/ChatMessage";
+
+interface MessageListProps {
+ threadId: string;
+ agentName: string;
+}
+
+export default function MessageList({ threadId, agentName }: MessageListProps) {
+ const { data: messages, isLoading } = useMessages(threadId);
+ const { streamingContent, isStreaming } = useChatStore();
+ const scrollRef = useRef(null);
+
+ // Auto-scroll to bottom when messages change or streaming content updates
+ useEffect(() => {
+ if (scrollRef.current) {
+ scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
+ }
+ }, [messages, streamingContent]);
+
+ if (isLoading) {
+ return (
+
+ );
+ }
+
+ const hasMessages = messages && messages.length > 0;
+
+ if (!hasMessages && !isStreaming) {
+ return (
+
+
+
+ chat_bubble_outline
+
+
+ Send a message to start the conversation
+
+
+
+ );
+ }
+
+ // Build a temporary streaming message
+ const streamingMessage: Message | null =
+ isStreaming && streamingContent
+ ? {
+ role: MessageRole.AI,
+ content: streamingContent,
+ timestamp: new Date().toISOString(),
+ tool_calls: null,
+ status: null,
+ structured_response: null,
+ }
+ : null;
+
+ return (
+
+
+ {messages?.map((msg, idx) => (
+
+ ))}
+
+ {streamingMessage && (
+
+ )}
+
+
+ );
+}
diff --git a/src/application/components/layout/.gitkeep b/src/application/components/layout/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/src/application/components/layout/MainLayout.tsx b/src/application/components/layout/MainLayout.tsx
new file mode 100644
index 0000000..2785b24
--- /dev/null
+++ b/src/application/components/layout/MainLayout.tsx
@@ -0,0 +1,25 @@
+import type { ReactNode } from "react";
+import TopNav from "@/application/components/layout/TopNav";
+import ThreadSidebar from "@/application/components/layout/ThreadSidebar";
+
+interface MainLayoutProps {
+ children: ReactNode;
+ showSidebar?: boolean;
+ activeThreadId?: string;
+}
+
+export default function MainLayout({
+ children,
+ showSidebar = false,
+ activeThreadId,
+}: MainLayoutProps) {
+ return (
+
+
+
+ {showSidebar && }
+ {children}
+
+
+ );
+}
diff --git a/src/application/components/layout/ThreadSidebar.tsx b/src/application/components/layout/ThreadSidebar.tsx
new file mode 100644
index 0000000..4d1bed2
--- /dev/null
+++ b/src/application/components/layout/ThreadSidebar.tsx
@@ -0,0 +1,117 @@
+import { useMemo } from "react";
+import { useNavigate } from "react-router-dom";
+import { cn } from "@/application/lib/utils";
+import { useThreads } from "@/application/hooks/chat/useThreads";
+import { useChatStore } from "@/application/stores/useChatStore";
+import type { Thread } from "@/domain/entities/chat/thread";
+
+interface ThreadSidebarProps {
+ activeThreadId?: string;
+}
+
+function formatDate(dateStr: string): string {
+ const date = new Date(dateStr);
+ return date.toLocaleDateString("en-US", {
+ month: "short",
+ day: "numeric",
+ hour: "2-digit",
+ minute: "2-digit",
+ });
+}
+
+export default function ThreadSidebar({ activeThreadId }: ThreadSidebarProps) {
+ const { data: threads, isLoading } = useThreads();
+ const setActiveThread = useChatStore((s) => s.setActiveThread);
+ const navigate = useNavigate();
+
+ const grouped = useMemo(() => {
+ if (!threads) return new Map();
+ const map = new Map();
+ for (const thread of threads) {
+ const list = map.get(thread.agent_name) ?? [];
+ list.push(thread);
+ map.set(thread.agent_name, list);
+ }
+ return map;
+ }, [threads]);
+
+ function handleThreadClick(threadId: string) {
+ setActiveThread(threadId);
+ navigate(`/chat/${threadId}`);
+ }
+
+ function handleNewConversation() {
+ setActiveThread(null);
+ navigate("/chat");
+ }
+
+ return (
+
+ );
+}
diff --git a/src/application/components/layout/TopNav.tsx b/src/application/components/layout/TopNav.tsx
new file mode 100644
index 0000000..b055d84
--- /dev/null
+++ b/src/application/components/layout/TopNav.tsx
@@ -0,0 +1,60 @@
+import { NavLink } from "react-router-dom";
+import { cn } from "@/application/lib/utils";
+
+const NAV_LINKS = [
+ { to: "/chat", label: "Orchestration" },
+ { to: "/agents", label: "Agents" },
+] as const;
+
+export default function TopNav() {
+ return (
+
+ );
+}
diff --git a/src/application/components/shared/.gitkeep b/src/application/components/shared/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/src/application/components/shared/StatusBadge.tsx b/src/application/components/shared/StatusBadge.tsx
new file mode 100644
index 0000000..e47bc99
--- /dev/null
+++ b/src/application/components/shared/StatusBadge.tsx
@@ -0,0 +1,35 @@
+import { cn } from "@/application/lib/utils";
+
+interface StatusBadgeProps {
+ status: string;
+}
+
+function resolveVariant(status: string): {
+ bg: string;
+ text: string;
+} {
+ const normalized = status.toLowerCase();
+ if (normalized === "active" || normalized === "completed") {
+ return { bg: "bg-emerald-100", text: "text-emerald-700" };
+ }
+ if (normalized === "awaiting_hitl") {
+ return { bg: "bg-orange-100", text: "text-orange-700" };
+ }
+ return { bg: "bg-slate-100", text: "text-slate-600" };
+}
+
+export default function StatusBadge({ status }: StatusBadgeProps) {
+ const { bg, text } = resolveVariant(status);
+
+ return (
+
+ {status}
+
+ );
+}
diff --git a/src/application/components/shared/ToolTag.tsx b/src/application/components/shared/ToolTag.tsx
new file mode 100644
index 0000000..31e3098
--- /dev/null
+++ b/src/application/components/shared/ToolTag.tsx
@@ -0,0 +1,11 @@
+interface ToolTagProps {
+ label: string;
+}
+
+export default function ToolTag({ label }: ToolTagProps) {
+ return (
+
+ {label}
+
+ );
+}
diff --git a/src/application/components/ui/.gitkeep b/src/application/components/ui/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/src/application/hooks/agent/.gitkeep b/src/application/hooks/agent/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/src/application/hooks/agent/useAgentConfig.ts b/src/application/hooks/agent/useAgentConfig.ts
new file mode 100644
index 0000000..5f69a6b
--- /dev/null
+++ b/src/application/hooks/agent/useAgentConfig.ts
@@ -0,0 +1,10 @@
+import { useQuery } from "@tanstack/react-query";
+import { agentApi } from "@/infrastructure/api/agent/agentApi";
+
+export function useAgentConfig(name: string | null) {
+ return useQuery({
+ queryKey: ["agent", name],
+ queryFn: () => agentApi.getAgent(name!),
+ enabled: !!name,
+ });
+}
diff --git a/src/application/hooks/agent/useAgents.ts b/src/application/hooks/agent/useAgents.ts
new file mode 100644
index 0000000..c9a549e
--- /dev/null
+++ b/src/application/hooks/agent/useAgents.ts
@@ -0,0 +1,9 @@
+import { useQuery } from "@tanstack/react-query";
+import { agentApi } from "@/infrastructure/api/agent/agentApi";
+
+export function useAgents() {
+ return useQuery({
+ queryKey: ["agents"],
+ queryFn: () => agentApi.listAgents(),
+ });
+}
diff --git a/src/application/hooks/agent/useCreateAgent.ts b/src/application/hooks/agent/useCreateAgent.ts
new file mode 100644
index 0000000..0532161
--- /dev/null
+++ b/src/application/hooks/agent/useCreateAgent.ts
@@ -0,0 +1,14 @@
+import { useMutation, useQueryClient } from "@tanstack/react-query";
+import { agentApi } from "@/infrastructure/api/agent/agentApi";
+
+export function useCreateAgent() {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: ({ name, yamlFile }: { name: string; yamlFile: File }) =>
+ agentApi.createAgent(name, yamlFile),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ["agents"] });
+ },
+ });
+}
diff --git a/src/application/hooks/agent/useDeleteAgent.ts b/src/application/hooks/agent/useDeleteAgent.ts
new file mode 100644
index 0000000..4cbd040
--- /dev/null
+++ b/src/application/hooks/agent/useDeleteAgent.ts
@@ -0,0 +1,13 @@
+import { useMutation, useQueryClient } from "@tanstack/react-query";
+import { agentApi } from "@/infrastructure/api/agent/agentApi";
+
+export function useDeleteAgent() {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: (name: string) => agentApi.deleteAgent(name),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ["agents"] });
+ },
+ });
+}
diff --git a/src/application/hooks/agent/useUpdateAgent.ts b/src/application/hooks/agent/useUpdateAgent.ts
new file mode 100644
index 0000000..a76e61d
--- /dev/null
+++ b/src/application/hooks/agent/useUpdateAgent.ts
@@ -0,0 +1,15 @@
+import { useMutation, useQueryClient } from "@tanstack/react-query";
+import { agentApi } from "@/infrastructure/api/agent/agentApi";
+
+export function useUpdateAgent() {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: ({ name, yamlFile }: { name: string; yamlFile: File }) =>
+ agentApi.updateAgent(name, yamlFile),
+ onSuccess: (_data, variables) => {
+ queryClient.invalidateQueries({ queryKey: ["agents"] });
+ queryClient.invalidateQueries({ queryKey: ["agent", variables.name] });
+ },
+ });
+}
diff --git a/src/application/hooks/chat/.gitkeep b/src/application/hooks/chat/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/src/application/hooks/chat/useCreateThread.ts b/src/application/hooks/chat/useCreateThread.ts
new file mode 100644
index 0000000..547e6f8
--- /dev/null
+++ b/src/application/hooks/chat/useCreateThread.ts
@@ -0,0 +1,13 @@
+import { useMutation, useQueryClient } from "@tanstack/react-query";
+import { chatApi } from "@/infrastructure/api/chat/chatApi";
+
+export function useCreateThread() {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: (agentName: string) => chatApi.createThread(agentName),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ["threads"] });
+ },
+ });
+}
diff --git a/src/application/hooks/chat/useDeleteThread.ts b/src/application/hooks/chat/useDeleteThread.ts
new file mode 100644
index 0000000..0dfc83d
--- /dev/null
+++ b/src/application/hooks/chat/useDeleteThread.ts
@@ -0,0 +1,13 @@
+import { useMutation, useQueryClient } from "@tanstack/react-query";
+import { chatApi } from "@/infrastructure/api/chat/chatApi";
+
+export function useDeleteThread() {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: (threadId: string) => chatApi.deleteThread(threadId),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ["threads"] });
+ },
+ });
+}
diff --git a/src/application/hooks/chat/useMessages.ts b/src/application/hooks/chat/useMessages.ts
new file mode 100644
index 0000000..f78751d
--- /dev/null
+++ b/src/application/hooks/chat/useMessages.ts
@@ -0,0 +1,10 @@
+import { useQuery } from "@tanstack/react-query";
+import { chatApi } from "@/infrastructure/api/chat/chatApi";
+
+export function useMessages(threadId: string | null) {
+ return useQuery({
+ queryKey: ["messages", threadId],
+ queryFn: () => chatApi.getMessages(threadId!),
+ enabled: !!threadId,
+ });
+}
diff --git a/src/application/hooks/chat/useSendMessage.ts b/src/application/hooks/chat/useSendMessage.ts
new file mode 100644
index 0000000..84c39b4
--- /dev/null
+++ b/src/application/hooks/chat/useSendMessage.ts
@@ -0,0 +1,20 @@
+import { useMutation, useQueryClient } from "@tanstack/react-query";
+import { chatApi } from "@/infrastructure/api/chat/chatApi";
+import type { ChatRequest } from "@/domain/entities/chat/chatRequest";
+
+export function useSendMessage(threadId: string | null) {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: (request: ChatRequest) => {
+ if (!threadId)
+ throw new Error("Cannot send message without an active thread");
+ return chatApi.sendMessage(threadId, request);
+ },
+ onSuccess: () => {
+ if (threadId) {
+ queryClient.invalidateQueries({ queryKey: ["messages", threadId] });
+ }
+ },
+ });
+}
diff --git a/src/application/hooks/chat/useStreamChat.ts b/src/application/hooks/chat/useStreamChat.ts
new file mode 100644
index 0000000..3d5616c
--- /dev/null
+++ b/src/application/hooks/chat/useStreamChat.ts
@@ -0,0 +1,54 @@
+import { useCallback, useEffect, useRef } from "react";
+import { useQueryClient } from "@tanstack/react-query";
+import { chatApi } from "@/infrastructure/api/chat/chatApi";
+import { useChatStore } from "@/application/stores/useChatStore";
+import type { ChatRequest } from "@/domain/entities/chat/chatRequest";
+
+export function useStreamChat(threadId: string | null) {
+ const queryClient = useQueryClient();
+ const { setStreaming, appendStreamChunk, clearStream } = useChatStore();
+ const abortRef = useRef(null);
+
+ const stream = useCallback(
+ (request: ChatRequest) => {
+ if (!threadId) return;
+
+ clearStream();
+ setStreaming(true);
+
+ abortRef.current = chatApi.streamMessage(
+ threadId,
+ request,
+ (chunk) => {
+ appendStreamChunk(chunk);
+ },
+ () => {
+ setStreaming(false);
+ queryClient.invalidateQueries({ queryKey: ["messages", threadId] });
+ },
+ (error) => {
+ console.error("Stream error:", error);
+ setStreaming(false);
+ },
+ );
+ },
+ [threadId, clearStream, setStreaming, appendStreamChunk, queryClient],
+ );
+
+ const cancel = useCallback(() => {
+ if (abortRef.current) {
+ abortRef.current.abort();
+ setStreaming(false);
+ }
+ }, [setStreaming]);
+
+ useEffect(() => {
+ return () => {
+ if (abortRef.current) {
+ abortRef.current.abort();
+ }
+ };
+ }, []);
+
+ return { stream, cancel };
+}
diff --git a/src/application/hooks/chat/useThreads.ts b/src/application/hooks/chat/useThreads.ts
new file mode 100644
index 0000000..c8e3bc8
--- /dev/null
+++ b/src/application/hooks/chat/useThreads.ts
@@ -0,0 +1,9 @@
+import { useQuery } from "@tanstack/react-query";
+import { chatApi } from "@/infrastructure/api/chat/chatApi";
+
+export function useThreads() {
+ return useQuery({
+ queryKey: ["threads"],
+ queryFn: () => chatApi.listThreads(),
+ });
+}
diff --git a/src/application/index.css b/src/application/index.css
new file mode 100644
index 0000000..ef66394
--- /dev/null
+++ b/src/application/index.css
@@ -0,0 +1,93 @@
+@import "tailwindcss";
+
+@theme {
+ --color-border: hsl(var(--border));
+ --color-input: hsl(var(--input));
+ --color-ring: hsl(var(--ring));
+ --color-background: hsl(var(--background));
+ --color-foreground: hsl(var(--foreground));
+ --color-primary: hsl(var(--primary));
+ --color-primary-foreground: hsl(var(--primary-foreground));
+ --color-secondary: hsl(var(--secondary));
+ --color-secondary-foreground: hsl(var(--secondary-foreground));
+ --color-destructive: hsl(var(--destructive));
+ --color-destructive-foreground: hsl(var(--destructive-foreground));
+ --color-muted: hsl(var(--muted));
+ --color-muted-foreground: hsl(var(--muted-foreground));
+ --color-accent: hsl(var(--accent));
+ --color-accent-foreground: hsl(var(--accent-foreground));
+ --color-popover: hsl(var(--popover));
+ --color-popover-foreground: hsl(var(--popover-foreground));
+ --color-card: hsl(var(--card));
+ --color-card-foreground: hsl(var(--card-foreground));
+
+ --color-surface: #f8f9fa;
+ --color-on-surface: #191c1d;
+ --color-on-surface-variant: #48464d;
+ --color-surface-container: #edeeef;
+ --color-surface-container-low: #f3f4f5;
+ --color-surface-container-high: #e7e8e9;
+ --color-surface-container-highest: #e1e3e4;
+ --color-surface-container-lowest: #ffffff;
+ --color-outline: #79767e;
+ --color-outline-variant: #c9c5ce;
+ --color-primary-container: #1c1835;
+ --color-on-primary-container: #8580a3;
+ --color-secondary-brand: #b3290d;
+ --color-secondary-container: #fe5e3e;
+ --color-on-secondary-container: #5c0b00;
+ --color-primary-fixed: #e5deff;
+ --color-secondary-fixed: #ffdad3;
+ --color-tertiary-fixed: #e1e0ff;
+
+ --font-headline: "Space Grotesk", sans-serif;
+ --font-body: "Inter", sans-serif;
+
+ --radius-lg: var(--radius);
+ --radius-md: calc(var(--radius) - 2px);
+ --radius-sm: calc(var(--radius) - 4px);
+}
+
+@layer base {
+ :root {
+ --background: 0 0% 98%;
+ --foreground: 200 7% 11%;
+ --card: 0 0% 100%;
+ --card-foreground: 200 7% 11%;
+ --popover: 0 0% 100%;
+ --popover-foreground: 200 7% 11%;
+ --primary: 0 0% 0%;
+ --primary-foreground: 0 0% 100%;
+ --secondary: 10 87% 38%;
+ --secondary-foreground: 0 0% 100%;
+ --muted: 200 5% 94%;
+ --muted-foreground: 260 3% 29%;
+ --accent: 200 5% 94%;
+ --accent-foreground: 200 7% 11%;
+ --destructive: 0 84% 41%;
+ --destructive-foreground: 0 0% 100%;
+ --border: 270 6% 79%;
+ --input: 270 6% 79%;
+ --ring: 0 0% 0%;
+ --radius: 0.5rem;
+ }
+}
+
+@layer base {
+ * {
+ @apply border-border;
+ }
+ body {
+ @apply bg-background text-foreground font-body antialiased;
+ }
+}
+
+.glass-panel {
+ background: rgba(255, 255, 255, 0.7);
+ backdrop-filter: blur(20px);
+ -webkit-backdrop-filter: blur(20px);
+}
+
+.ambient-shadow {
+ box-shadow: 0px 40px 40px -10px rgba(25, 28, 29, 0.06);
+}
diff --git a/src/application/lib/utils.ts b/src/application/lib/utils.ts
new file mode 100644
index 0000000..365058c
--- /dev/null
+++ b/src/application/lib/utils.ts
@@ -0,0 +1,6 @@
+import { type ClassValue, clsx } from "clsx";
+import { twMerge } from "tailwind-merge";
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs));
+}
diff --git a/src/application/main.tsx b/src/application/main.tsx
new file mode 100644
index 0000000..5566d74
--- /dev/null
+++ b/src/application/main.tsx
@@ -0,0 +1,27 @@
+import { StrictMode } from "react";
+import { createRoot } from "react-dom/client";
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import { BrowserRouter } from "react-router-dom";
+import { Toaster } from "sonner";
+import App from "./App";
+import "./index.css";
+
+const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ retry: 1,
+ refetchOnWindowFocus: false,
+ },
+ },
+});
+
+createRoot(document.getElementById("root")!).render(
+
+
+
+
+
+
+
+ ,
+);
diff --git a/src/application/pages/.gitkeep b/src/application/pages/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/src/application/pages/AgentsPage.tsx b/src/application/pages/AgentsPage.tsx
new file mode 100644
index 0000000..ca5f89d
--- /dev/null
+++ b/src/application/pages/AgentsPage.tsx
@@ -0,0 +1,62 @@
+import { useState } from "react";
+import MainLayout from "@/application/components/layout/MainLayout";
+import AgentGrid from "@/application/components/agent/AgentGrid";
+import CreateAgentDialog from "@/application/components/agent/CreateAgentDialog";
+import AgentConfigViewer from "@/application/components/agent/AgentConfigViewer";
+
+export default function AgentsPage() {
+ const [createDialogOpen, setCreateDialogOpen] = useState(false);
+ const [viewerAgent, setViewerAgent] = useState(null);
+
+ return (
+
+
+
+ {/* Header */}
+
+
+
+ Active Agents
+
+
+ Configure and manage your AI agent fleet. Each agent operates
+ with its own tools, middleware, and orchestration rules.
+
+
+
+
+
+ {/* Grid */}
+
setCreateDialogOpen(true)}
+ onConfigure={(name) => setViewerAgent(name)}
+ />
+
+
+
+ {/* Dialogs */}
+
+ {viewerAgent && (
+ {
+ if (!open) setViewerAgent(null);
+ }}
+ />
+ )}
+
+ );
+}
diff --git a/src/application/pages/ChatPage.tsx b/src/application/pages/ChatPage.tsx
new file mode 100644
index 0000000..acfae8a
--- /dev/null
+++ b/src/application/pages/ChatPage.tsx
@@ -0,0 +1,49 @@
+import { useEffect, useMemo } from "react";
+import { useParams } from "react-router-dom";
+import MainLayout from "@/application/components/layout/MainLayout";
+import MessageList from "@/application/components/chat/MessageList";
+import ChatInput from "@/application/components/chat/ChatInput";
+import { useChatStore } from "@/application/stores/useChatStore";
+import { useThreads } from "@/application/hooks/chat/useThreads";
+
+export default function ChatPage() {
+ const { threadId } = useParams<{ threadId?: string }>();
+ const setActiveThread = useChatStore((s) => s.setActiveThread);
+ const { data: threads } = useThreads();
+
+ useEffect(() => {
+ setActiveThread(threadId ?? null);
+ }, [threadId, setActiveThread]);
+
+ const agentName = useMemo(() => {
+ if (!threadId || !threads) return "Agent";
+ const thread = threads.find((t) => t.id === threadId);
+ return thread?.agent_name ?? "Agent";
+ }, [threadId, threads]);
+
+ return (
+
+ {threadId ? (
+
+
+
+
+ ) : (
+
+
+
+ forum
+
+
+ Orchestration Console
+
+
+ Select a thread from the sidebar or start a new conversation to
+ begin orchestrating your agents.
+
+
+
+ )}
+
+ );
+}
diff --git a/src/application/stores/.gitkeep b/src/application/stores/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/src/application/stores/useChatStore.ts b/src/application/stores/useChatStore.ts
new file mode 100644
index 0000000..e5d219c
--- /dev/null
+++ b/src/application/stores/useChatStore.ts
@@ -0,0 +1,22 @@
+import { create } from "zustand";
+
+interface ChatState {
+ activeThreadId: string | null;
+ streamingContent: string;
+ isStreaming: boolean;
+ setActiveThread: (id: string | null) => void;
+ appendStreamChunk: (chunk: string) => void;
+ clearStream: () => void;
+ setStreaming: (streaming: boolean) => void;
+}
+
+export const useChatStore = create((set) => ({
+ activeThreadId: null,
+ streamingContent: "",
+ isStreaming: false,
+ setActiveThread: (id) => set({ activeThreadId: id }),
+ appendStreamChunk: (chunk) =>
+ set((state) => ({ streamingContent: state.streamingContent + chunk })),
+ clearStream: () => set({ streamingContent: "", isStreaming: false }),
+ setStreaming: (streaming) => set({ isStreaming: streaming }),
+}));
diff --git a/src/application/vite-env.d.ts b/src/application/vite-env.d.ts
new file mode 100644
index 0000000..11f02fe
--- /dev/null
+++ b/src/application/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/src/domain/entities/agent/agentConfig.ts b/src/domain/entities/agent/agentConfig.ts
new file mode 100644
index 0000000..200d9c2
--- /dev/null
+++ b/src/domain/entities/agent/agentConfig.ts
@@ -0,0 +1,56 @@
+import type { McpServerConfig } from "./mcpServerConfig";
+
+export enum MiddlewareType {
+ TODO_LIST = "todo_list",
+ FILESYSTEM = "filesystem",
+ SUB_AGENT = "sub_agent",
+}
+
+export enum BackendType {
+ STATE = "state",
+ STORE = "store",
+ FILESYSTEM = "filesystem",
+ COMPOSITE = "composite",
+}
+
+export interface BackendConfig {
+ type: BackendType;
+ root_dir?: string;
+}
+
+export interface InterruptRule {
+ before?: boolean;
+ after?: boolean;
+}
+
+export interface HITLConfig {
+ rules: Record;
+}
+
+export interface SubAgentConfig {
+ name: string;
+ description: string;
+ instructions?: string;
+ model?: string;
+ tools: string[];
+ skills: string[];
+ mcp_servers: McpServerConfig[];
+ response_format?: Record;
+}
+
+export interface AgentConfig {
+ name: string;
+ model: string;
+ system_prompt?: string;
+ system_prompt_file?: string;
+ tools: string[];
+ middleware: MiddlewareType[];
+ backend: BackendConfig;
+ hitl: HITLConfig;
+ memory: string[];
+ skills: string[];
+ subagents: SubAgentConfig[];
+ mcp_servers: McpServerConfig[];
+ response_format?: Record;
+ debug: boolean;
+}
diff --git a/src/domain/entities/agent/agentConfigMetadata.ts b/src/domain/entities/agent/agentConfigMetadata.ts
new file mode 100644
index 0000000..64f15b9
--- /dev/null
+++ b/src/domain/entities/agent/agentConfigMetadata.ts
@@ -0,0 +1,8 @@
+export interface AgentConfigMetadata {
+ name: string;
+ model: string;
+ minio_path: string;
+ is_builtin: boolean;
+ created_at: string;
+ updated_at: string;
+}
diff --git a/src/domain/entities/agent/mcpServerConfig.ts b/src/domain/entities/agent/mcpServerConfig.ts
new file mode 100644
index 0000000..df32b81
--- /dev/null
+++ b/src/domain/entities/agent/mcpServerConfig.ts
@@ -0,0 +1,15 @@
+export enum McpTransportType {
+ STDIO = "stdio",
+ HTTP = "http",
+}
+
+export interface McpServerConfig {
+ name: string;
+ transport: McpTransportType;
+ command?: string;
+ args: string[];
+ url?: string;
+ headers: Record;
+ env: Record;
+ auth_token?: string;
+}
diff --git a/src/domain/entities/chat/chatRequest.ts b/src/domain/entities/chat/chatRequest.ts
new file mode 100644
index 0000000..ea2c248
--- /dev/null
+++ b/src/domain/entities/chat/chatRequest.ts
@@ -0,0 +1,9 @@
+export type HITLAction = "approve" | "reject" | "edit";
+
+export interface ChatRequest {
+ message?: string;
+ tool_call_id?: string;
+ action?: HITLAction;
+ reason?: string;
+ edits?: Record;
+}
diff --git a/src/domain/entities/chat/message.ts b/src/domain/entities/chat/message.ts
new file mode 100644
index 0000000..66864bb
--- /dev/null
+++ b/src/domain/entities/chat/message.ts
@@ -0,0 +1,27 @@
+export enum MessageRole {
+ HUMAN = "human",
+ AI = "ai",
+ SYSTEM = "system",
+ TOOL = "tool",
+}
+
+export enum MessageStatus {
+ COMPLETED = "completed",
+ AWAITING_HITL = "awaiting_hitl",
+}
+
+export interface ToolCall {
+ id: string;
+ name: string;
+ args: Record;
+ result?: string;
+}
+
+export interface Message {
+ role: MessageRole;
+ content: string;
+ timestamp: string;
+ tool_calls: ToolCall[] | null;
+ status: MessageStatus | null;
+ structured_response: unknown | null;
+}
diff --git a/src/domain/entities/chat/thread.ts b/src/domain/entities/chat/thread.ts
new file mode 100644
index 0000000..41f11c3
--- /dev/null
+++ b/src/domain/entities/chat/thread.ts
@@ -0,0 +1,9 @@
+import type { Message } from "./message";
+
+export interface Thread {
+ id: string;
+ agent_name: string;
+ messages: Message[];
+ created_at: string;
+ updated_at: string;
+}
diff --git a/src/domain/lib/.gitkeep b/src/domain/lib/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/src/domain/ports/agent/.gitkeep b/src/domain/ports/agent/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/src/domain/ports/agent/agentPort.ts b/src/domain/ports/agent/agentPort.ts
new file mode 100644
index 0000000..60429b1
--- /dev/null
+++ b/src/domain/ports/agent/agentPort.ts
@@ -0,0 +1,10 @@
+import type { AgentConfig } from "@/domain/entities/agent/agentConfig";
+import type { AgentConfigMetadata } from "@/domain/entities/agent/agentConfigMetadata";
+
+export interface IAgentPort {
+ listAgents(): Promise;
+ getAgent(name: string): Promise;
+ createAgent(name: string, yamlFile: File): Promise;
+ updateAgent(name: string, yamlFile: File): Promise;
+ deleteAgent(name: string): Promise;
+}
diff --git a/src/domain/ports/chat/.gitkeep b/src/domain/ports/chat/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/src/domain/ports/chat/chatPort.ts b/src/domain/ports/chat/chatPort.ts
new file mode 100644
index 0000000..6f23449
--- /dev/null
+++ b/src/domain/ports/chat/chatPort.ts
@@ -0,0 +1,19 @@
+import type { ChatRequest } from "@/domain/entities/chat/chatRequest";
+import type { Message } from "@/domain/entities/chat/message";
+import type { Thread } from "@/domain/entities/chat/thread";
+
+export interface IChatPort {
+ createThread(agentName: string): Promise;
+ listThreads(): Promise;
+ getThread(threadId: string): Promise;
+ deleteThread(threadId: string): Promise;
+ getMessages(threadId: string): Promise;
+ sendMessage(threadId: string, request: ChatRequest): Promise;
+ streamMessage(
+ threadId: string,
+ request: ChatRequest,
+ onChunk: (data: string) => void,
+ onComplete: () => void,
+ onError: (err: Error) => void,
+ ): AbortController;
+}
diff --git a/src/infrastructure/api/agent/.gitkeep b/src/infrastructure/api/agent/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/src/infrastructure/api/agent/agentApi.ts b/src/infrastructure/api/agent/agentApi.ts
new file mode 100644
index 0000000..281cdec
--- /dev/null
+++ b/src/infrastructure/api/agent/agentApi.ts
@@ -0,0 +1,52 @@
+import type { AgentConfig } from "@/domain/entities/agent/agentConfig";
+import type { AgentConfigMetadata } from "@/domain/entities/agent/agentConfigMetadata";
+import type { IAgentPort } from "@/domain/ports/agent/agentPort";
+import { apiClient } from "@/infrastructure/api/axiosInstance";
+
+export const agentApi: IAgentPort = {
+ async listAgents(): Promise {
+ const response =
+ await apiClient.get("/api/v1/agents");
+ return response.data;
+ },
+
+ async getAgent(name: string): Promise {
+ const response = await apiClient.get(
+ `/api/v1/agents/${encodeURIComponent(name)}`,
+ );
+ return response.data;
+ },
+
+ async createAgent(name: string, yamlFile: File): Promise {
+ const formData = new FormData();
+ formData.append("agent_name", name);
+ formData.append("file", yamlFile);
+
+ const response = await apiClient.post(
+ "/api/v1/agents",
+ formData,
+ {
+ headers: { "Content-Type": "multipart/form-data" },
+ },
+ );
+ return response.data;
+ },
+
+ async updateAgent(name: string, yamlFile: File): Promise {
+ const formData = new FormData();
+ formData.append("file", yamlFile);
+
+ const response = await apiClient.put(
+ `/api/v1/agents/${encodeURIComponent(name)}`,
+ formData,
+ {
+ headers: { "Content-Type": "multipart/form-data" },
+ },
+ );
+ return response.data;
+ },
+
+ async deleteAgent(name: string): Promise {
+ await apiClient.delete(`/api/v1/agents/${encodeURIComponent(name)}`);
+ },
+};
diff --git a/src/infrastructure/api/axiosInstance.ts b/src/infrastructure/api/axiosInstance.ts
new file mode 100644
index 0000000..37e18cd
--- /dev/null
+++ b/src/infrastructure/api/axiosInstance.ts
@@ -0,0 +1,21 @@
+import axios from "axios";
+import { envConfig } from "@/infrastructure/config/envConfig";
+
+export const apiClient = axios.create({
+ baseURL: envConfig.apiBaseUrl,
+ timeout: 30000,
+ headers: {
+ "Content-Type": "application/json",
+ },
+});
+
+apiClient.interceptors.response.use(
+ (response) => response,
+ (error) => {
+ if (error.response) {
+ const detail = error.response.data?.detail || error.message;
+ return Promise.reject(new Error(detail));
+ }
+ return Promise.reject(error);
+ },
+);
diff --git a/src/infrastructure/api/chat/.gitkeep b/src/infrastructure/api/chat/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/src/infrastructure/api/chat/chatApi.ts b/src/infrastructure/api/chat/chatApi.ts
new file mode 100644
index 0000000..c87350c
--- /dev/null
+++ b/src/infrastructure/api/chat/chatApi.ts
@@ -0,0 +1,76 @@
+import type { ChatRequest } from "@/domain/entities/chat/chatRequest";
+import type { Message } from "@/domain/entities/chat/message";
+import type { Thread } from "@/domain/entities/chat/thread";
+import type { IChatPort } from "@/domain/ports/chat/chatPort";
+import { apiClient } from "@/infrastructure/api/axiosInstance";
+import { fetchEventSource } from "@microsoft/fetch-event-source";
+import { envConfig } from "@/infrastructure/config/envConfig";
+
+export const chatApi: IChatPort = {
+ async createThread(agentName: string): Promise {
+ const response = await apiClient.post("/api/v1/threads", {
+ agent_name: agentName,
+ });
+ return response.data;
+ },
+
+ async listThreads(): Promise {
+ const response = await apiClient.get("/api/v1/threads");
+ return response.data;
+ },
+
+ async getThread(threadId: string): Promise {
+ const response = await apiClient.get(`/api/v1/threads/${threadId}`);
+ return response.data;
+ },
+
+ async deleteThread(threadId: string): Promise {
+ await apiClient.delete(`/api/v1/threads/${threadId}`);
+ },
+
+ async getMessages(threadId: string): Promise {
+ const response = await apiClient.get(
+ `/api/v1/threads/${threadId}/messages`,
+ );
+ return response.data;
+ },
+
+ async sendMessage(threadId: string, request: ChatRequest): Promise {
+ const response = await apiClient.post(
+ `/api/v1/chat/${threadId}`,
+ request,
+ );
+ return response.data;
+ },
+
+ streamMessage(
+ threadId: string,
+ request: ChatRequest,
+ onChunk: (data: string) => void,
+ onComplete: () => void,
+ onError: (err: Error) => void,
+ ): AbortController {
+ const ctrl = new AbortController();
+
+ fetchEventSource(`${envConfig.apiBaseUrl}/api/v1/chat/${threadId}/stream`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(request),
+ signal: ctrl.signal,
+ onmessage(ev) {
+ if (ev.data) {
+ onChunk(ev.data);
+ }
+ },
+ onclose() {
+ onComplete();
+ },
+ onerror(err) {
+ onError(err instanceof Error ? err : new Error(String(err)));
+ throw err;
+ },
+ });
+
+ return ctrl;
+ },
+};
diff --git a/src/infrastructure/config/.gitkeep b/src/infrastructure/config/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/src/infrastructure/config/envConfig.ts b/src/infrastructure/config/envConfig.ts
new file mode 100644
index 0000000..d338056
--- /dev/null
+++ b/src/infrastructure/config/envConfig.ts
@@ -0,0 +1,4 @@
+export const envConfig = {
+ apiBaseUrl: import.meta.env.VITE_API_BASE_URL || "http://localhost:8010",
+ wsBaseUrl: import.meta.env.VITE_WS_BASE_URL || "ws://localhost:8010",
+};
diff --git a/src/infrastructure/ws/.gitkeep b/src/infrastructure/ws/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/tailwind.config.ts b/tailwind.config.ts
new file mode 100644
index 0000000..aa5c187
--- /dev/null
+++ b/tailwind.config.ts
@@ -0,0 +1,74 @@
+import type { Config } from "tailwindcss";
+import tailwindcssAnimate from "tailwindcss-animate";
+
+export default {
+ darkMode: ["class"],
+ content: ["./index.html", "./src/**/*.{ts,tsx}"],
+ theme: {
+ extend: {
+ colors: {
+ border: "hsl(var(--border))",
+ input: "hsl(var(--input))",
+ ring: "hsl(var(--ring))",
+ background: "hsl(var(--background))",
+ foreground: "hsl(var(--foreground))",
+ primary: {
+ DEFAULT: "hsl(var(--primary))",
+ foreground: "hsl(var(--primary-foreground))",
+ },
+ secondary: {
+ DEFAULT: "hsl(var(--secondary))",
+ foreground: "hsl(var(--secondary-foreground))",
+ },
+ destructive: {
+ DEFAULT: "hsl(var(--destructive))",
+ foreground: "hsl(var(--destructive-foreground))",
+ },
+ muted: {
+ DEFAULT: "hsl(var(--muted))",
+ foreground: "hsl(var(--muted-foreground))",
+ },
+ accent: {
+ DEFAULT: "hsl(var(--accent))",
+ foreground: "hsl(var(--accent-foreground))",
+ },
+ popover: {
+ DEFAULT: "hsl(var(--popover))",
+ foreground: "hsl(var(--popover-foreground))",
+ },
+ card: {
+ DEFAULT: "hsl(var(--card))",
+ foreground: "hsl(var(--card-foreground))",
+ },
+ "surface": "#f8f9fa",
+ "on-surface": "#191c1d",
+ "on-surface-variant": "#48464d",
+ "surface-container": "#edeeef",
+ "surface-container-low": "#f3f4f5",
+ "surface-container-high": "#e7e8e9",
+ "surface-container-highest": "#e1e3e4",
+ "surface-container-lowest": "#ffffff",
+ "outline": "#79767e",
+ "outline-variant": "#c9c5ce",
+ "primary-container": "#1c1835",
+ "on-primary-container": "#8580a3",
+ "secondary-brand": "#b3290d",
+ "secondary-container": "#fe5e3e",
+ "on-secondary-container": "#5c0b00",
+ "primary-fixed": "#e5deff",
+ "secondary-fixed": "#ffdad3",
+ "tertiary-fixed": "#e1e0ff",
+ },
+ fontFamily: {
+ headline: ["Space Grotesk", "sans-serif"],
+ body: ["Inter", "sans-serif"],
+ },
+ borderRadius: {
+ lg: "var(--radius)",
+ md: "calc(var(--radius) - 2px)",
+ sm: "calc(var(--radius) - 4px)",
+ },
+ },
+ },
+ plugins: [tailwindcssAnimate],
+} satisfies Config;
diff --git a/tests/fixtures/external.ts b/tests/fixtures/external.ts
new file mode 100644
index 0000000..bdbfe01
--- /dev/null
+++ b/tests/fixtures/external.ts
@@ -0,0 +1,44 @@
+import { AgentConfigMetadata } from "@/domain/entities/agent/agentConfigMetadata";
+import { Thread } from "@/domain/entities/chat/thread";
+import {
+ Message,
+ MessageRole,
+ MessageStatus,
+} from "@/domain/entities/chat/message";
+
+export function createAgentConfigMetadata(
+ overrides: Partial = {},
+): AgentConfigMetadata {
+ return {
+ name: "test-agent",
+ model: "openai:anthropic/claude-haiku-4.5:nitro",
+ minio_path: "composable-agents/test-agent.yaml",
+ is_builtin: false,
+ created_at: "2026-04-06T10:00:00Z",
+ updated_at: "2026-04-06T10:00:00Z",
+ ...overrides,
+ };
+}
+
+export function createThread(overrides: Partial = {}): Thread {
+ return {
+ id: "thread-123",
+ agent_name: "test-agent",
+ messages: [],
+ created_at: "2026-04-06T10:00:00Z",
+ updated_at: "2026-04-06T10:00:00Z",
+ ...overrides,
+ };
+}
+
+export function createMessage(overrides: Partial = {}): Message {
+ return {
+ role: MessageRole.AI,
+ content: "Hello, I am your assistant.",
+ timestamp: "2026-04-06T10:00:00Z",
+ tool_calls: null,
+ status: MessageStatus.COMPLETED,
+ structured_response: null,
+ ...overrides,
+ };
+}
diff --git a/tests/setup.ts b/tests/setup.ts
new file mode 100644
index 0000000..a01459d
--- /dev/null
+++ b/tests/setup.ts
@@ -0,0 +1,90 @@
+import "@testing-library/jest-dom";
+import { cleanup } from "@testing-library/react";
+import { afterEach, vi } from "vitest";
+
+afterEach(() => {
+ cleanup();
+});
+
+// Mock localStorage
+const localStorageMock = (() => {
+ let store: Record = {};
+ return {
+ getItem: (key: string) => store[key] || null,
+ setItem: (key: string, value: string) => {
+ store[key] = value;
+ },
+ removeItem: (key: string) => {
+ delete store[key];
+ },
+ clear: () => {
+ store = {};
+ },
+ };
+})();
+
+Object.defineProperty(window, "localStorage", { value: localStorageMock });
+
+// Mock ResizeObserver (needed for Radix UI)
+globalThis.ResizeObserver = class ResizeObserver {
+ observe() {}
+ unobserve() {}
+ disconnect() {}
+};
+
+// Mock window.matchMedia
+Object.defineProperty(window, "matchMedia", {
+ writable: true,
+ value: vi.fn().mockImplementation((query: string) => ({
+ matches: false,
+ media: query,
+ onchange: null,
+ addListener: vi.fn(),
+ removeListener: vi.fn(),
+ addEventListener: vi.fn(),
+ removeEventListener: vi.fn(),
+ dispatchEvent: vi.fn(),
+ })),
+});
+
+// Mock framer-motion
+vi.mock("framer-motion", async () => {
+ const actual = await vi.importActual("framer-motion");
+ return {
+ ...(actual as object),
+ AnimatePresence: ({ children }: { children: React.ReactNode }) => children,
+ motion: new Proxy(
+ {},
+ {
+ get: (_target, prop) => {
+ if (typeof prop === "string") {
+ return (props: Record) => {
+ const { children, ...rest } = props;
+ const filteredProps: Record = {};
+ for (const [key, value] of Object.entries(rest)) {
+ if (
+ ![
+ "animate",
+ "initial",
+ "exit",
+ "transition",
+ "variants",
+ "whileHover",
+ "whileTap",
+ "whileInView",
+ "layout",
+ "layoutId",
+ ].includes(key)
+ ) {
+ filteredProps[key] = value;
+ }
+ }
+ return { type: prop, props: { ...filteredProps, children } };
+ };
+ }
+ return undefined;
+ },
+ },
+ ),
+ };
+});
diff --git a/tests/unit/components/agent/AgentCard.test.tsx b/tests/unit/components/agent/AgentCard.test.tsx
new file mode 100644
index 0000000..da8a1f7
--- /dev/null
+++ b/tests/unit/components/agent/AgentCard.test.tsx
@@ -0,0 +1,45 @@
+import { screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { describe, it, expect, vi } from "vitest";
+import { renderWithProviders } from "../../../utils/render";
+import AgentCard from "@/application/components/agent/AgentCard";
+import { createAgentConfigMetadata } from "../../../fixtures/external";
+
+describe("AgentCard", () => {
+ it("renders agent name", () => {
+ const agent = createAgentConfigMetadata({ name: "research-agent" });
+
+ renderWithProviders();
+
+ expect(screen.getByText("research-agent")).toBeInTheDocument();
+ });
+
+ it("renders agent model", () => {
+ const agent = createAgentConfigMetadata({ model: "openai:gpt-4o" });
+
+ renderWithProviders();
+
+ expect(screen.getByText("openai:gpt-4o")).toBeInTheDocument();
+ });
+
+ it("calls onConfigure when Configure button is clicked", async () => {
+ const user = userEvent.setup();
+ const agent = createAgentConfigMetadata({ name: "my-agent" });
+ const onConfigure = vi.fn();
+
+ renderWithProviders();
+
+ await user.click(screen.getByRole("button", { name: /configure/i }));
+
+ expect(onConfigure).toHaveBeenCalledOnce();
+ expect(onConfigure).toHaveBeenCalledWith("my-agent");
+ });
+
+ it("shows status badge", () => {
+ const agent = createAgentConfigMetadata({ is_builtin: true });
+
+ renderWithProviders();
+
+ expect(screen.getByText("Active")).toBeInTheDocument();
+ });
+});
diff --git a/tests/unit/components/agent/AgentGrid.test.tsx b/tests/unit/components/agent/AgentGrid.test.tsx
new file mode 100644
index 0000000..f9b9896
--- /dev/null
+++ b/tests/unit/components/agent/AgentGrid.test.tsx
@@ -0,0 +1,79 @@
+import { screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { describe, it, expect, vi } from "vitest";
+import { renderWithProviders } from "../../../utils/render";
+import AgentGrid from "@/application/components/agent/AgentGrid";
+import { createAgentConfigMetadata } from "../../../fixtures/external";
+
+vi.mock("@/application/hooks/agent/useAgents");
+
+import { useAgents } from "@/application/hooks/agent/useAgents";
+
+const mockedUseAgents = vi.mocked(useAgents);
+
+describe("AgentGrid", () => {
+ it("shows loading text when loading", () => {
+ mockedUseAgents.mockReturnValue({
+ data: undefined,
+ isLoading: true,
+ error: null,
+ } as ReturnType);
+
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByText("Loading agents...")).toBeInTheDocument();
+ });
+
+ it("renders agent cards when data is available", () => {
+ const agents = [
+ createAgentConfigMetadata({ name: "agent-alpha" }),
+ createAgentConfigMetadata({ name: "agent-beta" }),
+ ];
+ mockedUseAgents.mockReturnValue({
+ data: agents,
+ isLoading: false,
+ error: null,
+ } as ReturnType);
+
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByText("agent-alpha")).toBeInTheDocument();
+ expect(screen.getByText("agent-beta")).toBeInTheDocument();
+ });
+
+ it("shows New Agent Template card", () => {
+ mockedUseAgents.mockReturnValue({
+ data: [],
+ isLoading: false,
+ error: null,
+ } as ReturnType);
+
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByText("New Agent Template")).toBeInTheDocument();
+ });
+
+ it("calls onCreateNew when template card is clicked", async () => {
+ const user = userEvent.setup();
+ mockedUseAgents.mockReturnValue({
+ data: [],
+ isLoading: false,
+ error: null,
+ } as ReturnType);
+ const onCreateNew = vi.fn();
+
+ renderWithProviders(
+ ,
+ );
+
+ await user.click(screen.getByText("New Agent Template"));
+
+ expect(onCreateNew).toHaveBeenCalledOnce();
+ });
+});
diff --git a/tests/unit/components/chat/ChatInput.test.tsx b/tests/unit/components/chat/ChatInput.test.tsx
new file mode 100644
index 0000000..9c59e7a
--- /dev/null
+++ b/tests/unit/components/chat/ChatInput.test.tsx
@@ -0,0 +1,60 @@
+import { screen, waitFor } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { describe, it, expect, vi, beforeEach } from "vitest";
+import { renderWithProviders } from "../../../utils/render";
+import ChatInput from "@/application/components/chat/ChatInput";
+
+const mockStream = vi.fn();
+
+vi.mock("@/application/hooks/chat/useStreamChat", () => ({
+ useStreamChat: () => ({
+ stream: mockStream,
+ cancel: vi.fn(),
+ }),
+}));
+
+vi.mock("@/application/stores/useChatStore", () => ({
+ useChatStore: (selector: (state: { isStreaming: boolean }) => unknown) => {
+ const state = {
+ isStreaming: false,
+ };
+ return selector(state);
+ },
+}));
+
+describe("ChatInput", () => {
+ beforeEach(() => {
+ mockStream.mockClear();
+ });
+
+ it("renders input with correct placeholder", () => {
+ renderWithProviders();
+
+ expect(
+ screen.getByPlaceholderText("Orchestrate your next move..."),
+ ).toBeInTheDocument();
+ });
+
+ it("has a send button", () => {
+ renderWithProviders();
+
+ expect(screen.getByRole("button", { name: /send/i })).toBeInTheDocument();
+ });
+
+ it("clears input after submission", async () => {
+ const user = userEvent.setup();
+
+ renderWithProviders();
+
+ const textarea = screen.getByPlaceholderText(
+ "Orchestrate your next move...",
+ );
+ await user.type(textarea, "Hello agent");
+ await user.click(screen.getByRole("button", { name: /send/i }));
+
+ await waitFor(() => {
+ expect(textarea).toHaveValue("");
+ });
+ expect(mockStream).toHaveBeenCalledWith({ message: "Hello agent" });
+ });
+});
diff --git a/tests/unit/components/chat/ChatMessage.test.tsx b/tests/unit/components/chat/ChatMessage.test.tsx
new file mode 100644
index 0000000..faf34f9
--- /dev/null
+++ b/tests/unit/components/chat/ChatMessage.test.tsx
@@ -0,0 +1,64 @@
+import { screen } from "@testing-library/react";
+import { describe, it, expect } from "vitest";
+import { renderWithProviders } from "../../../utils/render";
+import ChatMessage from "@/application/components/chat/ChatMessage";
+import { createMessage } from "../../../fixtures/external";
+import { MessageRole, MessageStatus } from "@/domain/entities/chat/message";
+
+describe("ChatMessage", () => {
+ it("renders AI message with agent name and content", () => {
+ const message = createMessage({
+ role: MessageRole.AI,
+ content: "Hello from the agent",
+ });
+
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByText("research-bot")).toBeInTheDocument();
+ expect(screen.getByText("Hello from the agent")).toBeInTheDocument();
+ });
+
+ it("renders human message content", () => {
+ const message = createMessage({
+ role: MessageRole.HUMAN,
+ content: "What is the weather?",
+ status: null,
+ });
+
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByText("What is the weather?")).toBeInTheDocument();
+ });
+
+ it("shows status badge for AI messages with status", () => {
+ const message = createMessage({
+ role: MessageRole.AI,
+ content: "Processing your request",
+ status: MessageStatus.AWAITING_HITL,
+ });
+
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByText("awaiting_hitl")).toBeInTheDocument();
+ });
+
+ it("renders markdown content for AI messages", () => {
+ const message = createMessage({
+ role: MessageRole.AI,
+ content: "This is **bold** text",
+ });
+
+ renderWithProviders(
+ ,
+ );
+
+ const boldElement = screen.getByText("bold");
+ expect(boldElement.tagName).toBe("STRONG");
+ });
+});
diff --git a/tests/unit/components/shared/StatusBadge.test.tsx b/tests/unit/components/shared/StatusBadge.test.tsx
new file mode 100644
index 0000000..fef2eab
--- /dev/null
+++ b/tests/unit/components/shared/StatusBadge.test.tsx
@@ -0,0 +1,46 @@
+import { screen } from "@testing-library/react";
+import { describe, it, expect } from "vitest";
+import { renderWithProviders } from "../../../utils/render";
+import StatusBadge from "@/application/components/shared/StatusBadge";
+
+describe("StatusBadge", () => {
+ it("renders the status text in uppercase", () => {
+ renderWithProviders();
+
+ const badge = screen.getByText("Active");
+ expect(badge).toBeInTheDocument();
+ expect(badge).toHaveClass("uppercase");
+ });
+
+ it("applies green classes for 'Active' status", () => {
+ renderWithProviders();
+
+ const badge = screen.getByText("Active");
+ expect(badge).toHaveClass("bg-emerald-100");
+ expect(badge).toHaveClass("text-emerald-700");
+ });
+
+ it("applies green classes for 'completed' status", () => {
+ renderWithProviders();
+
+ const badge = screen.getByText("completed");
+ expect(badge).toHaveClass("bg-emerald-100");
+ expect(badge).toHaveClass("text-emerald-700");
+ });
+
+ it("applies orange classes for 'awaiting_hitl' status", () => {
+ renderWithProviders();
+
+ const badge = screen.getByText("awaiting_hitl");
+ expect(badge).toHaveClass("bg-orange-100");
+ expect(badge).toHaveClass("text-orange-700");
+ });
+
+ it("applies gray classes for unknown status", () => {
+ renderWithProviders();
+
+ const badge = screen.getByText("unknown");
+ expect(badge).toHaveClass("bg-slate-100");
+ expect(badge).toHaveClass("text-slate-600");
+ });
+});
diff --git a/tests/unit/components/shared/ToolTag.test.tsx b/tests/unit/components/shared/ToolTag.test.tsx
new file mode 100644
index 0000000..9d30d79
--- /dev/null
+++ b/tests/unit/components/shared/ToolTag.test.tsx
@@ -0,0 +1,12 @@
+import { screen } from "@testing-library/react";
+import { describe, it, expect } from "vitest";
+import { renderWithProviders } from "../../../utils/render";
+import ToolTag from "@/application/components/shared/ToolTag";
+
+describe("ToolTag", () => {
+ it("renders the label text", () => {
+ renderWithProviders();
+
+ expect(screen.getByText("web_search")).toBeInTheDocument();
+ });
+});
diff --git a/tests/unit/domain/entities/agentConfig.test.ts b/tests/unit/domain/entities/agentConfig.test.ts
new file mode 100644
index 0000000..a2ba56b
--- /dev/null
+++ b/tests/unit/domain/entities/agentConfig.test.ts
@@ -0,0 +1,30 @@
+import { describe, it, expect } from "vitest";
+import {
+ MiddlewareType,
+ BackendType,
+} from "@/domain/entities/agent/agentConfig";
+import { McpTransportType } from "@/domain/entities/agent/mcpServerConfig";
+
+describe("MiddlewareType", () => {
+ it("has the correct values", () => {
+ expect(MiddlewareType.TODO_LIST).toBe("todo_list");
+ expect(MiddlewareType.FILESYSTEM).toBe("filesystem");
+ expect(MiddlewareType.SUB_AGENT).toBe("sub_agent");
+ });
+});
+
+describe("BackendType", () => {
+ it("has the correct values", () => {
+ expect(BackendType.STATE).toBe("state");
+ expect(BackendType.STORE).toBe("store");
+ expect(BackendType.FILESYSTEM).toBe("filesystem");
+ expect(BackendType.COMPOSITE).toBe("composite");
+ });
+});
+
+describe("McpTransportType", () => {
+ it("has the correct values", () => {
+ expect(McpTransportType.STDIO).toBe("stdio");
+ expect(McpTransportType.HTTP).toBe("http");
+ });
+});
diff --git a/tests/unit/domain/entities/message.test.ts b/tests/unit/domain/entities/message.test.ts
new file mode 100644
index 0000000..68f992a
--- /dev/null
+++ b/tests/unit/domain/entities/message.test.ts
@@ -0,0 +1,18 @@
+import { describe, it, expect } from "vitest";
+import { MessageRole, MessageStatus } from "@/domain/entities/chat/message";
+
+describe("MessageRole", () => {
+ it("has the correct enum values", () => {
+ expect(MessageRole.HUMAN).toBe("human");
+ expect(MessageRole.AI).toBe("ai");
+ expect(MessageRole.SYSTEM).toBe("system");
+ expect(MessageRole.TOOL).toBe("tool");
+ });
+});
+
+describe("MessageStatus", () => {
+ it("has the correct enum values", () => {
+ expect(MessageStatus.COMPLETED).toBe("completed");
+ expect(MessageStatus.AWAITING_HITL).toBe("awaiting_hitl");
+ });
+});
diff --git a/tests/unit/infrastructure/api/agent/agentApi.test.ts b/tests/unit/infrastructure/api/agent/agentApi.test.ts
new file mode 100644
index 0000000..66d13a1
--- /dev/null
+++ b/tests/unit/infrastructure/api/agent/agentApi.test.ts
@@ -0,0 +1,101 @@
+import { vi, describe, it, expect, beforeEach } from "vitest";
+import { agentApi } from "@/infrastructure/api/agent/agentApi";
+import { apiClient } from "@/infrastructure/api/axiosInstance";
+import { createAgentConfigMetadata } from "../../../../fixtures/external";
+
+vi.mock("@/infrastructure/api/axiosInstance", () => ({
+ apiClient: {
+ get: vi.fn(),
+ post: vi.fn(),
+ put: vi.fn(),
+ delete: vi.fn(),
+ },
+}));
+
+describe("agentApi", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ describe("listAgents", () => {
+ it("fetches agents from GET /api/v1/agents", async () => {
+ const agents = [createAgentConfigMetadata()];
+ vi.mocked(apiClient.get).mockResolvedValue({ data: agents });
+
+ const result = await agentApi.listAgents();
+
+ expect(apiClient.get).toHaveBeenCalledWith("/api/v1/agents");
+ expect(result).toEqual(agents);
+ });
+ });
+
+ describe("getAgent", () => {
+ it("fetches agent config from GET /api/v1/agents/{name}", async () => {
+ const config = { name: "test-agent", model: "test-model" };
+ vi.mocked(apiClient.get).mockResolvedValue({ data: config });
+
+ const result = await agentApi.getAgent("test-agent");
+
+ expect(apiClient.get).toHaveBeenCalledWith("/api/v1/agents/test-agent");
+ expect(result).toEqual(config);
+ });
+
+ it("encodes agent name with special characters", async () => {
+ vi.mocked(apiClient.get).mockResolvedValue({ data: {} });
+
+ await agentApi.getAgent("my agent");
+
+ expect(apiClient.get).toHaveBeenCalledWith("/api/v1/agents/my%20agent");
+ });
+ });
+
+ describe("createAgent", () => {
+ it("posts FormData to POST /api/v1/agents", async () => {
+ const config = { name: "new-agent", model: "test" };
+ vi.mocked(apiClient.post).mockResolvedValue({ data: config });
+ const file = new File(["name: new-agent"], "agent.yaml", {
+ type: "application/yaml",
+ });
+
+ const result = await agentApi.createAgent("new-agent", file);
+
+ expect(apiClient.post).toHaveBeenCalledWith(
+ "/api/v1/agents",
+ expect.any(FormData),
+ { headers: { "Content-Type": "multipart/form-data" } },
+ );
+ expect(result).toEqual(config);
+ });
+ });
+
+ describe("updateAgent", () => {
+ it("puts FormData to PUT /api/v1/agents/{name}", async () => {
+ const config = { name: "test-agent", model: "updated" };
+ vi.mocked(apiClient.put).mockResolvedValue({ data: config });
+ const file = new File(["name: test-agent"], "agent.yaml", {
+ type: "application/yaml",
+ });
+
+ const result = await agentApi.updateAgent("test-agent", file);
+
+ expect(apiClient.put).toHaveBeenCalledWith(
+ "/api/v1/agents/test-agent",
+ expect.any(FormData),
+ { headers: { "Content-Type": "multipart/form-data" } },
+ );
+ expect(result).toEqual(config);
+ });
+ });
+
+ describe("deleteAgent", () => {
+ it("sends DELETE to /api/v1/agents/{name}", async () => {
+ vi.mocked(apiClient.delete).mockResolvedValue({});
+
+ await agentApi.deleteAgent("test-agent");
+
+ expect(apiClient.delete).toHaveBeenCalledWith(
+ "/api/v1/agents/test-agent",
+ );
+ });
+ });
+});
diff --git a/tests/unit/infrastructure/api/chat/chatApi.test.ts b/tests/unit/infrastructure/api/chat/chatApi.test.ts
new file mode 100644
index 0000000..f183b62
--- /dev/null
+++ b/tests/unit/infrastructure/api/chat/chatApi.test.ts
@@ -0,0 +1,102 @@
+import { vi, describe, it, expect, beforeEach } from "vitest";
+import { chatApi } from "@/infrastructure/api/chat/chatApi";
+import { apiClient } from "@/infrastructure/api/axiosInstance";
+import { createThread, createMessage } from "../../../../fixtures/external";
+
+vi.mock("@/infrastructure/api/axiosInstance", () => ({
+ apiClient: {
+ get: vi.fn(),
+ post: vi.fn(),
+ delete: vi.fn(),
+ },
+}));
+
+vi.mock("@microsoft/fetch-event-source", () => ({
+ fetchEventSource: vi.fn(),
+}));
+
+describe("chatApi", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ describe("createThread", () => {
+ it("posts to /api/v1/threads with agent_name", async () => {
+ const thread = createThread();
+ vi.mocked(apiClient.post).mockResolvedValue({ data: thread });
+
+ const result = await chatApi.createThread("test-agent");
+
+ expect(apiClient.post).toHaveBeenCalledWith("/api/v1/threads", {
+ agent_name: "test-agent",
+ });
+ expect(result).toEqual(thread);
+ });
+ });
+
+ describe("listThreads", () => {
+ it("fetches from GET /api/v1/threads", async () => {
+ const threads = [createThread()];
+ vi.mocked(apiClient.get).mockResolvedValue({ data: threads });
+
+ const result = await chatApi.listThreads();
+
+ expect(apiClient.get).toHaveBeenCalledWith("/api/v1/threads");
+ expect(result).toEqual(threads);
+ });
+ });
+
+ describe("getThread", () => {
+ it("fetches from GET /api/v1/threads/{id}", async () => {
+ const thread = createThread();
+ vi.mocked(apiClient.get).mockResolvedValue({ data: thread });
+
+ const result = await chatApi.getThread("thread-123");
+
+ expect(apiClient.get).toHaveBeenCalledWith("/api/v1/threads/thread-123");
+ expect(result).toEqual(thread);
+ });
+ });
+
+ describe("deleteThread", () => {
+ it("sends DELETE to /api/v1/threads/{id}", async () => {
+ vi.mocked(apiClient.delete).mockResolvedValue({});
+
+ await chatApi.deleteThread("thread-123");
+
+ expect(apiClient.delete).toHaveBeenCalledWith(
+ "/api/v1/threads/thread-123",
+ );
+ });
+ });
+
+ describe("getMessages", () => {
+ it("fetches from GET /api/v1/threads/{id}/messages", async () => {
+ const messages = [createMessage()];
+ vi.mocked(apiClient.get).mockResolvedValue({ data: messages });
+
+ const result = await chatApi.getMessages("thread-123");
+
+ expect(apiClient.get).toHaveBeenCalledWith(
+ "/api/v1/threads/thread-123/messages",
+ );
+ expect(result).toEqual(messages);
+ });
+ });
+
+ describe("sendMessage", () => {
+ it("posts ChatRequest to /api/v1/chat/{id}", async () => {
+ const message = createMessage();
+ vi.mocked(apiClient.post).mockResolvedValue({ data: message });
+
+ const result = await chatApi.sendMessage("thread-123", {
+ message: "Hello",
+ });
+
+ expect(apiClient.post).toHaveBeenCalledWith("/api/v1/chat/thread-123", {
+ message: "Hello",
+ });
+ expect(result).toEqual(message);
+ });
+ });
+});
diff --git a/tests/unit/pages/AgentsPage.test.tsx b/tests/unit/pages/AgentsPage.test.tsx
new file mode 100644
index 0000000..f4edd86
--- /dev/null
+++ b/tests/unit/pages/AgentsPage.test.tsx
@@ -0,0 +1,44 @@
+import { screen } from "@testing-library/react";
+import { describe, it, expect, vi } from "vitest";
+import { renderWithProviders } from "../../utils/render";
+import AgentsPage from "@/application/pages/AgentsPage";
+import { createAgentConfigMetadata } from "../../fixtures/external";
+
+vi.mock("@/application/hooks/agent/useAgents", () => ({
+ useAgents: () => ({
+ data: [createAgentConfigMetadata({ name: "my-agent", is_builtin: true })],
+ isLoading: false,
+ error: null,
+ }),
+}));
+
+vi.mock("@/application/hooks/agent/useCreateAgent", () => ({
+ useCreateAgent: () => ({
+ mutate: vi.fn(),
+ isPending: false,
+ }),
+}));
+
+describe("AgentsPage", () => {
+ it("renders Active Agents heading", () => {
+ renderWithProviders(, { initialEntries: ["/agents"] });
+
+ expect(
+ screen.getByRole("heading", { name: /active agents/i }),
+ ).toBeInTheDocument();
+ });
+
+ it("renders Create Agent button", () => {
+ renderWithProviders(, { initialEntries: ["/agents"] });
+
+ expect(
+ screen.getByRole("button", { name: /create agent/i }),
+ ).toBeInTheDocument();
+ });
+
+ it("shows agent grid with agents", () => {
+ renderWithProviders(, { initialEntries: ["/agents"] });
+
+ expect(screen.getByText("my-agent")).toBeInTheDocument();
+ });
+});
diff --git a/tests/unit/pages/ChatPage.test.tsx b/tests/unit/pages/ChatPage.test.tsx
new file mode 100644
index 0000000..1e5a9d3
--- /dev/null
+++ b/tests/unit/pages/ChatPage.test.tsx
@@ -0,0 +1,79 @@
+import { screen } from "@testing-library/react";
+import { describe, it, expect, vi } from "vitest";
+import { renderWithProviders } from "../../utils/render";
+import { Route, Routes } from "react-router-dom";
+import ChatPage from "@/application/pages/ChatPage";
+import { createMessage } from "../../fixtures/external";
+import { MessageRole } from "@/domain/entities/chat/message";
+
+vi.mock("@/application/hooks/chat/useThreads", () => ({
+ useThreads: () => ({
+ data: [
+ {
+ id: "thread-abc",
+ agent_name: "test-agent",
+ messages: [],
+ created_at: "2026-04-06T10:00:00Z",
+ updated_at: "2026-04-06T10:00:00Z",
+ },
+ ],
+ isLoading: false,
+ }),
+}));
+
+vi.mock("@/application/hooks/chat/useMessages", () => ({
+ useMessages: (threadId: string | null) => {
+ if (!threadId) return { data: undefined, isLoading: false };
+ return {
+ data: [
+ createMessage({
+ role: MessageRole.HUMAN,
+ content: "Hello there",
+ status: null,
+ }),
+ createMessage({
+ role: MessageRole.AI,
+ content: "Hi, how can I help?",
+ }),
+ ],
+ isLoading: false,
+ };
+ },
+}));
+
+vi.mock("@/application/hooks/chat/useStreamChat", () => ({
+ useStreamChat: () => ({
+ stream: vi.fn(),
+ cancel: vi.fn(),
+ }),
+}));
+
+describe("ChatPage", () => {
+ it("shows welcome message when no threadId", () => {
+ renderWithProviders(
+
+ } />
+ ,
+ { initialEntries: ["/chat"] },
+ );
+
+ expect(
+ screen.getByRole("heading", { name: /orchestration console/i }),
+ ).toBeInTheDocument();
+ expect(
+ screen.getByText(/select a thread from the sidebar/i),
+ ).toBeInTheDocument();
+ });
+
+ it("shows message list when threadId is provided", () => {
+ renderWithProviders(
+
+ } />
+ ,
+ { initialEntries: ["/chat/thread-abc"] },
+ );
+
+ expect(screen.getByText("Hello there")).toBeInTheDocument();
+ expect(screen.getByText("Hi, how can I help?")).toBeInTheDocument();
+ });
+});
diff --git a/tests/unit/smoke.test.ts b/tests/unit/smoke.test.ts
new file mode 100644
index 0000000..f802a20
--- /dev/null
+++ b/tests/unit/smoke.test.ts
@@ -0,0 +1,7 @@
+import { describe, it, expect } from "vitest";
+
+describe("smoke test", () => {
+ it("should pass", () => {
+ expect(true).toBe(true);
+ });
+});
diff --git a/tests/utils/render.tsx b/tests/utils/render.tsx
new file mode 100644
index 0000000..f40e188
--- /dev/null
+++ b/tests/utils/render.tsx
@@ -0,0 +1,42 @@
+import { render, RenderOptions } from "@testing-library/react";
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import { MemoryRouter } from "react-router-dom";
+import { ReactElement } from "react";
+
+function createTestQueryClient() {
+ return new QueryClient({
+ defaultOptions: {
+ queries: {
+ retry: false,
+ gcTime: 0,
+ },
+ mutations: {
+ retry: false,
+ },
+ },
+ });
+}
+
+interface CustomRenderOptions extends Omit {
+ initialEntries?: string[];
+}
+
+export function renderWithProviders(
+ ui: ReactElement,
+ { initialEntries = ["/"], ...options }: CustomRenderOptions = {},
+) {
+ const queryClient = createTestQueryClient();
+
+ function Wrapper({ children }: { children: React.ReactNode }) {
+ return (
+
+ {children}
+
+ );
+ }
+
+ return {
+ ...render(ui, { wrapper: Wrapper, ...options }),
+ queryClient,
+ };
+}
diff --git a/trivy.yaml b/trivy.yaml
new file mode 100644
index 0000000..f753968
--- /dev/null
+++ b/trivy.yaml
@@ -0,0 +1,11 @@
+severity:
+ - CRITICAL
+ - HIGH
+ - MEDIUM
+
+pkg:
+ types:
+ - os
+ - library
+
+ignorefile: .trivyignore
diff --git a/tsconfig.app.json b/tsconfig.app.json
new file mode 100644
index 0000000..2f5abd6
--- /dev/null
+++ b/tsconfig.app.json
@@ -0,0 +1,28 @@
+{
+ "compilerOptions": {
+ "target": "ES2021",
+ "useDefineForClassFields": true,
+ "lib": ["ES2021", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+ "composite": true,
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "isolatedModules": true,
+ "moduleDetection": "force",
+ "emitDeclarationOnly": true,
+ "declarationDir": "./dist/types",
+ "jsx": "react-jsx",
+ "strict": false,
+ "noUnusedLocals": false,
+ "noUnusedParameters": false,
+ "noImplicitAny": false,
+ "noFallthroughCasesInSwitch": false,
+ "ignoreDeprecations": "6.0",
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["./src/*"]
+ }
+ },
+ "include": ["src"]
+}
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..ad1f426
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,18 @@
+{
+ "files": [],
+ "references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }],
+ "compilerOptions": {
+ "ignoreDeprecations": "6.0",
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["./src/*"]
+ },
+ "noImplicitAny": false,
+ "noUnusedParameters": false,
+ "skipLibCheck": true,
+ "allowJs": true,
+ "noUnusedLocals": false,
+ "strictNullChecks": false
+ },
+ "include": ["src"]
+}
diff --git a/tsconfig.node.json b/tsconfig.node.json
new file mode 100644
index 0000000..a624139
--- /dev/null
+++ b/tsconfig.node.json
@@ -0,0 +1,20 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "lib": ["ES2023"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "isolatedModules": true,
+ "moduleDetection": "force",
+ "noEmit": false,
+ "emitDeclarationOnly": true,
+ "composite": true,
+ "strict": true,
+ "noUnusedLocals": false,
+ "noUnusedParameters": false,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["vite.config.ts", "vitest.config.ts"]
+}
diff --git a/vite.config.ts b/vite.config.ts
new file mode 100644
index 0000000..9246bfb
--- /dev/null
+++ b/vite.config.ts
@@ -0,0 +1,26 @@
+import react from "@vitejs/plugin-react-swc";
+import path from "path";
+import { defineConfig } from "vite";
+
+export default defineConfig(() => ({
+ server: {
+ host: "localhost",
+ port: 8030,
+ proxy: {
+ "/api": {
+ target: "http://localhost:8010",
+ changeOrigin: true,
+ },
+ "/api/v1/ws": {
+ target: "ws://localhost:8010",
+ ws: true,
+ },
+ },
+ },
+ plugins: [react()].filter(Boolean),
+ resolve: {
+ alias: {
+ "@": path.resolve(__dirname, "./src"),
+ },
+ },
+}));
diff --git a/vitest.config.ts b/vitest.config.ts
new file mode 100644
index 0000000..46efeec
--- /dev/null
+++ b/vitest.config.ts
@@ -0,0 +1,37 @@
+import { defineConfig } from 'vitest/config';
+import react from '@vitejs/plugin-react-swc';
+import path from 'path';
+
+export default defineConfig({
+ plugins: [react()] as any,
+ test: {
+ globals: true,
+ environment: 'jsdom',
+ environmentOptions: {
+ jsdom: {
+ url: 'http://localhost:3000',
+ },
+ },
+ setupFiles: './tests/setup.ts',
+ css: true,
+ coverage: {
+ provider: 'v8',
+ reporter: ['text', 'lcov'],
+ reportsDirectory: './coverage',
+ exclude: [
+ '**/node_modules/**',
+ '**/dist/**',
+ '**/*.test.ts',
+ '**/*.test.tsx',
+ '**/main.tsx',
+ '**/components/ui/**',
+ 'tests/**',
+ ],
+ },
+ },
+ resolve: {
+ alias: {
+ '@': path.resolve(__dirname, './src'),
+ },
+ },
+});
From 7f7300725cada842abae875ad20f5421bc268e52 Mon Sep 17 00:00:00 2001
From: Kaiohz
Date: Mon, 6 Apr 2026 08:23:14 +0200
Subject: [PATCH 2/9] fix: agent selection dialog, optimistic messages,
stream/standard toggle
- Add agent selection dialog in ThreadSidebar for new conversations
- Show user message immediately (optimistic rendering) before API response
- Add stream/standard mode toggle in ChatInput
- Default to standard mode (streaming has backend issues)
- Show typing indicator during streaming
- 61 tests passing
---
src/application/components/chat/ChatInput.tsx | 46 +++++-
.../components/chat/MessageList.tsx | 45 ++++--
.../components/layout/ThreadSidebar.tsx | 61 ++++++-
src/application/hooks/chat/useStreamChat.ts | 3 +
src/application/stores/useChatStore.ts | 12 +-
tests/unit/components/chat/ChatInput.test.tsx | 38 +++--
.../components/layout/ThreadSidebar.test.tsx | 149 ++++++++++++++++++
tests/unit/stores/useChatStore.test.ts | 81 ++++++++++
8 files changed, 403 insertions(+), 32 deletions(-)
create mode 100644 tests/unit/components/layout/ThreadSidebar.test.tsx
create mode 100644 tests/unit/stores/useChatStore.test.ts
diff --git a/src/application/components/chat/ChatInput.tsx b/src/application/components/chat/ChatInput.tsx
index 62ff352..610c023 100644
--- a/src/application/components/chat/ChatInput.tsx
+++ b/src/application/components/chat/ChatInput.tsx
@@ -1,6 +1,8 @@
import { useState, type FormEvent, type KeyboardEvent } from "react";
+import { useQueryClient } from "@tanstack/react-query";
import { cn } from "@/application/lib/utils";
import { useStreamChat } from "@/application/hooks/chat/useStreamChat";
+import { useSendMessage } from "@/application/hooks/chat/useSendMessage";
import { useChatStore } from "@/application/stores/useChatStore";
interface ChatInputProps {
@@ -10,14 +12,39 @@ interface ChatInputProps {
export default function ChatInput({ threadId }: ChatInputProps) {
const [input, setInput] = useState("");
const { stream } = useStreamChat(threadId);
+ const sendMessage = useSendMessage(threadId);
+ const queryClient = useQueryClient();
const isStreaming = useChatStore((s) => s.isStreaming);
+ const streamingMode = useChatStore((s) => s.useStreaming);
+ const toggleStreaming = useChatStore((s) => s.toggleStreaming);
function handleSubmit(e?: FormEvent) {
e?.preventDefault();
const trimmed = input.trim();
if (!trimmed || isStreaming) return;
- stream({ message: trimmed });
+ if (streamingMode) {
+ stream({ message: trimmed });
+ } else {
+ useChatStore.getState().setPendingUserMessage(trimmed);
+ useChatStore.getState().setStreaming(true);
+ sendMessage.mutate(
+ { message: trimmed },
+ {
+ onSuccess: () => {
+ useChatStore.getState().setPendingUserMessage(null);
+ useChatStore.getState().setStreaming(false);
+ queryClient.invalidateQueries({
+ queryKey: ["messages", threadId],
+ });
+ },
+ onError: () => {
+ useChatStore.getState().setPendingUserMessage(null);
+ useChatStore.getState().setStreaming(false);
+ },
+ },
+ );
+ }
setInput("");
}
@@ -44,6 +71,21 @@ export default function ChatInput({ threadId }: ChatInputProps) {
"disabled:opacity-50",
)}
/>
+
)}
+
+ {/* Agent selection dialog */}
+ {showAgentDialog && (
+ setShowAgentDialog(false)}
+ >
+
e.stopPropagation()}
+ >
+
+ Choose an Agent
+
+ {agentsLoading ? (
+
+ Loading agents...
+
+ ) : (
+
+ {agents?.map((agent) => (
+
+ ))}
+
+ )}
+
+
+ )}
);
}
diff --git a/src/application/hooks/chat/useStreamChat.ts b/src/application/hooks/chat/useStreamChat.ts
index 3d5616c..0528cd5 100644
--- a/src/application/hooks/chat/useStreamChat.ts
+++ b/src/application/hooks/chat/useStreamChat.ts
@@ -15,6 +15,7 @@ export function useStreamChat(threadId: string | null) {
clearStream();
setStreaming(true);
+ useChatStore.getState().setPendingUserMessage(request.message ?? null);
abortRef.current = chatApi.streamMessage(
threadId,
@@ -23,11 +24,13 @@ export function useStreamChat(threadId: string | null) {
appendStreamChunk(chunk);
},
() => {
+ useChatStore.getState().setPendingUserMessage(null);
setStreaming(false);
queryClient.invalidateQueries({ queryKey: ["messages", threadId] });
},
(error) => {
console.error("Stream error:", error);
+ useChatStore.getState().setPendingUserMessage(null);
setStreaming(false);
},
);
diff --git a/src/application/stores/useChatStore.ts b/src/application/stores/useChatStore.ts
index e5d219c..a88b4de 100644
--- a/src/application/stores/useChatStore.ts
+++ b/src/application/stores/useChatStore.ts
@@ -4,19 +4,29 @@ interface ChatState {
activeThreadId: string | null;
streamingContent: string;
isStreaming: boolean;
+ pendingUserMessage: string | null;
+ useStreaming: boolean;
setActiveThread: (id: string | null) => void;
appendStreamChunk: (chunk: string) => void;
clearStream: () => void;
setStreaming: (streaming: boolean) => void;
+ setPendingUserMessage: (msg: string | null) => void;
+ toggleStreaming: () => void;
}
export const useChatStore = create((set) => ({
activeThreadId: null,
streamingContent: "",
isStreaming: false,
+ pendingUserMessage: null,
+ useStreaming: false,
setActiveThread: (id) => set({ activeThreadId: id }),
appendStreamChunk: (chunk) =>
set((state) => ({ streamingContent: state.streamingContent + chunk })),
- clearStream: () => set({ streamingContent: "", isStreaming: false }),
+ clearStream: () =>
+ set({ streamingContent: "", isStreaming: false, pendingUserMessage: null }),
setStreaming: (streaming) => set({ isStreaming: streaming }),
+ setPendingUserMessage: (msg) => set({ pendingUserMessage: msg }),
+ toggleStreaming: () =>
+ set((state) => ({ useStreaming: !state.useStreaming })),
}));
diff --git a/tests/unit/components/chat/ChatInput.test.tsx b/tests/unit/components/chat/ChatInput.test.tsx
index 9c59e7a..be7a5a4 100644
--- a/tests/unit/components/chat/ChatInput.test.tsx
+++ b/tests/unit/components/chat/ChatInput.test.tsx
@@ -4,7 +4,18 @@ import { describe, it, expect, vi, beforeEach } from "vitest";
import { renderWithProviders } from "../../../utils/render";
import ChatInput from "@/application/components/chat/ChatInput";
-const mockStream = vi.fn();
+const { mockStream, mockSendMessageMutate, mockStoreState } = vi.hoisted(() => {
+ const mockStream = vi.fn();
+ const mockSendMessageMutate = vi.fn();
+ const mockStoreState = {
+ isStreaming: false,
+ useStreaming: false,
+ toggleStreaming: vi.fn(),
+ setPendingUserMessage: vi.fn(),
+ setStreaming: vi.fn(),
+ };
+ return { mockStream, mockSendMessageMutate, mockStoreState };
+});
vi.mock("@/application/hooks/chat/useStreamChat", () => ({
useStreamChat: () => ({
@@ -13,15 +24,21 @@ vi.mock("@/application/hooks/chat/useStreamChat", () => ({
}),
}));
-vi.mock("@/application/stores/useChatStore", () => ({
- useChatStore: (selector: (state: { isStreaming: boolean }) => unknown) => {
- const state = {
- isStreaming: false,
- };
- return selector(state);
- },
+vi.mock("@/application/hooks/chat/useSendMessage", () => ({
+ useSendMessage: () => ({
+ mutate: mockSendMessageMutate,
+ isPending: false,
+ }),
}));
+vi.mock("@/application/stores/useChatStore", () => {
+ const fn = (selector: (state: typeof mockStoreState) => unknown) => {
+ return selector(mockStoreState);
+ };
+ fn.getState = () => mockStoreState;
+ return { useChatStore: fn };
+});
+
describe("ChatInput", () => {
beforeEach(() => {
mockStream.mockClear();
@@ -55,6 +72,9 @@ describe("ChatInput", () => {
await waitFor(() => {
expect(textarea).toHaveValue("");
});
- expect(mockStream).toHaveBeenCalledWith({ message: "Hello agent" });
+ expect(mockSendMessageMutate).toHaveBeenCalledWith(
+ { message: "Hello agent" },
+ expect.any(Object),
+ );
});
});
diff --git a/tests/unit/components/layout/ThreadSidebar.test.tsx b/tests/unit/components/layout/ThreadSidebar.test.tsx
new file mode 100644
index 0000000..7ba2fa5
--- /dev/null
+++ b/tests/unit/components/layout/ThreadSidebar.test.tsx
@@ -0,0 +1,149 @@
+import { screen, waitFor } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { describe, it, expect, vi, beforeEach } from "vitest";
+import { renderWithProviders } from "../../../utils/render";
+import ThreadSidebar from "@/application/components/layout/ThreadSidebar";
+import {
+ createThread,
+ createAgentConfigMetadata,
+} from "../../../fixtures/external";
+
+const { mockThreadsData, mockAgentsData, mockCreateThreadMutate, mockSetActiveThread } =
+ vi.hoisted(() => {
+ return {
+ mockThreadsData: {
+ data: undefined as ReturnType[] | undefined,
+ isLoading: false,
+ },
+ mockAgentsData: {
+ data: undefined as ReturnType[] | undefined,
+ isLoading: false,
+ },
+ mockCreateThreadMutate: vi.fn(),
+ mockSetActiveThread: vi.fn(),
+ };
+ });
+
+vi.mock("@/application/hooks/chat/useThreads", () => ({
+ useThreads: () => mockThreadsData,
+}));
+
+vi.mock("@/application/hooks/agent/useAgents", () => ({
+ useAgents: () => mockAgentsData,
+}));
+
+vi.mock("@/application/hooks/chat/useCreateThread", () => ({
+ useCreateThread: () => ({
+ mutate: mockCreateThreadMutate,
+ isPending: false,
+ }),
+}));
+
+vi.mock("@/application/stores/useChatStore", () => {
+ const fn = (selector: (state: { setActiveThread: typeof mockSetActiveThread }) => unknown) => {
+ return selector({ setActiveThread: mockSetActiveThread });
+ };
+ fn.getState = () => ({ setActiveThread: mockSetActiveThread });
+ return { useChatStore: fn };
+});
+
+describe("ThreadSidebar", () => {
+ beforeEach(() => {
+ mockThreadsData.data = undefined;
+ mockThreadsData.isLoading = false;
+ mockAgentsData.data = undefined;
+ mockAgentsData.isLoading = false;
+ mockCreateThreadMutate.mockClear();
+ mockSetActiveThread.mockClear();
+ });
+
+ it("renders 'New Conversation' button", () => {
+ renderWithProviders();
+
+ expect(
+ screen.getByRole("button", { name: /new conversation/i }),
+ ).toBeInTheDocument();
+ });
+
+ it("clicking 'New Conversation' shows agent selection dialog", async () => {
+ mockAgentsData.data = [
+ createAgentConfigMetadata({ name: "agent-alpha", model: "gpt-4" }),
+ ];
+ const user = userEvent.setup();
+
+ renderWithProviders();
+ await user.click(screen.getByRole("button", { name: /new conversation/i }));
+
+ expect(screen.getByText("Choose an Agent")).toBeInTheDocument();
+ });
+
+ it("agent list shows available agents in the dialog", async () => {
+ mockAgentsData.data = [
+ createAgentConfigMetadata({ name: "agent-alpha", model: "gpt-4" }),
+ createAgentConfigMetadata({ name: "agent-beta", model: "claude-sonnet" }),
+ ];
+ const user = userEvent.setup();
+
+ renderWithProviders();
+ await user.click(screen.getByRole("button", { name: /new conversation/i }));
+
+ expect(screen.getByText("agent-alpha")).toBeInTheDocument();
+ expect(screen.getByText("gpt-4")).toBeInTheDocument();
+ expect(screen.getByText("agent-beta")).toBeInTheDocument();
+ expect(screen.getByText("claude-sonnet")).toBeInTheDocument();
+ });
+
+ it("selecting an agent calls createThread with the agent name", async () => {
+ mockAgentsData.data = [
+ createAgentConfigMetadata({ name: "agent-alpha", model: "gpt-4" }),
+ ];
+ const user = userEvent.setup();
+
+ renderWithProviders();
+ await user.click(screen.getByRole("button", { name: /new conversation/i }));
+ await user.click(screen.getByText("agent-alpha"));
+
+ expect(mockCreateThreadMutate).toHaveBeenCalledWith(
+ "agent-alpha",
+ expect.objectContaining({ onSuccess: expect.any(Function) }),
+ );
+ });
+
+ it("shows 'No conversations yet' when threads list is empty", () => {
+ mockThreadsData.data = [];
+
+ renderWithProviders();
+
+ expect(screen.getByText("No conversations yet")).toBeInTheDocument();
+ });
+
+ it("renders thread list grouped by agent name", () => {
+ mockThreadsData.data = [
+ createThread({ id: "t-1", agent_name: "agent-alpha" }),
+ createThread({ id: "t-2", agent_name: "agent-beta" }),
+ ];
+
+ renderWithProviders();
+
+ expect(screen.getByText("agent-alpha")).toBeInTheDocument();
+ expect(screen.getByText("agent-beta")).toBeInTheDocument();
+ });
+
+ it("shows loading text while threads are loading", () => {
+ mockThreadsData.isLoading = true;
+
+ renderWithProviders();
+
+ expect(screen.getByText("Loading threads...")).toBeInTheDocument();
+ });
+
+ it("shows loading text while agents are loading in the dialog", async () => {
+ mockAgentsData.isLoading = true;
+ const user = userEvent.setup();
+
+ renderWithProviders();
+ await user.click(screen.getByRole("button", { name: /new conversation/i }));
+
+ expect(screen.getByText("Loading agents...")).toBeInTheDocument();
+ });
+});
diff --git a/tests/unit/stores/useChatStore.test.ts b/tests/unit/stores/useChatStore.test.ts
new file mode 100644
index 0000000..cd9df1b
--- /dev/null
+++ b/tests/unit/stores/useChatStore.test.ts
@@ -0,0 +1,81 @@
+import { describe, it, expect, beforeEach } from "vitest";
+import { useChatStore } from "@/application/stores/useChatStore";
+
+describe("useChatStore", () => {
+ beforeEach(() => {
+ useChatStore.setState({
+ activeThreadId: null,
+ streamingContent: "",
+ isStreaming: false,
+ pendingUserMessage: null,
+ useStreaming: false,
+ });
+ });
+
+ it("has pendingUserMessage=null in initial state", () => {
+ const state = useChatStore.getState();
+ expect(state.pendingUserMessage).toBeNull();
+ });
+
+ it("has useStreaming=false in initial state", () => {
+ const state = useChatStore.getState();
+ expect(state.useStreaming).toBe(false);
+ });
+
+ it("setPendingUserMessage updates state", () => {
+ useChatStore.getState().setPendingUserMessage("Hello world");
+
+ expect(useChatStore.getState().pendingUserMessage).toBe("Hello world");
+ });
+
+ it("setPendingUserMessage clears with null", () => {
+ useChatStore.getState().setPendingUserMessage("Some message");
+ useChatStore.getState().setPendingUserMessage(null);
+
+ expect(useChatStore.getState().pendingUserMessage).toBeNull();
+ });
+
+ it("toggleStreaming toggles useStreaming from false to true", () => {
+ expect(useChatStore.getState().useStreaming).toBe(false);
+
+ useChatStore.getState().toggleStreaming();
+
+ expect(useChatStore.getState().useStreaming).toBe(true);
+ });
+
+ it("toggleStreaming toggles useStreaming from true to false", () => {
+ useChatStore.setState({ useStreaming: true });
+
+ useChatStore.getState().toggleStreaming();
+
+ expect(useChatStore.getState().useStreaming).toBe(false);
+ });
+
+ it("clearStream also clears pendingUserMessage", () => {
+ useChatStore.setState({
+ streamingContent: "partial content",
+ isStreaming: true,
+ pendingUserMessage: "user typed this",
+ });
+
+ useChatStore.getState().clearStream();
+
+ const state = useChatStore.getState();
+ expect(state.streamingContent).toBe("");
+ expect(state.isStreaming).toBe(false);
+ expect(state.pendingUserMessage).toBeNull();
+ });
+
+ it("appendStreamChunk appends to streamingContent", () => {
+ useChatStore.getState().appendStreamChunk("Hello ");
+ useChatStore.getState().appendStreamChunk("world");
+
+ expect(useChatStore.getState().streamingContent).toBe("Hello world");
+ });
+
+ it("setActiveThread updates activeThreadId", () => {
+ useChatStore.getState().setActiveThread("thread-42");
+
+ expect(useChatStore.getState().activeThreadId).toBe("thread-42");
+ });
+});
From 1c39b11d20efbb76d47e8a17b89bd9fd5f9ae367 Mon Sep 17 00:00:00 2001
From: Kaiohz
Date: Mon, 6 Apr 2026 08:26:59 +0200
Subject: [PATCH 3/9] fix: restore streaming as default mode
Backend streaming fix applied (composable-agents PR #8).
Set useStreaming=true as default in useChatStore.
---
src/application/stores/useChatStore.ts | 2 +-
tests/unit/components/chat/ChatInput.test.tsx | 7 ++-----
tests/unit/stores/useChatStore.test.ts | 18 +++++++++---------
3 files changed, 12 insertions(+), 15 deletions(-)
diff --git a/src/application/stores/useChatStore.ts b/src/application/stores/useChatStore.ts
index a88b4de..2755762 100644
--- a/src/application/stores/useChatStore.ts
+++ b/src/application/stores/useChatStore.ts
@@ -19,7 +19,7 @@ export const useChatStore = create((set) => ({
streamingContent: "",
isStreaming: false,
pendingUserMessage: null,
- useStreaming: false,
+ useStreaming: true,
setActiveThread: (id) => set({ activeThreadId: id }),
appendStreamChunk: (chunk) =>
set((state) => ({ streamingContent: state.streamingContent + chunk })),
diff --git a/tests/unit/components/chat/ChatInput.test.tsx b/tests/unit/components/chat/ChatInput.test.tsx
index be7a5a4..1612a5a 100644
--- a/tests/unit/components/chat/ChatInput.test.tsx
+++ b/tests/unit/components/chat/ChatInput.test.tsx
@@ -9,7 +9,7 @@ const { mockStream, mockSendMessageMutate, mockStoreState } = vi.hoisted(() => {
const mockSendMessageMutate = vi.fn();
const mockStoreState = {
isStreaming: false,
- useStreaming: false,
+ useStreaming: true,
toggleStreaming: vi.fn(),
setPendingUserMessage: vi.fn(),
setStreaming: vi.fn(),
@@ -72,9 +72,6 @@ describe("ChatInput", () => {
await waitFor(() => {
expect(textarea).toHaveValue("");
});
- expect(mockSendMessageMutate).toHaveBeenCalledWith(
- { message: "Hello agent" },
- expect.any(Object),
- );
+ expect(mockStream).toHaveBeenCalledWith({ message: "Hello agent" });
});
});
diff --git a/tests/unit/stores/useChatStore.test.ts b/tests/unit/stores/useChatStore.test.ts
index cd9df1b..05afccc 100644
--- a/tests/unit/stores/useChatStore.test.ts
+++ b/tests/unit/stores/useChatStore.test.ts
@@ -8,7 +8,7 @@ describe("useChatStore", () => {
streamingContent: "",
isStreaming: false,
pendingUserMessage: null,
- useStreaming: false,
+ useStreaming: true,
});
});
@@ -17,9 +17,9 @@ describe("useChatStore", () => {
expect(state.pendingUserMessage).toBeNull();
});
- it("has useStreaming=false in initial state", () => {
+ it("has useStreaming=true in initial state", () => {
const state = useChatStore.getState();
- expect(state.useStreaming).toBe(false);
+ expect(state.useStreaming).toBe(true);
});
it("setPendingUserMessage updates state", () => {
@@ -35,20 +35,20 @@ describe("useChatStore", () => {
expect(useChatStore.getState().pendingUserMessage).toBeNull();
});
- it("toggleStreaming toggles useStreaming from false to true", () => {
- expect(useChatStore.getState().useStreaming).toBe(false);
+ it("toggleStreaming toggles useStreaming from true to false", () => {
+ expect(useChatStore.getState().useStreaming).toBe(true);
useChatStore.getState().toggleStreaming();
- expect(useChatStore.getState().useStreaming).toBe(true);
+ expect(useChatStore.getState().useStreaming).toBe(false);
});
- it("toggleStreaming toggles useStreaming from true to false", () => {
- useChatStore.setState({ useStreaming: true });
+ it("toggleStreaming toggles useStreaming from false to true", () => {
+ useChatStore.setState({ useStreaming: false });
useChatStore.getState().toggleStreaming();
- expect(useChatStore.getState().useStreaming).toBe(false);
+ expect(useChatStore.getState().useStreaming).toBe(true);
});
it("clearStream also clears pendingUserMessage", () => {
From 55a7e444f70dccef37be361e50e5ee7da8d9d01d Mon Sep 17 00:00:00 2001
From: Kaiohz
Date: Mon, 6 Apr 2026 08:31:04 +0200
Subject: [PATCH 4/9] fix: auto-scroll follows messages and streaming bubble
overflow
- Auto-scroll triggers on pendingUserMessage and isStreaming changes
- Add overflow-hidden on AI message bubble container
- Add break-words and overflow-wrap-anywhere on markdown content
- Add overflow-x-auto on code blocks
---
src/application/components/chat/ChatMessage.tsx | 8 ++++----
src/application/components/chat/MessageList.tsx | 2 +-
src/application/index.css | 4 ++++
3 files changed, 9 insertions(+), 5 deletions(-)
diff --git a/src/application/components/chat/ChatMessage.tsx b/src/application/components/chat/ChatMessage.tsx
index 65fcb70..ece7789 100644
--- a/src/application/components/chat/ChatMessage.tsx
+++ b/src/application/components/chat/ChatMessage.tsx
@@ -69,14 +69,14 @@ export default function ChatMessage({
{/* Content bubble */}
-
+
diff --git a/src/application/components/chat/MessageList.tsx b/src/application/components/chat/MessageList.tsx
index 512f77f..13c3e93 100644
--- a/src/application/components/chat/MessageList.tsx
+++ b/src/application/components/chat/MessageList.tsx
@@ -19,7 +19,7 @@ export default function MessageList({ threadId, agentName }: MessageListProps) {
if (scrollRef.current) {
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
}
- }, [messages, streamingContent]);
+ }, [messages, streamingContent, pendingUserMessage, isStreaming]);
if (isLoading) {
return (
diff --git a/src/application/index.css b/src/application/index.css
index ef66394..8ffa766 100644
--- a/src/application/index.css
+++ b/src/application/index.css
@@ -91,3 +91,7 @@
.ambient-shadow {
box-shadow: 0px 40px 40px -10px rgba(25, 28, 29, 0.06);
}
+
+.overflow-wrap-anywhere {
+ overflow-wrap: anywhere;
+}
From a7793c83640fa08b6832318bdc28d90029504389 Mon Sep 17 00:00:00 2001
From: Kaiohz
Date: Mon, 6 Apr 2026 08:52:21 +0200
Subject: [PATCH 5/9] fix: add visible loading indicator while waiting for
agent response
Replace subtle "..." text with a spinner + "Thinking..." label in an
agent-styled bubble. Shows immediately after sending a message, before
any streaming content arrives.
---
.../components/chat/MessageList.tsx | 32 +++++++++++++++++--
1 file changed, 29 insertions(+), 3 deletions(-)
diff --git a/src/application/components/chat/MessageList.tsx b/src/application/components/chat/MessageList.tsx
index 13c3e93..f5d4ace 100644
--- a/src/application/components/chat/MessageList.tsx
+++ b/src/application/components/chat/MessageList.tsx
@@ -77,12 +77,12 @@ export default function MessageList({ threadId, agentName }: MessageListProps) {
/>
)}
- {/* Streaming or typing indicator */}
- {isStreaming && (
+ {/* Streaming content or loading indicator */}
+ {isStreaming && streamingContent && (
)}
+
+ {/* Loading indicator while waiting for response */}
+ {isStreaming && !streamingContent && (
+
+
+
+ hub
+
+
+
+
+
+ {agentName}
+
+
+
+
+
+ progress_activity
+
+ Thinking...
+
+
+
+
+ )}
);
From 823a403f4916a11bc043b9ea798882737ae5522a Mon Sep 17 00:00:00 2001
From: Kaiohz
Date: Mon, 6 Apr 2026 08:57:48 +0200
Subject: [PATCH 6/9] fix: keep loading indicator visible during entire agent
processing
Show spinner throughout the streaming session, not just before first
chunk. Displays "Thinking..." initially, then "Processing..." once
content starts arriving. Stays visible until stream fully completes.
---
.../components/chat/MessageList.tsx | 27 +++++++------------
1 file changed, 9 insertions(+), 18 deletions(-)
diff --git a/src/application/components/chat/MessageList.tsx b/src/application/components/chat/MessageList.tsx
index f5d4ace..a64540e 100644
--- a/src/application/components/chat/MessageList.tsx
+++ b/src/application/components/chat/MessageList.tsx
@@ -77,7 +77,7 @@ export default function MessageList({ threadId, agentName }: MessageListProps) {
/>
)}
- {/* Streaming content or loading indicator */}
+ {/* Streaming content */}
{isStreaming && streamingContent && (
)}
- {/* Loading indicator while waiting for response */}
- {isStreaming && !streamingContent && (
+ {/* Loading indicator - always visible while agent is working */}
+ {isStreaming && (
-
+
hub
-
-
-
- {agentName}
-
-
-
-
-
- progress_activity
-
- Thinking...
-
-
+
+
+ progress_activity
+
+ {streamingContent ? "Processing..." : "Thinking..."}
)}
From 49ebe8f12887dd1ecd6abefc9c82d5ca851fca91 Mon Sep 17 00:00:00 2001
From: Kaiohz
Date: Mon, 6 Apr 2026 09:03:37 +0200
Subject: [PATCH 7/9] fix: integrate spinner inside streaming bubble instead of
separate block
Single agent bubble with content + spinner at the bottom. Shows
"Thinking..." before first chunk, then content + "Processing..." below
until stream completes.
---
.../components/chat/MessageList.tsx | 50 ++++++++++---------
1 file changed, 27 insertions(+), 23 deletions(-)
diff --git a/src/application/components/chat/MessageList.tsx b/src/application/components/chat/MessageList.tsx
index a64540e..7088bbb 100644
--- a/src/application/components/chat/MessageList.tsx
+++ b/src/application/components/chat/MessageList.tsx
@@ -1,4 +1,6 @@
import { useEffect, useRef } from "react";
+import ReactMarkdown from "react-markdown";
+import remarkGfm from "remark-gfm";
import { useMessages } from "@/application/hooks/chat/useMessages";
import { useChatStore } from "@/application/stores/useChatStore";
import { MessageRole } from "@/domain/entities/chat/message";
@@ -77,35 +79,37 @@ export default function MessageList({ threadId, agentName }: MessageListProps) {
/>
)}
- {/* Streaming content */}
- {isStreaming && streamingContent && (
-
- )}
-
- {/* Loading indicator - always visible while agent is working */}
+ {/* Streaming: single agent bubble with content + spinner */}
{isStreaming && (
-
+
hub
-
-
- progress_activity
-
-
{streamingContent ? "Processing..." : "Thinking..."}
+
+
+
+ {agentName}
+
+
+
+ {streamingContent && (
+
+
+ {streamingContent}
+
+
+ )}
+
+
+ progress_activity
+
+
+ {streamingContent ? "Processing..." : "Thinking..."}
+
+
+
)}
From 099d8827986beeab4aed717111443972441a73f3 Mon Sep 17 00:00:00 2001
From: Kaiohz
Date: Mon, 6 Apr 2026 09:50:55 +0200
Subject: [PATCH 8/9] fix: update brand name to Composables in TopNav component
and add Vite configuration files
---
src/application/components/layout/TopNav.tsx | 2 +-
vite.config.d.ts | 2 ++
vitest.config.d.ts | 2 ++
3 files changed, 5 insertions(+), 1 deletion(-)
create mode 100644 vite.config.d.ts
create mode 100644 vitest.config.d.ts
diff --git a/src/application/components/layout/TopNav.tsx b/src/application/components/layout/TopNav.tsx
index b055d84..de82508 100644
--- a/src/application/components/layout/TopNav.tsx
+++ b/src/application/components/layout/TopNav.tsx
@@ -12,7 +12,7 @@ export default function TopNav() {
{/* Left: Brand */}
- Solu Matrix
+ Composables
{/* Center: Navigation */}
diff --git a/vite.config.d.ts b/vite.config.d.ts
new file mode 100644
index 0000000..089eeef
--- /dev/null
+++ b/vite.config.d.ts
@@ -0,0 +1,2 @@
+declare const _default: import("vite").UserConfigFnObject;
+export default _default;
diff --git a/vitest.config.d.ts b/vitest.config.d.ts
new file mode 100644
index 0000000..340562a
--- /dev/null
+++ b/vitest.config.d.ts
@@ -0,0 +1,2 @@
+declare const _default: import("vite").UserConfig;
+export default _default;
From 1afee78694ea8a2a499c1fa20b7a0e1a07cc4831 Mon Sep 17 00:00:00 2001
From: Kaiohz
Date: Mon, 6 Apr 2026 10:14:32 +0200
Subject: [PATCH 9/9] fix: resolve SonarQube issues and increase coverage to
79%
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
SonarQube fixes (27 of 32 resolved):
- Replace div role="dialog" with native
+
);
}
-function SectionLabel({ children }: { children: React.ReactNode }) {
+function SectionLabel({ children }: Readonly<{ children: React.ReactNode }>) {
return (
{children}
diff --git a/src/application/components/agent/AgentGrid.tsx b/src/application/components/agent/AgentGrid.tsx
index 116bbce..1b999c3 100644
--- a/src/application/components/agent/AgentGrid.tsx
+++ b/src/application/components/agent/AgentGrid.tsx
@@ -9,7 +9,7 @@ interface AgentGridProps {
export default function AgentGrid({
onCreateNew,
onConfigure,
-}: AgentGridProps) {
+}: Readonly) {
const { data: agents, isLoading, error } = useAgents();
if (isLoading) {
diff --git a/src/application/components/agent/CreateAgentDialog.tsx b/src/application/components/agent/CreateAgentDialog.tsx
index a674cae..b7510bd 100644
--- a/src/application/components/agent/CreateAgentDialog.tsx
+++ b/src/application/components/agent/CreateAgentDialog.tsx
@@ -1,4 +1,4 @@
-import { useState, useRef, type FormEvent } from "react";
+import { useState, useRef } from "react";
import { toast } from "sonner";
import { useCreateAgent } from "@/application/hooks/agent/useCreateAgent";
@@ -10,14 +10,14 @@ interface CreateAgentDialogProps {
export default function CreateAgentDialog({
open,
onOpenChange,
-}: CreateAgentDialogProps) {
+}: Readonly) {
const [name, setName] = useState("");
const fileInputRef = useRef(null);
const createAgent = useCreateAgent();
if (!open) return null;
- function handleSubmit(e: FormEvent) {
+ function handleSubmit(e: { preventDefault: () => void }) {
e.preventDefault();
const file = fileInputRef.current?.files?.[0];
@@ -53,9 +53,11 @@ export default function CreateAgentDialog({
}
return (
- { if (e.key === "Escape") onOpenChange(false); }}
>
{/* Header */}
@@ -127,6 +129,6 @@ export default function CreateAgentDialog({
-
+
);
}
diff --git a/src/application/components/chat/ChatInput.tsx b/src/application/components/chat/ChatInput.tsx
index 610c023..2a41508 100644
--- a/src/application/components/chat/ChatInput.tsx
+++ b/src/application/components/chat/ChatInput.tsx
@@ -1,4 +1,4 @@
-import { useState, type FormEvent, type KeyboardEvent } from "react";
+import { useState, type KeyboardEvent } from "react";
import { useQueryClient } from "@tanstack/react-query";
import { cn } from "@/application/lib/utils";
import { useStreamChat } from "@/application/hooks/chat/useStreamChat";
@@ -9,7 +9,7 @@ interface ChatInputProps {
threadId: string;
}
-export default function ChatInput({ threadId }: ChatInputProps) {
+export default function ChatInput({ threadId }: Readonly
) {
const [input, setInput] = useState("");
const { stream } = useStreamChat(threadId);
const sendMessage = useSendMessage(threadId);
@@ -18,7 +18,7 @@ export default function ChatInput({ threadId }: ChatInputProps) {
const streamingMode = useChatStore((s) => s.useStreaming);
const toggleStreaming = useChatStore((s) => s.toggleStreaming);
- function handleSubmit(e?: FormEvent) {
+ function handleSubmit(e?: { preventDefault: () => void }) {
e?.preventDefault();
const trimmed = input.trim();
if (!trimmed || isStreaming) return;
diff --git a/src/application/components/chat/ChatMessage.tsx b/src/application/components/chat/ChatMessage.tsx
index ece7789..008511d 100644
--- a/src/application/components/chat/ChatMessage.tsx
+++ b/src/application/components/chat/ChatMessage.tsx
@@ -27,7 +27,7 @@ export default function ChatMessage({
message,
agentName,
threadId,
-}: ChatMessageProps) {
+}: Readonly) {
const isHuman = message.role === MessageRole.HUMAN;
const isAi = message.role === MessageRole.AI;
const isAwaitingHitl = message.status === MessageStatus.AWAITING_HITL;
diff --git a/src/application/components/chat/HITLReviewPanel.tsx b/src/application/components/chat/HITLReviewPanel.tsx
index 6859f27..55a8c17 100644
--- a/src/application/components/chat/HITLReviewPanel.tsx
+++ b/src/application/components/chat/HITLReviewPanel.tsx
@@ -13,7 +13,7 @@ type ReviewState = "idle" | "reviewing" | "rejecting";
export default function HITLReviewPanel({
toolCalls,
threadId,
-}: HITLReviewPanelProps) {
+}: Readonly) {
const [reviewState, setReviewState] = useState("idle");
const [rejectReason, setRejectReason] = useState("");
const sendMessage = useSendMessage(threadId);
diff --git a/src/application/components/chat/MessageList.tsx b/src/application/components/chat/MessageList.tsx
index 7088bbb..90b27d2 100644
--- a/src/application/components/chat/MessageList.tsx
+++ b/src/application/components/chat/MessageList.tsx
@@ -11,7 +11,7 @@ interface MessageListProps {
agentName: string;
}
-export default function MessageList({ threadId, agentName }: MessageListProps) {
+export default function MessageList({ threadId, agentName }: Readonly) {
const { data: messages, isLoading } = useMessages(threadId);
const { streamingContent, isStreaming, pendingUserMessage } = useChatStore();
const scrollRef = useRef(null);
diff --git a/src/application/components/layout/MainLayout.tsx b/src/application/components/layout/MainLayout.tsx
index 2785b24..3815188 100644
--- a/src/application/components/layout/MainLayout.tsx
+++ b/src/application/components/layout/MainLayout.tsx
@@ -12,7 +12,7 @@ export default function MainLayout({
children,
showSidebar = false,
activeThreadId,
-}: MainLayoutProps) {
+}: Readonly) {
return (
diff --git a/src/application/components/layout/ThreadSidebar.tsx b/src/application/components/layout/ThreadSidebar.tsx
index 71a1733..3e1b297 100644
--- a/src/application/components/layout/ThreadSidebar.tsx
+++ b/src/application/components/layout/ThreadSidebar.tsx
@@ -21,7 +21,7 @@ function formatDate(dateStr: string): string {
});
}
-export default function ThreadSidebar({ activeThreadId }: ThreadSidebarProps) {
+export default function ThreadSidebar({ activeThreadId }: Readonly) {
const { data: threads, isLoading } = useThreads();
const { data: agents, isLoading: agentsLoading } = useAgents();
const createThread = useCreateThread();
@@ -69,7 +69,7 @@ export default function ThreadSidebar({ activeThreadId }: ThreadSidebarProps) {
className="w-full flex items-center gap-2 px-4 py-3 rounded-xl bg-primary-container text-white font-headline text-xs font-bold uppercase tracking-widest hover:opacity-90 transition-opacity"
>
add
- New Conversation
+ {" "}New Conversation
@@ -129,9 +129,11 @@ export default function ThreadSidebar({ activeThreadId }: ThreadSidebarProps) {
{/* Agent selection dialog */}
{showAgentDialog && (
- setShowAgentDialog(false)}
+ onKeyDown={(e) => { if (e.key === "Escape") setShowAgentDialog(false); }}
>
)}
-
+
)}
);
diff --git a/src/application/components/shared/StatusBadge.tsx b/src/application/components/shared/StatusBadge.tsx
index e47bc99..8dc0acf 100644
--- a/src/application/components/shared/StatusBadge.tsx
+++ b/src/application/components/shared/StatusBadge.tsx
@@ -18,7 +18,7 @@ function resolveVariant(status: string): {
return { bg: "bg-slate-100", text: "text-slate-600" };
}
-export default function StatusBadge({ status }: StatusBadgeProps) {
+export default function StatusBadge({ status }: Readonly) {
const { bg, text } = resolveVariant(status);
return (
diff --git a/src/application/components/shared/ToolTag.tsx b/src/application/components/shared/ToolTag.tsx
index 31e3098..a43a4a3 100644
--- a/src/application/components/shared/ToolTag.tsx
+++ b/src/application/components/shared/ToolTag.tsx
@@ -2,7 +2,7 @@ interface ToolTagProps {
label: string;
}
-export default function ToolTag({ label }: ToolTagProps) {
+export default function ToolTag({ label }: Readonly) {
return (
{label}
diff --git a/src/application/hooks/agent/useAgentConfig.ts b/src/application/hooks/agent/useAgentConfig.ts
index 5f69a6b..2c5b805 100644
--- a/src/application/hooks/agent/useAgentConfig.ts
+++ b/src/application/hooks/agent/useAgentConfig.ts
@@ -4,7 +4,10 @@ import { agentApi } from "@/infrastructure/api/agent/agentApi";
export function useAgentConfig(name: string | null) {
return useQuery({
queryKey: ["agent", name],
- queryFn: () => agentApi.getAgent(name!),
+ queryFn: () => {
+ if (!name) throw new Error("name is required");
+ return agentApi.getAgent(name);
+ },
enabled: !!name,
});
}
diff --git a/src/application/hooks/chat/useMessages.ts b/src/application/hooks/chat/useMessages.ts
index f78751d..f182ae1 100644
--- a/src/application/hooks/chat/useMessages.ts
+++ b/src/application/hooks/chat/useMessages.ts
@@ -4,7 +4,10 @@ import { chatApi } from "@/infrastructure/api/chat/chatApi";
export function useMessages(threadId: string | null) {
return useQuery({
queryKey: ["messages", threadId],
- queryFn: () => chatApi.getMessages(threadId!),
+ queryFn: () => {
+ if (!threadId) throw new Error("threadId is required");
+ return chatApi.getMessages(threadId);
+ },
enabled: !!threadId,
});
}
diff --git a/src/application/pages/AgentsPage.tsx b/src/application/pages/AgentsPage.tsx
index ca5f89d..8e3aba1 100644
--- a/src/application/pages/AgentsPage.tsx
+++ b/src/application/pages/AgentsPage.tsx
@@ -31,7 +31,7 @@ export default function AgentsPage() {
add_circle
- Create Agent (YAML)
+ {" "}Create Agent (YAML)
diff --git a/src/domain/entities/chat/message.ts b/src/domain/entities/chat/message.ts
index 66864bb..8542bcd 100644
--- a/src/domain/entities/chat/message.ts
+++ b/src/domain/entities/chat/message.ts
@@ -23,5 +23,5 @@ export interface Message {
timestamp: string;
tool_calls: ToolCall[] | null;
status: MessageStatus | null;
- structured_response: unknown | null;
+ structured_response: unknown;
}
diff --git a/tests/unit/components/agent/AgentConfigViewer.test.tsx b/tests/unit/components/agent/AgentConfigViewer.test.tsx
new file mode 100644
index 0000000..d4c1130
--- /dev/null
+++ b/tests/unit/components/agent/AgentConfigViewer.test.tsx
@@ -0,0 +1,419 @@
+import { screen, waitFor } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { describe, it, expect, vi, beforeEach } from "vitest";
+import { renderWithProviders } from "../../../utils/render";
+import AgentConfigViewer from "@/application/components/agent/AgentConfigViewer";
+import type { AgentConfig } from "@/domain/entities/agent/agentConfig";
+import { BackendType, MiddlewareType } from "@/domain/entities/agent/agentConfig";
+
+const { mockAgentConfigData, mockDeleteMutate } = vi.hoisted(() => {
+ return {
+ mockAgentConfigData: {
+ data: undefined as AgentConfig | undefined,
+ isLoading: false,
+ },
+ mockDeleteMutate: vi.fn(),
+ };
+});
+
+vi.mock("@/application/hooks/agent/useAgentConfig", () => ({
+ useAgentConfig: () => mockAgentConfigData,
+}));
+
+vi.mock("@/application/hooks/agent/useDeleteAgent", () => ({
+ useDeleteAgent: () => ({
+ mutate: mockDeleteMutate,
+ isPending: false,
+ }),
+}));
+
+vi.mock("sonner", () => ({
+ toast: {
+ success: vi.fn(),
+ error: vi.fn(),
+ },
+}));
+
+const fullConfig: AgentConfig = {
+ name: "test-agent",
+ model: "openai:gpt-4o",
+ system_prompt: "You are a helpful assistant that provides accurate information.",
+ tools: ["search", "calculator"],
+ middleware: [],
+ backend: { type: "state" as BackendType },
+ hitl: { rules: {} },
+ memory: [],
+ skills: [],
+ subagents: [],
+ mcp_servers: [],
+ debug: false,
+};
+
+describe("AgentConfigViewer", () => {
+ beforeEach(() => {
+ mockAgentConfigData.data = undefined;
+ mockAgentConfigData.isLoading = false;
+ mockDeleteMutate.mockClear();
+ });
+
+ it("returns null when open is false", () => {
+ const { container } = renderWithProviders(
+ ,
+ );
+
+ expect(container.innerHTML).toBe("");
+ });
+
+ it("renders agent name when open", () => {
+ mockAgentConfigData.data = fullConfig;
+
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByText("test-agent")).toBeInTheDocument();
+ });
+
+ it("shows loading state", () => {
+ mockAgentConfigData.isLoading = true;
+
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByText("Loading configuration...")).toBeInTheDocument();
+ });
+
+ it("renders model name from config", () => {
+ mockAgentConfigData.data = fullConfig;
+
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByText("openai:gpt-4o")).toBeInTheDocument();
+ });
+
+ it("renders tools list", () => {
+ mockAgentConfigData.data = fullConfig;
+
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByText("search")).toBeInTheDocument();
+ expect(screen.getByText("calculator")).toBeInTheDocument();
+ expect(screen.getByText("Tools (2)")).toBeInTheDocument();
+ });
+
+ it("renders backend type", () => {
+ mockAgentConfigData.data = fullConfig;
+
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByText(/state/)).toBeInTheDocument();
+ });
+
+ it("calls onOpenChange when close button is clicked", async () => {
+ mockAgentConfigData.data = fullConfig;
+ const user = userEvent.setup();
+ const onOpenChange = vi.fn();
+
+ renderWithProviders(
+ ,
+ );
+
+ // The close button contains the "close" text in a span
+ const closeButtons = screen.getAllByRole("button");
+ // First button in header is the close (X) button
+ const closeButton = closeButtons.find((btn) =>
+ btn.textContent?.includes("close"),
+ );
+ expect(closeButton).toBeDefined();
+ await user.click(closeButton!);
+
+ expect(onOpenChange).toHaveBeenCalledWith(false);
+ });
+
+ it("shows delete button and requires confirmation", async () => {
+ mockAgentConfigData.data = fullConfig;
+ const user = userEvent.setup();
+
+ renderWithProviders(
+ ,
+ );
+
+ const deleteButton = screen.getByRole("button", { name: /^delete$/i });
+ expect(deleteButton).toBeInTheDocument();
+
+ await user.click(deleteButton);
+
+ // After first click, button should show confirm text
+ expect(
+ screen.getByRole("button", { name: /confirm delete/i }),
+ ).toBeInTheDocument();
+ });
+
+ it("renders debug status badge", () => {
+ mockAgentConfigData.data = fullConfig;
+
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByText("Off")).toBeInTheDocument();
+ });
+
+ it("renders middleware list when present", () => {
+ mockAgentConfigData.data = {
+ ...fullConfig,
+ middleware: [MiddlewareType.TODO_LIST, MiddlewareType.FILESYSTEM],
+ };
+
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByText("Middleware (2)")).toBeInTheDocument();
+ expect(screen.getByText("todo_list")).toBeInTheDocument();
+ expect(screen.getByText("filesystem")).toBeInTheDocument();
+ });
+
+ it("renders HITL rules when present", () => {
+ mockAgentConfigData.data = {
+ ...fullConfig,
+ hitl: { rules: { create_file: true, delete_file: false } },
+ };
+
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByText("HITL Rules")).toBeInTheDocument();
+ expect(screen.getByText("create_file")).toBeInTheDocument();
+ expect(screen.getByText("Enabled")).toBeInTheDocument();
+ expect(screen.getByText("delete_file")).toBeInTheDocument();
+ expect(screen.getByText("Disabled")).toBeInTheDocument();
+ });
+
+ it("renders MCP servers when present", () => {
+ mockAgentConfigData.data = {
+ ...fullConfig,
+ mcp_servers: [
+ { name: "filesystem-server", transport: "stdio" as any, args: [], headers: {}, env: {} },
+ ],
+ };
+
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByText("MCP Servers (1)")).toBeInTheDocument();
+ expect(screen.getByText("filesystem-server")).toBeInTheDocument();
+ expect(screen.getByText("stdio")).toBeInTheDocument();
+ });
+
+ it("renders subagents when present", () => {
+ mockAgentConfigData.data = {
+ ...fullConfig,
+ subagents: [
+ {
+ name: "research-sub",
+ description: "Research subagent",
+ tools: [],
+ skills: [],
+ mcp_servers: [],
+ },
+ ],
+ };
+
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByText("Subagents (1)")).toBeInTheDocument();
+ expect(screen.getByText("research-sub")).toBeInTheDocument();
+ expect(screen.getByText("Research subagent")).toBeInTheDocument();
+ });
+
+ it("renders backend root_dir when present", () => {
+ mockAgentConfigData.data = {
+ ...fullConfig,
+ backend: { type: BackendType.FILESYSTEM, root_dir: "/data/agents" },
+ };
+
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByText(/\/data\/agents/)).toBeInTheDocument();
+ });
+
+ it("expands and collapses long system prompt", async () => {
+ const longPrompt = "A".repeat(300);
+ mockAgentConfigData.data = {
+ ...fullConfig,
+ system_prompt: longPrompt,
+ };
+ const user = userEvent.setup();
+
+ renderWithProviders(
+ ,
+ );
+
+ // Initially shows truncated (200 chars) with "Show more"
+ expect(screen.getByText(/\.\.\.Show more/)).toBeInTheDocument();
+
+ await user.click(screen.getByText(/\.\.\.Show more/));
+
+ // After click, shows "Show less"
+ expect(screen.getByText("Show less")).toBeInTheDocument();
+ });
+
+ it("calls deleteAgent.mutate on confirm delete", async () => {
+ mockAgentConfigData.data = fullConfig;
+ const user = userEvent.setup();
+
+ renderWithProviders(
+ ,
+ );
+
+ // First click sets confirmDelete
+ await user.click(screen.getByRole("button", { name: /^delete$/i }));
+ // Second click triggers actual delete
+ await user.click(screen.getByRole("button", { name: /confirm delete/i }));
+
+ expect(mockDeleteMutate).toHaveBeenCalledWith(
+ "test-agent",
+ expect.objectContaining({ onSuccess: expect.any(Function), onError: expect.any(Function) }),
+ );
+ });
+
+ it("closes dialog via Close button in header", async () => {
+ mockAgentConfigData.data = fullConfig;
+ const user = userEvent.setup();
+ const onOpenChange = vi.fn();
+
+ renderWithProviders(
+ ,
+ );
+
+ // Header close (X) button contains "close" icon text
+ const allButtons = screen.getAllByRole("button");
+ const headerClose = allButtons.find(
+ (btn) => btn.textContent?.trim() === "close",
+ );
+ expect(headerClose).toBeDefined();
+ await user.click(headerClose!);
+
+ expect(onOpenChange).toHaveBeenCalledWith(false);
+ });
+
+ it("closes via Close button in footer", async () => {
+ mockAgentConfigData.data = fullConfig;
+ const user = userEvent.setup();
+ const onOpenChange = vi.fn();
+
+ renderWithProviders(
+ ,
+ );
+
+ // The footer "Close" button
+ const closeButtons = screen.getAllByRole("button");
+ const footerClose = closeButtons.find(
+ (btn) => btn.textContent === "Close",
+ );
+ expect(footerClose).toBeDefined();
+ await user.click(footerClose!);
+
+ expect(onOpenChange).toHaveBeenCalledWith(false);
+ });
+
+ it("shows Active badge when debug is true", () => {
+ mockAgentConfigData.data = { ...fullConfig, debug: true };
+
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByText("Active")).toBeInTheDocument();
+ });
+});
diff --git a/tests/unit/components/agent/CreateAgentDialog.test.tsx b/tests/unit/components/agent/CreateAgentDialog.test.tsx
new file mode 100644
index 0000000..11dae3c
--- /dev/null
+++ b/tests/unit/components/agent/CreateAgentDialog.test.tsx
@@ -0,0 +1,175 @@
+import { screen, waitFor } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { describe, it, expect, vi, beforeEach } from "vitest";
+import { renderWithProviders } from "../../../utils/render";
+import CreateAgentDialog from "@/application/components/agent/CreateAgentDialog";
+
+const { mockCreateAgentMutate } = vi.hoisted(() => {
+ return {
+ mockCreateAgentMutate: vi.fn(),
+ };
+});
+
+vi.mock("@/application/hooks/agent/useCreateAgent", () => ({
+ useCreateAgent: () => ({
+ mutate: mockCreateAgentMutate,
+ isPending: false,
+ }),
+}));
+
+vi.mock("sonner", () => ({
+ toast: {
+ success: vi.fn(),
+ error: vi.fn(),
+ },
+}));
+
+describe("CreateAgentDialog", () => {
+ beforeEach(() => {
+ mockCreateAgentMutate.mockClear();
+ });
+
+ it("returns null when open is false", () => {
+ const { container } = renderWithProviders(
+ ,
+ );
+
+ expect(container.innerHTML).toBe("");
+ });
+
+ it("renders form with title when open", () => {
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByText("Create Agent")).toBeInTheDocument();
+ });
+
+ it("has name input with label", () => {
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByLabelText(/agent name/i)).toBeInTheDocument();
+ });
+
+ it("has YAML file input", () => {
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByLabelText(/yaml configuration/i)).toBeInTheDocument();
+ });
+
+ it("has Create and Cancel buttons", () => {
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByRole("button", { name: /create/i })).toBeInTheDocument();
+ expect(screen.getByRole("button", { name: /cancel/i })).toBeInTheDocument();
+ });
+
+ it("calls onOpenChange when Cancel is clicked", async () => {
+ const user = userEvent.setup();
+ const onOpenChange = vi.fn();
+
+ renderWithProviders(
+ ,
+ );
+
+ await user.click(screen.getByRole("button", { name: /cancel/i }));
+
+ expect(onOpenChange).toHaveBeenCalledWith(false);
+ });
+
+ it("allows typing in the name input", async () => {
+ const user = userEvent.setup();
+
+ renderWithProviders(
+ ,
+ );
+
+ const input = screen.getByLabelText(/agent name/i);
+ await user.type(input, "my-new-agent");
+
+ expect(input).toHaveValue("my-new-agent");
+ });
+
+ it("calls createAgent.mutate when form is submitted with name and file", async () => {
+ const user = userEvent.setup();
+
+ renderWithProviders(
+ ,
+ );
+
+ // Type agent name
+ await user.type(screen.getByLabelText(/agent name/i), "my-agent");
+
+ // Upload a file
+ const file = new File(["model: gpt-4o"], "config.yaml", {
+ type: "application/x-yaml",
+ });
+ const fileInput = screen.getByLabelText(/yaml configuration/i);
+ await user.upload(fileInput, file);
+
+ // Submit
+ await user.click(screen.getByRole("button", { name: /^create$/i }));
+
+ expect(mockCreateAgentMutate).toHaveBeenCalledWith(
+ { name: "my-agent", yamlFile: file },
+ expect.objectContaining({ onSuccess: expect.any(Function), onError: expect.any(Function) }),
+ );
+ });
+
+ it("shows error toast when name is empty on submit", async () => {
+ const { toast } = await import("sonner");
+ const user = userEvent.setup();
+
+ renderWithProviders(
+ ,
+ );
+
+ // Submit without name
+ await user.click(screen.getByRole("button", { name: /^create$/i }));
+
+ expect(toast.error).toHaveBeenCalledWith("Agent name is required");
+ expect(mockCreateAgentMutate).not.toHaveBeenCalled();
+ });
+
+ it("shows error toast when file is not selected on submit", async () => {
+ const { toast } = await import("sonner");
+ const user = userEvent.setup();
+
+ renderWithProviders(
+ ,
+ );
+
+ // Type name but don't upload file
+ await user.type(screen.getByLabelText(/agent name/i), "my-agent");
+ await user.click(screen.getByRole("button", { name: /^create$/i }));
+
+ expect(toast.error).toHaveBeenCalledWith(
+ "YAML configuration file is required",
+ );
+ expect(mockCreateAgentMutate).not.toHaveBeenCalled();
+ });
+
+ it("closes dialog via header close button", async () => {
+ const user = userEvent.setup();
+ const onOpenChange = vi.fn();
+
+ renderWithProviders(
+ ,
+ );
+
+ const allButtons = screen.getAllByRole("button");
+ const headerClose = allButtons.find(
+ (btn) => btn.textContent?.trim() === "close",
+ );
+ expect(headerClose).toBeDefined();
+ await user.click(headerClose!);
+
+ expect(onOpenChange).toHaveBeenCalledWith(false);
+ });
+});
diff --git a/tests/unit/components/chat/ChatInput.test.tsx b/tests/unit/components/chat/ChatInput.test.tsx
index 1612a5a..3dbdb53 100644
--- a/tests/unit/components/chat/ChatInput.test.tsx
+++ b/tests/unit/components/chat/ChatInput.test.tsx
@@ -74,4 +74,87 @@ describe("ChatInput", () => {
});
expect(mockStream).toHaveBeenCalledWith({ message: "Hello agent" });
});
+
+ it("submits on Enter key (without Shift)", async () => {
+ const user = userEvent.setup();
+
+ renderWithProviders();
+
+ const textarea = screen.getByPlaceholderText(
+ "Orchestrate your next move...",
+ );
+ await user.type(textarea, "Enter message");
+ await user.keyboard("{Enter}");
+
+ await waitFor(() => {
+ expect(textarea).toHaveValue("");
+ });
+ expect(mockStream).toHaveBeenCalledWith({ message: "Enter message" });
+ });
+
+ it("does not submit on Shift+Enter", async () => {
+ const user = userEvent.setup();
+
+ renderWithProviders();
+
+ const textarea = screen.getByPlaceholderText(
+ "Orchestrate your next move...",
+ );
+ await user.type(textarea, "Line 1");
+ await user.keyboard("{Shift>}{Enter}{/Shift}");
+
+ expect(mockStream).not.toHaveBeenCalled();
+ });
+
+ it("does not submit when input is empty", async () => {
+ const user = userEvent.setup();
+
+ renderWithProviders();
+
+ const submitButton = screen.getByRole("button", { name: /send/i });
+ expect(submitButton).toBeDisabled();
+ });
+
+ it("uses sendMessage.mutate in standard mode (non-streaming)", async () => {
+ mockStoreState.useStreaming = false;
+ const user = userEvent.setup();
+
+ renderWithProviders();
+
+ const textarea = screen.getByPlaceholderText(
+ "Orchestrate your next move...",
+ );
+ await user.type(textarea, "Standard message");
+ await user.click(screen.getByRole("button", { name: /send/i }));
+
+ await waitFor(() => {
+ expect(textarea).toHaveValue("");
+ });
+ expect(mockSendMessageMutate).toHaveBeenCalledWith(
+ { message: "Standard message" },
+ expect.objectContaining({ onSuccess: expect.any(Function), onError: expect.any(Function) }),
+ );
+
+ // Restore
+ mockStoreState.useStreaming = true;
+ });
+
+ it("disables textarea when isStreaming is true", () => {
+ mockStoreState.isStreaming = true;
+
+ renderWithProviders();
+
+ const textarea = screen.getByPlaceholderText(
+ "Orchestrate your next move...",
+ );
+ expect(textarea).toBeDisabled();
+
+ mockStoreState.isStreaming = false;
+ });
+
+ it("renders version text", () => {
+ renderWithProviders();
+
+ expect(screen.getByText("Composables v0.1")).toBeInTheDocument();
+ });
});
diff --git a/tests/unit/components/chat/HITLReviewPanel.test.tsx b/tests/unit/components/chat/HITLReviewPanel.test.tsx
new file mode 100644
index 0000000..4803364
--- /dev/null
+++ b/tests/unit/components/chat/HITLReviewPanel.test.tsx
@@ -0,0 +1,145 @@
+import { screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { describe, it, expect, vi, beforeEach } from "vitest";
+import { renderWithProviders } from "../../../utils/render";
+import HITLReviewPanel from "@/application/components/chat/HITLReviewPanel";
+import type { ToolCall } from "@/domain/entities/chat/message";
+
+const { mockSendMessageMutate } = vi.hoisted(() => {
+ return {
+ mockSendMessageMutate: vi.fn(),
+ };
+});
+
+vi.mock("@/application/hooks/chat/useSendMessage", () => ({
+ useSendMessage: () => ({
+ mutate: mockSendMessageMutate,
+ isPending: false,
+ }),
+}));
+
+const toolCalls: ToolCall[] = [
+ {
+ id: "tc-1",
+ name: "create_file",
+ args: { path: "/tmp/test.txt", content: "hello" },
+ },
+];
+
+describe("HITLReviewPanel", () => {
+ beforeEach(() => {
+ mockSendMessageMutate.mockClear();
+ });
+
+ it("renders tool name in idle state", () => {
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByText("create_file")).toBeInTheDocument();
+ });
+
+ it("shows 'Review Data' button in idle state", () => {
+ renderWithProviders(
+ ,
+ );
+
+ expect(
+ screen.getByRole("button", { name: /review data/i }),
+ ).toBeInTheDocument();
+ });
+
+ it("shows validation required text", () => {
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByText(/validation required/i)).toBeInTheDocument();
+ });
+
+ it("transitions to reviewing state when Review Data is clicked", async () => {
+ const user = userEvent.setup();
+
+ renderWithProviders(
+ ,
+ );
+
+ await user.click(screen.getByRole("button", { name: /review data/i }));
+
+ // In reviewing state, Approve and Reject buttons should appear
+ expect(screen.getByRole("button", { name: /approve/i })).toBeInTheDocument();
+ expect(screen.getByRole("button", { name: /reject/i })).toBeInTheDocument();
+ expect(screen.getByRole("button", { name: /cancel/i })).toBeInTheDocument();
+ });
+
+ it("shows tool args in reviewing state", async () => {
+ const user = userEvent.setup();
+
+ renderWithProviders(
+ ,
+ );
+
+ await user.click(screen.getByRole("button", { name: /review data/i }));
+
+ expect(screen.getByText(/\/tmp\/test\.txt/)).toBeInTheDocument();
+ });
+
+ it("calls sendMessage with approve action when Approve is clicked", async () => {
+ const user = userEvent.setup();
+
+ renderWithProviders(
+ ,
+ );
+
+ await user.click(screen.getByRole("button", { name: /review data/i }));
+ await user.click(screen.getByRole("button", { name: /approve/i }));
+
+ expect(mockSendMessageMutate).toHaveBeenCalledWith({
+ tool_call_id: "tc-1",
+ action: "approve",
+ });
+ });
+
+ it("transitions to rejecting state on first Reject click", async () => {
+ const user = userEvent.setup();
+
+ renderWithProviders(
+ ,
+ );
+
+ await user.click(screen.getByRole("button", { name: /review data/i }));
+ await user.click(screen.getByRole("button", { name: /^reject$/i }));
+
+ // Should now show "Confirm Reject" and a reason input
+ expect(
+ screen.getByRole("button", { name: /confirm reject/i }),
+ ).toBeInTheDocument();
+ expect(
+ screen.getByPlaceholderText(/reason for rejection/i),
+ ).toBeInTheDocument();
+ });
+
+ it("returns to idle state when Cancel is clicked", async () => {
+ const user = userEvent.setup();
+
+ renderWithProviders(
+ ,
+ );
+
+ await user.click(screen.getByRole("button", { name: /review data/i }));
+ await user.click(screen.getByRole("button", { name: /cancel/i }));
+
+ // Should be back to idle with "Review Data" button
+ expect(
+ screen.getByRole("button", { name: /review data/i }),
+ ).toBeInTheDocument();
+ });
+
+ it("handles empty tool calls gracefully", () => {
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByText("Unknown tool")).toBeInTheDocument();
+ });
+});
diff --git a/tests/unit/components/layout/MainLayout.test.tsx b/tests/unit/components/layout/MainLayout.test.tsx
new file mode 100644
index 0000000..b9af002
--- /dev/null
+++ b/tests/unit/components/layout/MainLayout.test.tsx
@@ -0,0 +1,93 @@
+import { screen } from "@testing-library/react";
+import { describe, it, expect, vi } from "vitest";
+import { renderWithProviders } from "../../../utils/render";
+import MainLayout from "@/application/components/layout/MainLayout";
+import { createThread, createAgentConfigMetadata } from "../../../fixtures/external";
+
+const { mockThreadsData, mockAgentsData, mockCreateThreadMutate, mockSetActiveThread } =
+ vi.hoisted(() => {
+ return {
+ mockThreadsData: {
+ data: undefined as ReturnType[] | undefined,
+ isLoading: false,
+ },
+ mockAgentsData: {
+ data: undefined as ReturnType[] | undefined,
+ isLoading: false,
+ },
+ mockCreateThreadMutate: vi.fn(),
+ mockSetActiveThread: vi.fn(),
+ };
+ });
+
+// Mock the hooks used by ThreadSidebar (child component)
+vi.mock("@/application/hooks/chat/useThreads", () => ({
+ useThreads: () => mockThreadsData,
+}));
+
+vi.mock("@/application/hooks/agent/useAgents", () => ({
+ useAgents: () => mockAgentsData,
+}));
+
+vi.mock("@/application/hooks/chat/useCreateThread", () => ({
+ useCreateThread: () => ({
+ mutate: mockCreateThreadMutate,
+ isPending: false,
+ }),
+}));
+
+vi.mock("@/application/stores/useChatStore", () => {
+ const fn = (selector: (state: { setActiveThread: typeof mockSetActiveThread }) => unknown) => {
+ return selector({ setActiveThread: mockSetActiveThread });
+ };
+ fn.getState = () => ({ setActiveThread: mockSetActiveThread });
+ return { useChatStore: fn };
+});
+
+describe("MainLayout", () => {
+ it("renders children content", () => {
+ renderWithProviders(
+
+ My Page Content
+ ,
+ );
+
+ expect(screen.getByText("My Page Content")).toBeInTheDocument();
+ });
+
+ it("renders TopNav with app name", () => {
+ renderWithProviders(
+
+ Content
+ ,
+ );
+
+ expect(screen.getByText("Composables")).toBeInTheDocument();
+ });
+
+ it("shows sidebar when showSidebar is true", () => {
+ mockThreadsData.data = [];
+
+ renderWithProviders(
+
+ Content
+ ,
+ );
+
+ expect(
+ screen.getByRole("button", { name: /new conversation/i }),
+ ).toBeInTheDocument();
+ });
+
+ it("does not show sidebar when showSidebar is false", () => {
+ renderWithProviders(
+
+ Content
+ ,
+ );
+
+ expect(
+ screen.queryByRole("button", { name: /new conversation/i }),
+ ).not.toBeInTheDocument();
+ });
+});
diff --git a/tests/unit/components/layout/ThreadSidebar.test.tsx b/tests/unit/components/layout/ThreadSidebar.test.tsx
index 7ba2fa5..7f1151b 100644
--- a/tests/unit/components/layout/ThreadSidebar.test.tsx
+++ b/tests/unit/components/layout/ThreadSidebar.test.tsx
@@ -1,4 +1,4 @@
-import { screen, waitFor } from "@testing-library/react";
+import { screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, it, expect, vi, beforeEach } from "vitest";
import { renderWithProviders } from "../../../utils/render";
diff --git a/tests/unit/components/layout/TopNav.test.tsx b/tests/unit/components/layout/TopNav.test.tsx
new file mode 100644
index 0000000..0f836f9
--- /dev/null
+++ b/tests/unit/components/layout/TopNav.test.tsx
@@ -0,0 +1,35 @@
+import { screen } from "@testing-library/react";
+import { describe, it, expect } from "vitest";
+import { renderWithProviders } from "../../../utils/render";
+import TopNav from "@/application/components/layout/TopNav";
+
+describe("TopNav", () => {
+ it("renders app name 'Composables'", () => {
+ renderWithProviders();
+
+ expect(screen.getByText("Composables")).toBeInTheDocument();
+ });
+
+ it("renders Orchestration link", () => {
+ renderWithProviders();
+
+ const link = screen.getByRole("link", { name: /orchestration/i });
+ expect(link).toBeInTheDocument();
+ expect(link).toHaveAttribute("href", "/chat");
+ });
+
+ it("renders Agents link", () => {
+ renderWithProviders();
+
+ const link = screen.getByRole("link", { name: /agents/i });
+ expect(link).toBeInTheDocument();
+ expect(link).toHaveAttribute("href", "/agents");
+ });
+
+ it("renders settings and account buttons", () => {
+ renderWithProviders();
+
+ const buttons = screen.getAllByRole("button");
+ expect(buttons.length).toBeGreaterThanOrEqual(2);
+ });
+});
diff --git a/tests/unit/hooks/agent/useAgentConfig.test.tsx b/tests/unit/hooks/agent/useAgentConfig.test.tsx
new file mode 100644
index 0000000..24fc0b1
--- /dev/null
+++ b/tests/unit/hooks/agent/useAgentConfig.test.tsx
@@ -0,0 +1,81 @@
+import { renderHook, waitFor } from "@testing-library/react";
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import { vi, describe, it, expect, beforeEach } from "vitest";
+import { useAgentConfig } from "@/application/hooks/agent/useAgentConfig";
+import { agentApi } from "@/infrastructure/api/agent/agentApi";
+import type { AgentConfig, BackendType } from "@/domain/entities/agent/agentConfig";
+import type { ReactNode } from "react";
+
+vi.mock("@/infrastructure/api/agent/agentApi", () => ({
+ agentApi: {
+ getAgent: vi.fn(),
+ listAgents: vi.fn(),
+ createAgent: vi.fn(),
+ updateAgent: vi.fn(),
+ deleteAgent: vi.fn(),
+ },
+}));
+
+function createWrapper() {
+ const queryClient = new QueryClient({
+ defaultOptions: { queries: { retry: false, gcTime: 0 } },
+ });
+ return ({ children }: { children: ReactNode }) => (
+ {children}
+ );
+}
+
+const mockAgentConfig: AgentConfig = {
+ name: "test-agent",
+ model: "openai:gpt-4o",
+ system_prompt: "You are a helpful assistant.",
+ tools: ["search", "calculator"],
+ middleware: [],
+ backend: { type: "state" as BackendType },
+ hitl: { rules: {} },
+ memory: [],
+ skills: [],
+ subagents: [],
+ mcp_servers: [],
+ debug: false,
+};
+
+describe("useAgentConfig", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it("returns agent config when name is provided", async () => {
+ vi.mocked(agentApi.getAgent).mockResolvedValue(mockAgentConfig);
+
+ const { result } = renderHook(() => useAgentConfig("test-agent"), {
+ wrapper: createWrapper(),
+ });
+
+ await waitFor(() => expect(result.current.isSuccess).toBe(true));
+ expect(result.current.data).toEqual(mockAgentConfig);
+ expect(agentApi.getAgent).toHaveBeenCalledWith("test-agent");
+ });
+
+ it("does not fetch when name is null", async () => {
+ const { result } = renderHook(() => useAgentConfig(null), {
+ wrapper: createWrapper(),
+ });
+
+ // The query should not be fetching since enabled is false
+ expect(result.current.isFetching).toBe(false);
+ expect(agentApi.getAgent).not.toHaveBeenCalled();
+ });
+
+ it("returns error state when API call fails", async () => {
+ vi.mocked(agentApi.getAgent).mockRejectedValue(new Error("Not found"));
+
+ const { result } = renderHook(() => useAgentConfig("unknown-agent"), {
+ wrapper: createWrapper(),
+ });
+
+ await waitFor(() => expect(result.current.isError).toBe(true));
+ expect(result.current.error).toBeInstanceOf(Error);
+ expect(result.current.error?.message).toBe("Not found");
+ });
+});
diff --git a/tests/unit/hooks/agent/useCreateAgent.test.tsx b/tests/unit/hooks/agent/useCreateAgent.test.tsx
new file mode 100644
index 0000000..c5663bd
--- /dev/null
+++ b/tests/unit/hooks/agent/useCreateAgent.test.tsx
@@ -0,0 +1,87 @@
+import { renderHook, waitFor, act } from "@testing-library/react";
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import { vi, describe, it, expect, beforeEach } from "vitest";
+import { useCreateAgent } from "@/application/hooks/agent/useCreateAgent";
+import { agentApi } from "@/infrastructure/api/agent/agentApi";
+import type { AgentConfig, BackendType } from "@/domain/entities/agent/agentConfig";
+import type { ReactNode } from "react";
+
+vi.mock("@/infrastructure/api/agent/agentApi", () => ({
+ agentApi: {
+ getAgent: vi.fn(),
+ listAgents: vi.fn(),
+ createAgent: vi.fn(),
+ updateAgent: vi.fn(),
+ deleteAgent: vi.fn(),
+ },
+}));
+
+function createWrapper() {
+ const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: { retry: false, gcTime: 0 },
+ mutations: { retry: false },
+ },
+ });
+ return {
+ wrapper: ({ children }: { children: ReactNode }) => (
+ {children}
+ ),
+ queryClient,
+ };
+}
+
+const mockCreatedConfig: AgentConfig = {
+ name: "new-agent",
+ model: "openai:gpt-4o",
+ tools: [],
+ middleware: [],
+ backend: { type: "state" as BackendType },
+ hitl: { rules: {} },
+ memory: [],
+ skills: [],
+ subagents: [],
+ mcp_servers: [],
+ debug: false,
+};
+
+describe("useCreateAgent", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it("calls agentApi.createAgent on mutate", async () => {
+ vi.mocked(agentApi.createAgent).mockResolvedValue(mockCreatedConfig);
+ const { wrapper } = createWrapper();
+ const file = new File(["model: gpt-4o"], "config.yaml", {
+ type: "application/x-yaml",
+ });
+
+ const { result } = renderHook(() => useCreateAgent(), { wrapper });
+
+ act(() => {
+ result.current.mutate({ name: "new-agent", yamlFile: file });
+ });
+
+ await waitFor(() => expect(result.current.isSuccess).toBe(true));
+ expect(agentApi.createAgent).toHaveBeenCalledWith("new-agent", file);
+ });
+
+ it("invalidates agents query on success", async () => {
+ vi.mocked(agentApi.createAgent).mockResolvedValue(mockCreatedConfig);
+ const { wrapper, queryClient } = createWrapper();
+ const invalidateSpy = vi.spyOn(queryClient, "invalidateQueries");
+ const file = new File(["model: gpt-4o"], "config.yaml", {
+ type: "application/x-yaml",
+ });
+
+ const { result } = renderHook(() => useCreateAgent(), { wrapper });
+
+ act(() => {
+ result.current.mutate({ name: "new-agent", yamlFile: file });
+ });
+
+ await waitFor(() => expect(result.current.isSuccess).toBe(true));
+ expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ["agents"] });
+ });
+});
diff --git a/tests/unit/hooks/agent/useDeleteAgent.test.tsx b/tests/unit/hooks/agent/useDeleteAgent.test.tsx
new file mode 100644
index 0000000..dff5547
--- /dev/null
+++ b/tests/unit/hooks/agent/useDeleteAgent.test.tsx
@@ -0,0 +1,82 @@
+import { renderHook, waitFor, act } from "@testing-library/react";
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import { vi, describe, it, expect, beforeEach } from "vitest";
+import { useDeleteAgent } from "@/application/hooks/agent/useDeleteAgent";
+import { agentApi } from "@/infrastructure/api/agent/agentApi";
+import type { ReactNode } from "react";
+
+vi.mock("@/infrastructure/api/agent/agentApi", () => ({
+ agentApi: {
+ getAgent: vi.fn(),
+ listAgents: vi.fn(),
+ createAgent: vi.fn(),
+ updateAgent: vi.fn(),
+ deleteAgent: vi.fn(),
+ },
+}));
+
+function createWrapper() {
+ const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: { retry: false, gcTime: 0 },
+ mutations: { retry: false },
+ },
+ });
+ return {
+ wrapper: ({ children }: { children: ReactNode }) => (
+ {children}
+ ),
+ queryClient,
+ };
+}
+
+describe("useDeleteAgent", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it("calls agentApi.deleteAgent on mutate", async () => {
+ vi.mocked(agentApi.deleteAgent).mockResolvedValue(undefined);
+ const { wrapper } = createWrapper();
+
+ const { result } = renderHook(() => useDeleteAgent(), { wrapper });
+
+ act(() => {
+ result.current.mutate("my-agent");
+ });
+
+ await waitFor(() => expect(result.current.isSuccess).toBe(true));
+ expect(agentApi.deleteAgent).toHaveBeenCalledWith("my-agent");
+ });
+
+ it("invalidates agents query on success", async () => {
+ vi.mocked(agentApi.deleteAgent).mockResolvedValue(undefined);
+ const { wrapper, queryClient } = createWrapper();
+ const invalidateSpy = vi.spyOn(queryClient, "invalidateQueries");
+
+ const { result } = renderHook(() => useDeleteAgent(), { wrapper });
+
+ act(() => {
+ result.current.mutate("my-agent");
+ });
+
+ await waitFor(() => expect(result.current.isSuccess).toBe(true));
+ expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ["agents"] });
+ });
+
+ it("returns error state when deletion fails", async () => {
+ vi.mocked(agentApi.deleteAgent).mockRejectedValue(
+ new Error("Forbidden"),
+ );
+ const { wrapper } = createWrapper();
+
+ const { result } = renderHook(() => useDeleteAgent(), { wrapper });
+
+ act(() => {
+ result.current.mutate("protected-agent");
+ });
+
+ await waitFor(() => expect(result.current.isError).toBe(true));
+ expect(result.current.error?.message).toBe("Forbidden");
+ });
+});
diff --git a/tests/unit/hooks/agent/useUpdateAgent.test.tsx b/tests/unit/hooks/agent/useUpdateAgent.test.tsx
new file mode 100644
index 0000000..a6a31bb
--- /dev/null
+++ b/tests/unit/hooks/agent/useUpdateAgent.test.tsx
@@ -0,0 +1,109 @@
+import { renderHook, waitFor, act } from "@testing-library/react";
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import { vi, describe, it, expect, beforeEach } from "vitest";
+import { useUpdateAgent } from "@/application/hooks/agent/useUpdateAgent";
+import { agentApi } from "@/infrastructure/api/agent/agentApi";
+import type { AgentConfig, BackendType } from "@/domain/entities/agent/agentConfig";
+import type { ReactNode } from "react";
+
+vi.mock("@/infrastructure/api/agent/agentApi", () => ({
+ agentApi: {
+ getAgent: vi.fn(),
+ listAgents: vi.fn(),
+ createAgent: vi.fn(),
+ updateAgent: vi.fn(),
+ deleteAgent: vi.fn(),
+ },
+}));
+
+function createWrapper() {
+ const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: { retry: false, gcTime: 0 },
+ mutations: { retry: false },
+ },
+ });
+ return {
+ wrapper: ({ children }: { children: ReactNode }) => (
+ {children}
+ ),
+ queryClient,
+ };
+}
+
+const mockUpdatedConfig: AgentConfig = {
+ name: "my-agent",
+ model: "openai:gpt-4o",
+ tools: [],
+ middleware: [],
+ backend: { type: "state" as BackendType },
+ hitl: { rules: {} },
+ memory: [],
+ skills: [],
+ subagents: [],
+ mcp_servers: [],
+ debug: false,
+};
+
+describe("useUpdateAgent", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it("calls agentApi.updateAgent on mutate", async () => {
+ vi.mocked(agentApi.updateAgent).mockResolvedValue(mockUpdatedConfig);
+ const { wrapper } = createWrapper();
+ const file = new File(["model: gpt-4o"], "config.yaml", {
+ type: "application/x-yaml",
+ });
+
+ const { result } = renderHook(() => useUpdateAgent(), { wrapper });
+
+ act(() => {
+ result.current.mutate({ name: "my-agent", yamlFile: file });
+ });
+
+ await waitFor(() => expect(result.current.isSuccess).toBe(true));
+ expect(agentApi.updateAgent).toHaveBeenCalledWith("my-agent", file);
+ });
+
+ it("invalidates both agents list and specific agent query on success", async () => {
+ vi.mocked(agentApi.updateAgent).mockResolvedValue(mockUpdatedConfig);
+ const { wrapper, queryClient } = createWrapper();
+ const invalidateSpy = vi.spyOn(queryClient, "invalidateQueries");
+ const file = new File(["model: gpt-4o"], "config.yaml", {
+ type: "application/x-yaml",
+ });
+
+ const { result } = renderHook(() => useUpdateAgent(), { wrapper });
+
+ act(() => {
+ result.current.mutate({ name: "my-agent", yamlFile: file });
+ });
+
+ await waitFor(() => expect(result.current.isSuccess).toBe(true));
+ expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ["agents"] });
+ expect(invalidateSpy).toHaveBeenCalledWith({
+ queryKey: ["agent", "my-agent"],
+ });
+ });
+
+ it("returns error state when update fails", async () => {
+ vi.mocked(agentApi.updateAgent).mockRejectedValue(
+ new Error("Invalid YAML"),
+ );
+ const { wrapper } = createWrapper();
+ const file = new File(["invalid"], "bad.yaml", {
+ type: "application/x-yaml",
+ });
+
+ const { result } = renderHook(() => useUpdateAgent(), { wrapper });
+
+ act(() => {
+ result.current.mutate({ name: "my-agent", yamlFile: file });
+ });
+
+ await waitFor(() => expect(result.current.isError).toBe(true));
+ expect(result.current.error?.message).toBe("Invalid YAML");
+ });
+});
diff --git a/tests/unit/hooks/chat/useCreateThread.test.tsx b/tests/unit/hooks/chat/useCreateThread.test.tsx
new file mode 100644
index 0000000..4c40580
--- /dev/null
+++ b/tests/unit/hooks/chat/useCreateThread.test.tsx
@@ -0,0 +1,88 @@
+import { renderHook, waitFor, act } from "@testing-library/react";
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import { vi, describe, it, expect, beforeEach } from "vitest";
+import { useCreateThread } from "@/application/hooks/chat/useCreateThread";
+import { chatApi } from "@/infrastructure/api/chat/chatApi";
+import { createThread } from "../../../fixtures/external";
+import type { ReactNode } from "react";
+
+vi.mock("@/infrastructure/api/chat/chatApi", () => ({
+ chatApi: {
+ listThreads: vi.fn(),
+ createThread: vi.fn(),
+ getThread: vi.fn(),
+ deleteThread: vi.fn(),
+ getMessages: vi.fn(),
+ sendMessage: vi.fn(),
+ streamMessage: vi.fn(),
+ },
+}));
+
+function createWrapper() {
+ const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: { retry: false, gcTime: 0 },
+ mutations: { retry: false },
+ },
+ });
+ return {
+ wrapper: ({ children }: { children: ReactNode }) => (
+ {children}
+ ),
+ queryClient,
+ };
+}
+
+describe("useCreateThread", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it("calls chatApi.createThread on mutate", async () => {
+ const mockThread = createThread({ id: "new-thread", agent_name: "my-agent" });
+ vi.mocked(chatApi.createThread).mockResolvedValue(mockThread);
+ const { wrapper } = createWrapper();
+
+ const { result } = renderHook(() => useCreateThread(), { wrapper });
+
+ act(() => {
+ result.current.mutate("my-agent");
+ });
+
+ await waitFor(() => expect(result.current.isSuccess).toBe(true));
+ expect(chatApi.createThread).toHaveBeenCalledWith("my-agent");
+ expect(result.current.data).toEqual(mockThread);
+ });
+
+ it("invalidates threads query on success", async () => {
+ const mockThread = createThread({ id: "new-thread", agent_name: "my-agent" });
+ vi.mocked(chatApi.createThread).mockResolvedValue(mockThread);
+ const { wrapper, queryClient } = createWrapper();
+ const invalidateSpy = vi.spyOn(queryClient, "invalidateQueries");
+
+ const { result } = renderHook(() => useCreateThread(), { wrapper });
+
+ act(() => {
+ result.current.mutate("my-agent");
+ });
+
+ await waitFor(() => expect(result.current.isSuccess).toBe(true));
+ expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ["threads"] });
+ });
+
+ it("returns error state when creation fails", async () => {
+ vi.mocked(chatApi.createThread).mockRejectedValue(
+ new Error("Agent not found"),
+ );
+ const { wrapper } = createWrapper();
+
+ const { result } = renderHook(() => useCreateThread(), { wrapper });
+
+ act(() => {
+ result.current.mutate("nonexistent-agent");
+ });
+
+ await waitFor(() => expect(result.current.isError).toBe(true));
+ expect(result.current.error?.message).toBe("Agent not found");
+ });
+});
diff --git a/tests/unit/hooks/chat/useDeleteThread.test.tsx b/tests/unit/hooks/chat/useDeleteThread.test.tsx
new file mode 100644
index 0000000..19396e6
--- /dev/null
+++ b/tests/unit/hooks/chat/useDeleteThread.test.tsx
@@ -0,0 +1,84 @@
+import { renderHook, waitFor, act } from "@testing-library/react";
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import { vi, describe, it, expect, beforeEach } from "vitest";
+import { useDeleteThread } from "@/application/hooks/chat/useDeleteThread";
+import { chatApi } from "@/infrastructure/api/chat/chatApi";
+import type { ReactNode } from "react";
+
+vi.mock("@/infrastructure/api/chat/chatApi", () => ({
+ chatApi: {
+ listThreads: vi.fn(),
+ createThread: vi.fn(),
+ getThread: vi.fn(),
+ deleteThread: vi.fn(),
+ getMessages: vi.fn(),
+ sendMessage: vi.fn(),
+ streamMessage: vi.fn(),
+ },
+}));
+
+function createWrapper() {
+ const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: { retry: false, gcTime: 0 },
+ mutations: { retry: false },
+ },
+ });
+ return {
+ wrapper: ({ children }: { children: ReactNode }) => (
+ {children}
+ ),
+ queryClient,
+ };
+}
+
+describe("useDeleteThread", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it("calls chatApi.deleteThread on mutate", async () => {
+ vi.mocked(chatApi.deleteThread).mockResolvedValue(undefined);
+ const { wrapper } = createWrapper();
+
+ const { result } = renderHook(() => useDeleteThread(), { wrapper });
+
+ act(() => {
+ result.current.mutate("thread-123");
+ });
+
+ await waitFor(() => expect(result.current.isSuccess).toBe(true));
+ expect(chatApi.deleteThread).toHaveBeenCalledWith("thread-123");
+ });
+
+ it("invalidates threads query on success", async () => {
+ vi.mocked(chatApi.deleteThread).mockResolvedValue(undefined);
+ const { wrapper, queryClient } = createWrapper();
+ const invalidateSpy = vi.spyOn(queryClient, "invalidateQueries");
+
+ const { result } = renderHook(() => useDeleteThread(), { wrapper });
+
+ act(() => {
+ result.current.mutate("thread-123");
+ });
+
+ await waitFor(() => expect(result.current.isSuccess).toBe(true));
+ expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ["threads"] });
+ });
+
+ it("returns error state when deletion fails", async () => {
+ vi.mocked(chatApi.deleteThread).mockRejectedValue(
+ new Error("Thread not found"),
+ );
+ const { wrapper } = createWrapper();
+
+ const { result } = renderHook(() => useDeleteThread(), { wrapper });
+
+ act(() => {
+ result.current.mutate("unknown-thread");
+ });
+
+ await waitFor(() => expect(result.current.isError).toBe(true));
+ expect(result.current.error?.message).toBe("Thread not found");
+ });
+});
diff --git a/tests/unit/hooks/chat/useMessages.test.tsx b/tests/unit/hooks/chat/useMessages.test.tsx
new file mode 100644
index 0000000..fdc3610
--- /dev/null
+++ b/tests/unit/hooks/chat/useMessages.test.tsx
@@ -0,0 +1,73 @@
+import { renderHook, waitFor } from "@testing-library/react";
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import { vi, describe, it, expect, beforeEach } from "vitest";
+import { useMessages } from "@/application/hooks/chat/useMessages";
+import { chatApi } from "@/infrastructure/api/chat/chatApi";
+import { createMessage } from "../../../fixtures/external";
+import { MessageRole } from "@/domain/entities/chat/message";
+import type { ReactNode } from "react";
+
+vi.mock("@/infrastructure/api/chat/chatApi", () => ({
+ chatApi: {
+ listThreads: vi.fn(),
+ createThread: vi.fn(),
+ getThread: vi.fn(),
+ deleteThread: vi.fn(),
+ getMessages: vi.fn(),
+ sendMessage: vi.fn(),
+ streamMessage: vi.fn(),
+ },
+}));
+
+function createWrapper() {
+ const queryClient = new QueryClient({
+ defaultOptions: { queries: { retry: false, gcTime: 0 } },
+ });
+ return ({ children }: { children: ReactNode }) => (
+ {children}
+ );
+}
+
+describe("useMessages", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it("fetches messages when threadId is provided", async () => {
+ const mockMessages = [
+ createMessage({ role: MessageRole.HUMAN, content: "Hello" }),
+ createMessage({ role: MessageRole.AI, content: "Hi there!" }),
+ ];
+ vi.mocked(chatApi.getMessages).mockResolvedValue(mockMessages);
+
+ const { result } = renderHook(() => useMessages("thread-123"), {
+ wrapper: createWrapper(),
+ });
+
+ await waitFor(() => expect(result.current.isSuccess).toBe(true));
+ expect(result.current.data).toEqual(mockMessages);
+ expect(chatApi.getMessages).toHaveBeenCalledWith("thread-123");
+ });
+
+ it("does not fetch when threadId is null", async () => {
+ const { result } = renderHook(() => useMessages(null), {
+ wrapper: createWrapper(),
+ });
+
+ expect(result.current.isFetching).toBe(false);
+ expect(chatApi.getMessages).not.toHaveBeenCalled();
+ });
+
+ it("returns error state when API call fails", async () => {
+ vi.mocked(chatApi.getMessages).mockRejectedValue(
+ new Error("Thread not found"),
+ );
+
+ const { result } = renderHook(() => useMessages("bad-thread"), {
+ wrapper: createWrapper(),
+ });
+
+ await waitFor(() => expect(result.current.isError).toBe(true));
+ expect(result.current.error?.message).toBe("Thread not found");
+ });
+});
diff --git a/tests/unit/hooks/chat/useSendMessage.test.tsx b/tests/unit/hooks/chat/useSendMessage.test.tsx
new file mode 100644
index 0000000..8cead14
--- /dev/null
+++ b/tests/unit/hooks/chat/useSendMessage.test.tsx
@@ -0,0 +1,101 @@
+import { renderHook, waitFor, act } from "@testing-library/react";
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import { vi, describe, it, expect, beforeEach } from "vitest";
+import { useSendMessage } from "@/application/hooks/chat/useSendMessage";
+import { chatApi } from "@/infrastructure/api/chat/chatApi";
+import { createMessage } from "../../../fixtures/external";
+import { MessageRole } from "@/domain/entities/chat/message";
+import type { ReactNode } from "react";
+
+vi.mock("@/infrastructure/api/chat/chatApi", () => ({
+ chatApi: {
+ listThreads: vi.fn(),
+ createThread: vi.fn(),
+ getThread: vi.fn(),
+ deleteThread: vi.fn(),
+ getMessages: vi.fn(),
+ sendMessage: vi.fn(),
+ streamMessage: vi.fn(),
+ },
+}));
+
+function createWrapper() {
+ const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: { retry: false, gcTime: 0 },
+ mutations: { retry: false },
+ },
+ });
+ return {
+ wrapper: ({ children }: { children: ReactNode }) => (
+ {children}
+ ),
+ queryClient,
+ };
+}
+
+describe("useSendMessage", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it("calls chatApi.sendMessage with threadId and request", async () => {
+ const mockResponse = createMessage({
+ role: MessageRole.AI,
+ content: "Response",
+ });
+ vi.mocked(chatApi.sendMessage).mockResolvedValue(mockResponse);
+ const { wrapper } = createWrapper();
+
+ const { result } = renderHook(() => useSendMessage("thread-123"), {
+ wrapper,
+ });
+
+ act(() => {
+ result.current.mutate({ message: "Hello" });
+ });
+
+ await waitFor(() => expect(result.current.isSuccess).toBe(true));
+ expect(chatApi.sendMessage).toHaveBeenCalledWith("thread-123", {
+ message: "Hello",
+ });
+ });
+
+ it("throws error when threadId is null", async () => {
+ const { wrapper } = createWrapper();
+
+ const { result } = renderHook(() => useSendMessage(null), { wrapper });
+
+ act(() => {
+ result.current.mutate({ message: "Hello" });
+ });
+
+ await waitFor(() => expect(result.current.isError).toBe(true));
+ expect(result.current.error?.message).toBe(
+ "Cannot send message without an active thread",
+ );
+ });
+
+ it("invalidates messages query on success", async () => {
+ const mockResponse = createMessage({
+ role: MessageRole.AI,
+ content: "Response",
+ });
+ vi.mocked(chatApi.sendMessage).mockResolvedValue(mockResponse);
+ const { wrapper, queryClient } = createWrapper();
+ const invalidateSpy = vi.spyOn(queryClient, "invalidateQueries");
+
+ const { result } = renderHook(() => useSendMessage("thread-123"), {
+ wrapper,
+ });
+
+ act(() => {
+ result.current.mutate({ message: "Hello" });
+ });
+
+ await waitFor(() => expect(result.current.isSuccess).toBe(true));
+ expect(invalidateSpy).toHaveBeenCalledWith({
+ queryKey: ["messages", "thread-123"],
+ });
+ });
+});
diff --git a/tests/unit/hooks/chat/useThreads.test.tsx b/tests/unit/hooks/chat/useThreads.test.tsx
new file mode 100644
index 0000000..49d1800
--- /dev/null
+++ b/tests/unit/hooks/chat/useThreads.test.tsx
@@ -0,0 +1,63 @@
+import { renderHook, waitFor } from "@testing-library/react";
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import { vi, describe, it, expect, beforeEach } from "vitest";
+import { useThreads } from "@/application/hooks/chat/useThreads";
+import { chatApi } from "@/infrastructure/api/chat/chatApi";
+import { createThread } from "../../../fixtures/external";
+import type { ReactNode } from "react";
+
+vi.mock("@/infrastructure/api/chat/chatApi", () => ({
+ chatApi: {
+ listThreads: vi.fn(),
+ createThread: vi.fn(),
+ getThread: vi.fn(),
+ deleteThread: vi.fn(),
+ getMessages: vi.fn(),
+ sendMessage: vi.fn(),
+ streamMessage: vi.fn(),
+ },
+}));
+
+function createWrapper() {
+ const queryClient = new QueryClient({
+ defaultOptions: { queries: { retry: false, gcTime: 0 } },
+ });
+ return ({ children }: { children: ReactNode }) => (
+ {children}
+ );
+}
+
+describe("useThreads", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it("fetches threads from chatApi.listThreads", async () => {
+ const mockThreads = [
+ createThread({ id: "t-1", agent_name: "agent-a" }),
+ createThread({ id: "t-2", agent_name: "agent-b" }),
+ ];
+ vi.mocked(chatApi.listThreads).mockResolvedValue(mockThreads);
+
+ const { result } = renderHook(() => useThreads(), {
+ wrapper: createWrapper(),
+ });
+
+ await waitFor(() => expect(result.current.isSuccess).toBe(true));
+ expect(result.current.data).toEqual(mockThreads);
+ expect(chatApi.listThreads).toHaveBeenCalledOnce();
+ });
+
+ it("returns error state when API call fails", async () => {
+ vi.mocked(chatApi.listThreads).mockRejectedValue(
+ new Error("Network error"),
+ );
+
+ const { result } = renderHook(() => useThreads(), {
+ wrapper: createWrapper(),
+ });
+
+ await waitFor(() => expect(result.current.isError).toBe(true));
+ expect(result.current.error?.message).toBe("Network error");
+ });
+});
diff --git a/tests/unit/infrastructure/axiosInstance.test.ts b/tests/unit/infrastructure/axiosInstance.test.ts
new file mode 100644
index 0000000..ff825b9
--- /dev/null
+++ b/tests/unit/infrastructure/axiosInstance.test.ts
@@ -0,0 +1,92 @@
+import { describe, it, expect, vi } from "vitest";
+
+vi.mock("@/infrastructure/config/envConfig", () => ({
+ envConfig: {
+ apiBaseUrl: "http://test-api:8010",
+ wsBaseUrl: "ws://test-api:8010",
+ },
+}));
+
+describe("axiosInstance", () => {
+ it("apiClient has correct baseURL from envConfig", async () => {
+ const { apiClient } = await import("@/infrastructure/api/axiosInstance");
+
+ expect(apiClient.defaults.baseURL).toBe("http://test-api:8010");
+ });
+
+ it("apiClient has 30 second timeout", async () => {
+ const { apiClient } = await import("@/infrastructure/api/axiosInstance");
+
+ expect(apiClient.defaults.timeout).toBe(30000);
+ });
+
+ it("apiClient has Content-Type header set to application/json", async () => {
+ const { apiClient } = await import("@/infrastructure/api/axiosInstance");
+
+ expect(apiClient.defaults.headers["Content-Type"]).toBe("application/json");
+ });
+
+ it("error interceptor extracts detail from response", async () => {
+ const { apiClient } = await import("@/infrastructure/api/axiosInstance");
+
+ // Simulate an axios error with response.data.detail
+ const axiosError = {
+ response: {
+ status: 400,
+ data: { detail: "Agent not found" },
+ },
+ message: "Request failed with status code 400",
+ };
+
+ // Get the error interceptor (second argument of the response interceptor)
+ const interceptors = (apiClient.interceptors.response as any).handlers;
+ const errorHandler = interceptors[0]?.rejected;
+
+ expect(errorHandler).toBeDefined();
+
+ try {
+ await errorHandler(axiosError);
+ } catch (error: any) {
+ expect(error).toBeInstanceOf(Error);
+ expect(error.message).toBe("Agent not found");
+ }
+ });
+
+ it("error interceptor falls back to error.message when detail is absent", async () => {
+ const { apiClient } = await import("@/infrastructure/api/axiosInstance");
+
+ const axiosError = {
+ response: {
+ status: 500,
+ data: {},
+ },
+ message: "Internal Server Error",
+ };
+
+ const interceptors = (apiClient.interceptors.response as any).handlers;
+ const errorHandler = interceptors[0]?.rejected;
+
+ try {
+ await errorHandler(axiosError);
+ } catch (error: any) {
+ expect(error).toBeInstanceOf(Error);
+ expect(error.message).toBe("Internal Server Error");
+ }
+ });
+
+ it("error interceptor passes through errors without response", async () => {
+ const { apiClient } = await import("@/infrastructure/api/axiosInstance");
+
+ const networkError = new Error("Network Error");
+
+ const interceptors = (apiClient.interceptors.response as any).handlers;
+ const errorHandler = interceptors[0]?.rejected;
+
+ try {
+ await errorHandler(networkError);
+ } catch (error: any) {
+ expect(error).toBeInstanceOf(Error);
+ expect(error.message).toBe("Network Error");
+ }
+ });
+});
diff --git a/tests/unit/pages/AgentsPage.test.tsx b/tests/unit/pages/AgentsPage.test.tsx
index f4edd86..396191b 100644
--- a/tests/unit/pages/AgentsPage.test.tsx
+++ b/tests/unit/pages/AgentsPage.test.tsx
@@ -1,4 +1,5 @@
import { screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
import { describe, it, expect, vi } from "vitest";
import { renderWithProviders } from "../../utils/render";
import AgentsPage from "@/application/pages/AgentsPage";
@@ -19,6 +20,27 @@ vi.mock("@/application/hooks/agent/useCreateAgent", () => ({
}),
}));
+vi.mock("@/application/hooks/agent/useAgentConfig", () => ({
+ useAgentConfig: () => ({
+ data: undefined,
+ isLoading: false,
+ }),
+}));
+
+vi.mock("@/application/hooks/agent/useDeleteAgent", () => ({
+ useDeleteAgent: () => ({
+ mutate: vi.fn(),
+ isPending: false,
+ }),
+}));
+
+vi.mock("sonner", () => ({
+ toast: {
+ success: vi.fn(),
+ error: vi.fn(),
+ },
+}));
+
describe("AgentsPage", () => {
it("renders Active Agents heading", () => {
renderWithProviders(, { initialEntries: ["/agents"] });
@@ -41,4 +63,40 @@ describe("AgentsPage", () => {
expect(screen.getByText("my-agent")).toBeInTheDocument();
});
+
+ it("opens CreateAgentDialog when Create Agent button is clicked", async () => {
+ const user = userEvent.setup();
+
+ renderWithProviders(, { initialEntries: ["/agents"] });
+
+ await user.click(
+ screen.getByRole("button", { name: /create agent/i }),
+ );
+
+ expect(screen.getByText("Create Agent")).toBeInTheDocument();
+ expect(screen.getByLabelText(/agent name/i)).toBeInTheDocument();
+ });
+
+ it("opens AgentConfigViewer when Configure button is clicked", async () => {
+ const user = userEvent.setup();
+
+ renderWithProviders(, { initialEntries: ["/agents"] });
+
+ await user.click(
+ screen.getByRole("button", { name: /configure/i }),
+ );
+
+ // The AgentConfigViewer should be rendered with Delete button (unique to viewer)
+ expect(screen.getByRole("button", { name: /^delete$/i })).toBeInTheDocument();
+ // The dialog should be present
+ expect(screen.getByRole("dialog")).toBeInTheDocument();
+ });
+
+ it("renders description text", () => {
+ renderWithProviders(, { initialEntries: ["/agents"] });
+
+ expect(
+ screen.getByText(/configure and manage your ai agent fleet/i),
+ ).toBeInTheDocument();
+ });
});