diff --git a/.bazelversion b/.bazelversion new file mode 100644 index 00000000..6d289079 --- /dev/null +++ b/.bazelversion @@ -0,0 +1 @@ +8.5.0 diff --git a/.github/ISSUE_TEMPLATE/1.BUG_REPORT.yml b/.github/ISSUE_TEMPLATE/1.BUG_REPORT.yml deleted file mode 100644 index 964aba61..00000000 --- a/.github/ISSUE_TEMPLATE/1.BUG_REPORT.yml +++ /dev/null @@ -1,132 +0,0 @@ -name: Bug report -description: Create a bug report for SuperCoder. -labels: ['status: needs triage'] -body: - - type: markdown - attributes: - value: | - ### ⚠️ Issue Creation Guideline - * Join our [discord](https://discord.gg/dXbRe5BHJC) to discuss what's going on - * If you need help, you can ask in the [#general-discussion](https://discord.com/channels/1107593006032355359/1257985438627663924) section or in [#supercoder-support](https://discord.com/channels/1107593006032355359/1256153481387970632) - * **Thoroughly search the [existing issues](https://github.com/TransformerOptimus/SuperCoder/issues) before creating a new one** - - - type: checkboxes - attributes: - label: ⚠️ Check for existing issues before proceeding. ⚠️ - description: > - Please [search the history](https://github.com/TransformerOptimus/SuperCoder/issues) - to see if an issue already exists for the same problem. - options: - - label: I have searched the existing issues, and there is no existing issue for my problem - required: true - - - type: markdown - attributes: - value: | - Please confirm that the issue you have is described well and precise in the title above ⬆️. - Think like this: What would you type if you were searching for the issue? - - For example: - ❌ - SuperCoder workbench server shows 503 error. - ✅ - Even after story is executed completely and moved to "Done" stage, SuperCoder workbench server shows 503 error. - - Please help us help you by following these steps: - - Search for existing issues, adding a comment when you have the same or similar issue is tidier than "new issue" and - newer issues will not be reviewed earlier, this is dependent on the current priorities set by our wonderful team - - Ask on our Discord if your issue is known when you are unsure (https://discord.gg/dXbRe5BHJC) - - Provide relevant info: - - Provide Docker Logs(docker compose logs) and workbench ID (looks like "be-oqajm-24ec535d-eb35-4099-91c2-6fb7415337dd") whenever possible. - - If it's a pip/packages issue, mention this in the title and provide pip version, python version. - - Check network tab and provide any screenshot if you see a failing request with your issue. - - type: dropdown - attributes: - label: Where are you using SuperCoder? - description: > - Please select the operating system you were using to run SuperCoder when this problem occurred. - options: - - Windows - - Linux - - MacOS - - Codespaces - - Cloud - - Other - validations: - required: true - nested_fields: - - type: text - attributes: - label: Specify the system - description: Please specify the system you are working on. - - - type: dropdown - attributes: - label: Which branch of SuperCoder are you using? - description: | - Please select which version of SuperCoder you were using when this issue occurred. - If installed with git you can run `git branch` to see which version of SuperCoder you are running. - options: - - Main - - Cloud Version - - Other - validations: - required: true - - - - type: dropdown - attributes: - label: Which area covers your issue best? - description: > - Select the area related to the issue you are reporting. - options: - - Installation and setup - - Front-End Board - - Front-End Workbench - - Back-End Board - - Back-End Workbench - - Code Editor - - Pull Request - - Settings - - Other - validations: - required: true - autolabels: true - nested_fields: - - type: text - attributes: - label: Specify the area - description: Please specify the area you think is best related to the issue. - - - type: textarea - attributes: - label: Describe your issue. - description: Describe the problem you are experiencing. Try to describe only the issue and phrase it short but clear. ⚠️ Provide NO other data in this field - validations: - required: true - - - type: textarea - attributes: - label: How to replicate your Issue? - description: | - Always Mention Project Name, Workbench ID. - For Back-End Story mention - Story Summary, Description, Test Cases and Dev instructions. - Provide any other data which might be relevant for us to replicate this issue. - ⚠️ Provide NO other data in this field - validations: - required: false - - - type: markdown - attributes: - value: | - ⚠️ Please keep in mind that the log files may contain personal information such as credentials. Make sure you hide them before copy/pasting it! ⚠️ - - type: input - attributes: - label: Upload Error Log Content - description: | - Upload the error log content, this can help us understand the issue better. - To do this, you can simply copy the logs from the terminal with which you did 'docker compose up' or in a new terminal, - enter 'docker compose logs' and copy/paste the error contents to this field. - If you're using the cloud version, add network tab details or any traceback error if applicable. if no error log present, write N/A. - ⚠️ The activity log may contain personal data given to SuperCoder by you in prompt or input as well as - any personal information that SuperCoder collected out of files during last run. Please hide them before sharing. ⚠️ - validations: - required: true diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml new file mode 100644 index 00000000..7a9740c0 --- /dev/null +++ b/.github/release-drafter.yml @@ -0,0 +1,31 @@ +name-template: 'v$RESOLVED_VERSION' +tag-template: 'v$RESOLVED_VERSION' +categories: + - title: '🚀 Features' + labels: + - 'feature' + - 'enhancement' + - title: '🐛 Bug Fixes' + labels: + - 'fix' + - 'bugfix' + - 'bug' + - title: '🧰 Maintenance' + label: 'chore' +change-template: '- $TITLE @$AUTHOR (#$NUMBER)' +change-title-escapes: '\<*_&' +version-resolver: + major: + labels: + - 'major' + minor: + labels: + - 'minor' + patch: + labels: + - 'patch' + default: patch +template: | + ## Changes + + $CHANGES diff --git a/.github/scripts/set-version.mjs b/.github/scripts/set-version.mjs new file mode 100644 index 00000000..37ed1145 --- /dev/null +++ b/.github/scripts/set-version.mjs @@ -0,0 +1,33 @@ +// Stamp a release version into the three desktop-app manifests so the bundle +// version matches the published tag. Run from the repo root: +// node .github/scripts/set-version.mjs 1.2.3 +import { readFileSync, writeFileSync } from 'node:fs'; + +const version = process.argv[2]; +if (!version) { + console.error('usage: set-version.mjs '); + process.exit(1); +} + +function setJson(path, key) { + const obj = JSON.parse(readFileSync(path, 'utf8')); + obj[key] = version; + writeFileSync(path, JSON.stringify(obj, null, 2) + '\n'); +} + +setJson('apps/desktop/package.json', 'version'); +setJson('apps/desktop/src-tauri/tauri.conf.json', 'version'); + +const cargoPath = 'apps/desktop/src-tauri/Cargo.toml'; +const cargo = readFileSync(cargoPath, 'utf8'); +// Replace only the [package] version (first `version = "..."` at column 0). +// Assert the line exists so a layout change fails loudly instead of silently +// shipping a stale version (a same-version no-op stamp is still fine). +const versionLine = /^version = ".*"/m; +if (!versionLine.test(cargo)) { + console.error(`${cargoPath}: no [package] version line matched — aborting`); + process.exit(1); +} +writeFileSync(cargoPath, cargo.replace(versionLine, `version = "${version}"`)); + +console.log(`stamped version ${version}`); diff --git a/.github/workflows/build-ide-image.yaml b/.github/workflows/build-ide-image.yaml deleted file mode 100644 index 47a2a144..00000000 --- a/.github/workflows/build-ide-image.yaml +++ /dev/null @@ -1,42 +0,0 @@ -name: Build IDE image - -on: - workflow_dispatch: - inputs: - environment: - type: choice - description: 'The ide to build' - options: - - python - - node - -concurrency: - group: build-ide-image - cancel-in-progress: false - -jobs: - build: - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v2 - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Login to Docker Hub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Build and push - uses: docker/build-push-action@v6 - with: - platforms: linux/amd64,linux/arm64 - context: ./ide/${{ github.event.inputs.environment }} - push: true - tags: superagidev/supercoder-${{ github.event.inputs.environment }}-ide:latest \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..23502e50 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,84 @@ +name: CI + +# Lightweight checks on PRs and pushes to main: the Rust workspace compiles, +# the desktop frontend type-checks, and the context-engine service builds. +# (Release builds + image publishing live in release.yml / context-engine-images.yml.) +on: + pull_request: + push: + branches: [main] + +permissions: + contents: read + +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + rust: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + + - uses: dtolnay/rust-toolchain@stable + + - uses: swatinem/rust-cache@v2 + + - name: Install Tauri Linux dependencies + run: | + sudo apt-get update + sudo apt-get install -y \ + libwebkit2gtk-4.1-dev \ + libgtk-3-dev \ + libayatana-appindicator3-dev \ + librsvg2-dev \ + patchelf \ + libssl-dev \ + libxdo-dev + + - name: Stub frontend assets + # tauri::generate_context! requires frontendDist (../dist) to exist at + # compile time. The frontend job builds it for real; a stub is enough to + # type-check the Rust here. + run: | + mkdir -p apps/desktop/dist + echo 'stub' > apps/desktop/dist/index.html + + - name: cargo check (workspace) + run: cargo check --workspace --all-targets + + frontend: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + cache-dependency-path: apps/desktop/package-lock.json + + - name: Install dependencies + working-directory: apps/desktop + run: npm ci + + - name: Build (type-check + bundle) + working-directory: apps/desktop + run: npm run build + + context-engine: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache: true + + - name: Build + run: go build ./services/... + + - name: Vet + run: go vet ./services/... diff --git a/.github/workflows/context-engine-images.yml b/.github/workflows/context-engine-images.yml new file mode 100644 index 00000000..691a5c09 --- /dev/null +++ b/.github/workflows/context-engine-images.yml @@ -0,0 +1,71 @@ +name: Context Engine Images + +# Builds and pushes the optional context-engine Docker images (server+worker and +# the one-shot migrate) as multi-arch images to GHCR. Rides the same "Publish" +# event as the app so they're versioned together. The future app-managed engine +# (Phase 6) pulls these by the release tag. +on: + release: + types: [published] + workflow_dispatch: + inputs: + version: + description: "Version tag to publish (e.g. 0.1.0)." + required: false + +permissions: + contents: read + packages: write + +jobs: + build-push: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + # server + worker: built from the repo root (needs the root go.mod). + - name: context-engine + context: . + dockerfile: services/context-engine/Dockerfile + # one-shot Atlas migrations: built from the service dir. + - name: context-engine-migrate + context: services/context-engine + dockerfile: services/context-engine/Dockerfile.migrate + steps: + - uses: actions/checkout@v4 + + - uses: docker/setup-qemu-action@v3 + + - uses: docker/setup-buildx-action@v3 + + - uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Image metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ghcr.io/${{ github.repository }}/${{ matrix.name }} + tags: | + type=semver,pattern=v{{version}} + type=raw,value=${{ github.event.inputs.version }},enable=${{ github.event_name == 'workflow_dispatch' && github.event.inputs.version != '' }} + type=raw,value=latest,enable=${{ github.event_name == 'release' }} + type=sha,format=short + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: ${{ matrix.context }} + file: ${{ matrix.dockerfile }} + platforms: linux/amd64,linux/arm64 + # Push on a real release, or on a manual dispatch only when a version is + # given. A bare workflow_dispatch is a build-only dry run (nothing pushed). + push: ${{ github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.version != '') }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml new file mode 100644 index 00000000..5b009c38 --- /dev/null +++ b/.github/workflows/release-drafter.yml @@ -0,0 +1,21 @@ +name: Release Drafter + +on: + push: + branches: [main] + +permissions: + contents: read + +jobs: + update_release_draft: + permissions: + contents: write # write the draft release + pull-requests: read # read PR titles/labels for the changelog + runs-on: ubuntu-latest + steps: + - uses: release-drafter/release-drafter@v6 + with: + config-name: release-drafter.yml + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..ccc4d241 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,107 @@ +name: Release (desktop app) + +# Builds the SuperCoder desktop app for all platforms and attaches the installers +# to the published GitHub Release. The release itself is drafted by +# release-drafter and goes live when you click "Publish". +on: + release: + types: [published] + workflow_dispatch: + inputs: + version: + description: "Version to stamp for a build-only test run (e.g. 0.1.0). Not published." + required: false + +permissions: + contents: write + +jobs: + build: + strategy: + fail-fast: false + matrix: + include: + - runner: macos-latest + target: aarch64-apple-darwin + - runner: macos-13 + target: x86_64-apple-darwin + - runner: windows-latest + target: x86_64-pc-windows-msvc + - runner: ubuntu-22.04 + target: x86_64-unknown-linux-gnu + runs-on: ${{ matrix.runner }} + steps: + - uses: actions/checkout@v4 + + - name: Determine version + id: ver + shell: bash + run: | + if [ "${{ github.event_name }}" = "release" ]; then + echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT" + else + echo "version=${{ github.event.inputs.version }}" >> "$GITHUB_OUTPUT" + fi + + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + cache-dependency-path: apps/desktop/package-lock.json + + - name: Stamp version into app manifests + if: steps.ver.outputs.version != '' + run: node .github/scripts/set-version.mjs "${{ steps.ver.outputs.version }}" + + - uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.target }} + + - uses: swatinem/rust-cache@v2 + with: + key: ${{ matrix.target }} + + - name: Install Linux build dependencies + if: matrix.runner == 'ubuntu-22.04' + run: | + sudo apt-get update + sudo apt-get install -y \ + libwebkit2gtk-4.1-dev \ + libgtk-3-dev \ + libayatana-appindicator3-dev \ + librsvg2-dev \ + patchelf \ + libssl-dev \ + libxdo-dev + + - name: Install frontend dependencies + working-directory: apps/desktop + run: npm ci + + - name: Build (and publish on release) + uses: tauri-apps/tauri-action@v0 + id: tauri + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # macOS signing/notarization — used only if these secrets are configured. + # When absent they're empty and the build is produced UNSIGNED (no rework + # needed to turn signing on later: just add the secrets). + APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} + APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + with: + projectPath: apps/desktop + args: --target ${{ matrix.target }} + # Attach artifacts to the published release; empty on workflow_dispatch. + releaseId: ${{ github.event_name == 'release' && github.event.release.id || '' }} + + - name: Upload test bundles (workflow_dispatch only) + if: github.event_name == 'workflow_dispatch' + uses: actions/upload-artifact@v4 + with: + name: supercoder-${{ matrix.target }} + path: ${{ join(fromJSON(steps.tauri.outputs.artifactPaths), '\n') }} + if-no-files-found: ignore diff --git a/.gitignore b/.gitignore index f09ff79b..d1d10f66 100644 --- a/.gitignore +++ b/.gitignore @@ -1,35 +1,45 @@ -# Binaries for programs and plugins +# Rust +target/ + +# Node +node_modules/ +dist/ +*.tsbuildinfo + +# Go *.exe *.exe~ *.dll *.so *.dylib *.test - -# Output of the go coverage tool, specifically when used with LiteIDE *.out - -# Dependency directories (remove the comment below if you use vendoring) vendor/ - -# Go workspace file go.work -# IDE and editor directories and files +# Compiled context-engine binaries built at repo root +/server +/worker +/migrations + +# Bazel +bazel-* + +# IDE / editor .idea/ +.vscode/ *.swp *.swo *.swn *~ -# Mac OS specific +# OS .DS_Store - -# Windows specific Thumbs.db -#workspace virtual environment -workspace/venv/ +# Env .env .envrc -gui/.husky/pre-commit \ No newline at end of file + +# Tauri generated +apps/*/src-tauri/gen/ diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 00000000..e17c8b03 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,125 @@ +# Architecture + +SuperCoder is a polyglot monorepo built around one idea: **the agent core is the +spine, everything else is an adapter over it.** The Rust agent crate owns the +loop, the tools, and the provider protocols. The desktop app is one adapter; a +headless benchmark runner (planned) will be another. No adapter reaches into the +core's internals. + +``` +┌─────────────────────────────────────────────────────────────┐ +│ apps/desktop - Tauri 2 + React (adapter #1) │ +│ React UI <-- Tauri IPC --> agent_bridge │ +│ sessions / diff / terminal / plan / settings │ +│ SQLite-only persistence + checkpoints │ +└──────────────────────────────┬──────────────────────────────┘ + │ drives + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ crates/agent - the harness (the spine) │ +│ loop / tools / Ask / Plan / Coding modes │ +│ subagents / skills / approval / prompt cache │ +│ llm/ native OpenAI + Anthropic │ +│ checkpoints via crates/git-ops │ +└────────────┬─────────────────────────────────┬──────────────┘ + │ │ (optional) + ▼ ▼ +┌──────────────────────────┐ ┌──────────────────────────────┐ +│ LLM provider │ │ services/context-engine │ +│ your endpoint, │ │ tree-sitter -> Qdrant + │ +│ your key │ │ FalkorDB + BM25 (:8106) │ +└──────────────────────────┘ └──────────────────────────────┘ +``` + +## Components + +### `crates/agent` — the agent core (the spine) + +A pure-Rust crate with no dependency on the desktop app, no chat/UI coupling, and +no network framework assumptions. It contains: + +- **The agent loop** — turn orchestration, streaming events, compaction, token +accounting. +- **Tools** — file read/write/edit, bash, search, plus the optional +`codebase_search` / `codebase_graph` tools (gated on the Context Engine). +- **Modes** — Ask, Plan, and Coding. The mode is fixed per session and chosen at +session creation. `save_plan` / `ask_user` yield to the adapter, not to any +remote service. +- **Subagents & skills** — spawnable child agents (with their own approval +routing) and reusable skill definitions under `default-subagents/`. +- **Providers** — a `Provider` enum (OpenAI | Anthropic). `llm/openai.rs` and +`llm/anthropic.rs` speak each wire format natively: URL, auth, SSE parsing, +prompt-cache markers, and extended thinking are owned here. There is **no +translation proxy** in the runtime path. +- **Persistence trait** — the core defines a message-persistence interface keyed +by `session_id`; the adapter supplies the implementation. + +The core never decides *where* messages are stored or *how* tool approvals are +surfaced — it calls trait methods the adapter implements. + +### `crates/git-ops` — checkpoints over the working tree + +Since the agent edits the project **in place** (no per-session git worktrees), +`git-ops` provides file-snapshot checkpoints: `backup_file` captures a file's +prior contents (first-write-wins per turn), `restore_to` reverts exactly the +files a turn touched, and `diff_turn` / `list` / `delete_from` drive diff review +and rewind. Restore is bounded to the project root (`is_within_root`) so a +checkpoint can never write outside the session folder. + +### `apps/desktop` — the desktop adapter + +- `**src-tauri/src/agent_bridge/`** — the bridge between Tauri and the agent core. +`commands.rs` exposes the Tauri commands the UI calls; `events.rs` relays agent +events to the frontend; `db.rs` is the **local SQLite** store +(`agent_data.db`) — the sole datastore. There is no remote persistence. Each +session gets a `checkpoint_dir` under the app data directory, outside the user's +project. +- `**src/`** — a React (Vite + antd + zustand) UI: session-list sidebar, diff +review, interactive terminal, file explorer, plan panel, subagent/skill/ +permission dialogs, and Settings (LLM providers + Context Engine toggle). + +The bridge implements the core's persistence and approval traits; it does not +reach inside the loop. + +### `services/context-engine` — optional graph-aware retrieval + +A Go service (built and run via `docker compose`) that indexes a repository with +tree-sitter and serves semantic + structural search: + +- **Stores:** Qdrant (vectors), FalkorDB (call/symbol graph), BM25 (lexical), +Postgres (metadata), Redis (queue). A worker (Asynq) does the indexing. +- **Streaming sync:** the app streams the repo up on session start and +incrementally thereafter — `/index/diff` → `/index/stream` → `/index/sync-complete` +(gzipped NDJSON). A Merkle tree on a local-disk volume makes incremental syncs +cheap. +- **Opt-in:** disabled by default. When off, `codebase_search` / `codebase_graph` +are gated off and the agent runs fully zero-backend. Enabled via a Settings +toggle + editable `base_url` (default `http://127.0.0.1:8106`). +- **Config:** environment prefix `SUPERCODER_`. The server-side embedding key +lives in the service's `.env`, never in the app. + +### `v1/` — frozen legacy + +The 2024 autonomous-dev codegen pipeline, preserved verbatim for history. It is +not built, tested, or maintained, and shares nothing with the current code beyond +the name. + +## Data & trust boundaries + +- **Your code → LLM provider.** Source and prompts go directly from your machine +to the endpoint you configured in Settings. No SuperCoder-operated server sits +in between. +- **Your code → Context Engine (optional, local).** When enabled, the repo is +streamed to the locally-running Docker stack. The embedding key it uses is the +service's own, kept server-side. +- **Local persistence.** Sessions, messages, and checkpoints live in local SQLite +and the app data directory — nothing is synced anywhere. + +## Why this shape + +Keeping the harness as a standalone crate with trait-shaped seams means the same +core can be driven by the desktop app today and by a headless benchmark runner +later (see the Roadmap in the [README](./README.md)) without forking logic. The +provider protocols living *in* the core — rather than behind a gateway — is what +makes "bring your own model, no middleman" true at the architecture level, not +just the marketing level. \ No newline at end of file diff --git a/BUILD.bazel b/BUILD.bazel new file mode 100644 index 00000000..8d7eccb4 --- /dev/null +++ b/BUILD.bazel @@ -0,0 +1,9 @@ +load("@bazel_gazelle//:def.bzl", "gazelle") + +# gazelle:prefix github.com/TransformerOptimus/SuperCoder +# resty.dev/v3's library target is //:resty, not the version-suffix default //:v3 +# gazelle:resolve go resty.dev/v3 @dev_resty_v3//:resty +# gazelle:exclude v1 +# gazelle:exclude crates +# gazelle:exclude apps +gazelle(name = "gazelle") diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 18c91471..db879a42 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -59,8 +59,7 @@ representative at an online or offline event. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported to the community leaders responsible for enforcement at -. +reported privately to the project maintainers. All complaints will be reviewed and investigated promptly and fairly. All community leaders are obligated to respect the privacy and security of the diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..4d3df601 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,101 @@ +# Contributing to SuperCoder + +Thanks for your interest in contributing. This guide covers how to set up a dev +environment, where things live, and what we expect in a pull request. + +By participating you agree to abide by our [Code of Conduct](./CODE_OF_CONDUCT.md). + +## Where to start + +- **Bugs & feature requests:** open a GitHub Issue. +- **Questions & ideas:** start a GitHub Discussion. +- **Security issues:** do **not** open a public issue — see [SECURITY.md](./SECURITY.md). + +If you're planning a non-trivial change, open an issue or discussion first so we +can agree on the approach before you write code. + +## Repository layout + +``` +crates/agent/ Rust agent core (the harness) — loop, tools, modes, providers +crates/git-ops/ Checkpoint / diff / restore over the working tree +apps/desktop/ Tauri 2 + React desktop app (src-tauri = Rust bridge, src = React) +services/context-engine/ Optional Go indexing service (run via docker compose) +v1/ Frozen 2024 legacy pipeline — do not modify +``` + +See [ARCHITECTURE.md](./ARCHITECTURE.md) for how the pieces relate. The agent +core is intentionally decoupled from the app: keep adapter concerns +(persistence, UI, IPC) in `apps/desktop`, and keep loop/tool/provider logic in +`crates/agent`. + +## Prerequisites + +- [Rust](https://rustup.rs/) (stable; the crates use edition 2021) and the + [Tauri 2 system prerequisites](https://v2.tauri.app/start/prerequisites/) for + your platform. +- [Node.js](https://nodejs.org/) 20+ and npm. +- [Docker](https://docs.docker.com/get-docker/) with Compose — only needed to + work on the Context Engine. +- A Go toolchain (1.25+) — only needed to work on the Context Engine outside of + Docker. + +## Building and running + +**Desktop app** + +```bash +cd apps/desktop +npm install +npm run tauri:dev # run the app in development +npm run tauri:build # produce a release bundle +``` + +**Agent core / git-ops (Rust)** + +```bash +cargo build # whole workspace +cargo test -p agent # core unit + integration tests +cargo test -p git-ops # checkpoint tests +``` + +Live LLM round-trip tests are marked `#[ignore]` and require a real API key in +the environment; run them explicitly with `cargo test -p agent -- --ignored`. +Don't commit keys — pass them via environment variables only. + +**Context Engine (Go service)** + +```bash +cd services/context-engine +cp .env.example .env # set SUPERCODER_OPENAI_API_KEY +docker compose up -d --build +``` + +See [`services/context-engine/README.md`](./services/context-engine/README.md) +for health checks and wiring it to the app. + +## Conventions + +- **Follow the surrounding code.** Match existing naming, structure, and idioms + in the file/crate you're editing rather than introducing new patterns. +- **Keep changes surgical.** Prefer small, focused PRs that do one thing. +- **Rust:** keep the core free of app/UI/network-framework coupling; new tools + and providers belong behind the existing trait/enum seams. +- **Commits:** we use [Conventional Commits](https://www.conventionalcommits.org/) + prefixes (`feat:`, `fix:`, `chore:`, `docs:`, …), optionally scoped + (`feat(agent): …`). +- **Tests:** add or update tests for behavior you change. Rust changes should + keep `cargo test -p agent -p git-ops` green; frontend changes run under + `npm run test` (vitest) in `apps/desktop`. +- **Don't touch `v1/`.** It's frozen history. + +## Pull requests + +1. Fork and branch from `main` (or the active development branch). +2. Make your change, with tests, and ensure the relevant build/test commands + above pass. +3. Open a PR and fill out the [pull request template](./.github/PULL_REQUEST_TEMPLATE.md): + a clear description, related issue, and how you tested it. +4. Keep the PR atomic and focused on a single change. + +A maintainer will review. Thanks for helping make SuperCoder better. diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 00000000..9537a3ac --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,6586 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "agent" +version = "0.1.0" +dependencies = [ + "async-trait", + "bytes", + "chrono", + "env_logger", + "futures", + "git-ops", + "globset", + "ignore", + "include_dir", + "log", + "percent-encoding", + "regex", + "reqwest 0.12.28", + "serde", + "serde_json", + "serde_yml", + "strsim", + "tempfile", + "thiserror 2.0.18", + "tokio", + "tokio-util", + "uuid", +] + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "assert-json-diff" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "async-signal" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "atk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" +dependencies = [ + "atk-sys", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +dependencies = [ + "serde_core", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + +[[package]] +name = "brotli" +version = "8.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8119e4516436f5708bbc474a9d395bf12f1b5395e93a92a56e647ac3388c8610" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5962523e1b92ce1b5e793d9169b9943eece10d39f62550bc04bb605d75b94924" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bstr" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +dependencies = [ + "serde", +] + +[[package]] +name = "cairo-rs" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" +dependencies = [ + "bitflags 2.11.1", + "cairo-sys-rs", + "glib", + "libc", + "once_cell", + "thiserror 1.0.69", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "camino" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.18", +] + +[[package]] +name = "cargo_toml" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77" +dependencies = [ + "serde", + "toml 0.9.12+spec-1.1.0", +] + +[[package]] +name = "cc" +version = "1.2.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfb" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" +dependencies = [ + "byteorder", + "fnv", + "uuid", +] + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link 0.2.1", +] + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "time", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" +dependencies = [ + "bitflags 2.11.1", + "core-foundation 0.10.1", + "core-graphics-types", + "foreign-types 0.5.0", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.11.1", + "core-foundation 0.10.1", + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "cssparser" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf", + "smallvec", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[package]] +name = "ctor" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "352d39c2f7bef1d6ad73db6f5160efcaed66d94ef8c6c573a8410c00bf909a98" +dependencies = [ + "ctor-proc-macro", + "dtor", +] + +[[package]] +name = "ctor-proc-macro" +version = "0.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.117", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "dbus" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b942602992bb7acfd1f51c49811c58a610ef9181b6e66f3e519d79b540a3bf73" +dependencies = [ + "libc", + "libdbus-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "deadpool" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0be2b1d1d6ec8d846f05e137292d0b89133caf95ef33695424c09568bdd39b1b" +dependencies = [ + "deadpool-runtime", + "lazy_static", + "num_cpus", + "tokio", +] + +[[package]] +name = "deadpool-runtime" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b" + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", + "serde_core", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.117", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys 0.4.1", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys 0.5.0", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users 0.4.6", + "windows-sys 0.48.0", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users 0.5.2", + "windows-sys 0.61.2", +] + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.11.1", + "block2", + "libc", + "objc2", +] + +[[package]] +name = "displaydoc" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "dlopen2" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlopen2_derive" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "dom_query" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89" +dependencies = [ + "bit-set", + "cssparser", + "foldhash 0.2.0", + "html5ever", + "precomputed-hash", + "selectors", + "tendril", +] + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +dependencies = [ + "serde", +] + +[[package]] +name = "dtoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "dtor" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1057d6c64987086ff8ed0fd3fbf377a6b7d205cc7715868cd401705f715cbe4" +dependencies = [ + "dtor-proc-macro", +] + +[[package]] +name = "dtor-proc-macro" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "embed-resource" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31a88c8d26de40ed18fe748c547845aa39de1db3afd958f8cb91579f3644bcb" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 1.1.2+spec-1.1.0", + "vswhom", + "winreg 0.55.0", +] + +[[package]] +name = "embed_plist" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "endi" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "env_filter" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e90c2accc4b07a8456ea0debdc2e7587bdd890680d71173a15d4ae604f6eef" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased-serde" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "filedescriptor" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d" +dependencies = [ + "libc", + "thiserror 1.0.69", + "winapi", +] + +[[package]] +name = "filetime" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" +dependencies = [ + "cfg-if", + "libc", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared 0.1.1", +] + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared 0.3.1", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "gdk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", + "once_cell", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkwayland-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" +dependencies = [ + "gdk-sys", + "glib-sys", + "gobject-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkx11" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe" +dependencies = [ + "gdk", + "gdkx11-sys", + "gio", + "glib", + "libc", + "x11", +] + +[[package]] +name = "gdkx11-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" +dependencies = [ + "gdk-sys", + "glib-sys", + "libc", + "system-deps", + "x11", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + +[[package]] +name = "gio" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "git-ops" +version = "0.1.0" +dependencies = [ + "log", + "reqwest 0.12.28", + "serde", + "serde_json", + "tempfile", + "thiserror 2.0.18", + "tokio", +] + +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags 2.11.1", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +dependencies = [ + "heck 0.4.1", + "proc-macro-crate 2.0.2", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "globset" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gtk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" +dependencies = [ + "atk", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk3-macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "h2" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap 2.14.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "hashlink" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +dependencies = [ + "hashbrown 0.14.5", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "html5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2" +dependencies = [ + "log", + "markup5ever", +] + +[[package]] +name = "http" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be7462df143984c4598a256ef469b251d7d7f9e271135073e78fc535414f3d0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ico" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" +dependencies = [ + "byteorder", + "png 0.17.16", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "ignore" +version = "0.4.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a" +dependencies = [ + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata", + "same-file", + "walkdir", + "winapi-util", +] + +[[package]] +name = "include_dir" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "923d117408f1e49d914f1a379a309cffe4f18c05cf4e3d12e613a15fc81bd0dd" +dependencies = [ + "include_dir_macros", +] + +[[package]] +name = "include_dir_macros" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "infer" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" +dependencies = [ + "cfb", +] + +[[package]] +name = "inotify" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff" +dependencies = [ + "bitflags 1.3.2", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "javascriptcore-rs" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" +dependencies = [ + "bitflags 1.3.2", + "glib", + "javascriptcore-rs-sys", +] + +[[package]] +name = "javascriptcore-rs-sys" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "jiff" +version = "0.2.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4603d3033e49e2b0e31229fcab20a5d40089c607d975cd9c80551dc69eed9102" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-static" +version = "0.2.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "782d32378dddf207193ac91cefb848ad41abb58195c95168e1291227a0832b47" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[package]] +name = "js-sys" +version = "0.3.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "json-patch" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08" +dependencies = [ + "jsonptr", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "jsonptr" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dea2b27dd239b2556ed7a25ba842fe47fd602e7fc7433c2a8d6106d4d9edd70" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "keyboard-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +dependencies = [ + "bitflags 2.11.1", + "serde", + "unicode-segmentation", +] + +[[package]] +name = "kqueue" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "273c0752728918e0ac4976f2b275b6fefb9ecd400585dec929419f3844cd87b5" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07293a4e297ac234359b510362495713f75ea345d5307140414f20c69ffeb087" +dependencies = [ + "bitflags 2.11.1", + "libc", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libappindicator" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" +dependencies = [ + "glib", + "gtk", + "gtk-sys", + "libappindicator-sys", + "log", +] + +[[package]] +name = "libappindicator-sys" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" +dependencies = [ + "gtk-sys", + "libloading", + "once_cell", +] + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libdbus-sys" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" +dependencies = [ + "pkg-config", +] + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "libredox" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3" +dependencies = [ + "libc", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c10584274047cb335c23d3e61bcef8e323adae7c5c8c760540f73610177fc3f" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "libyml" +version = "0.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3302702afa434ffa30847a83305f0a69d6abd74293b6554c18ec85c7ef30c980" +dependencies = [ + "anyhow", + "version_check", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5" + +[[package]] +name = "mac-notification-sys" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29a16783dd1a47849b8c8133c9cd3eb2112cfbc6901670af3dba47c8bbfb07d3" +dependencies = [ + "cc", + "objc2", + "objc2-foundation", + "time", +] + +[[package]] +name = "markup5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8983d30f2915feeaaab2d6babdd6bc7e9ed1a00b66b5e6d74df19aa9c0e91862" +dependencies = [ + "log", + "tendril", + "web_atoms", +] + +[[package]] +name = "memchr" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.48.0", +] + +[[package]] +name = "mio" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "muda" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47a2e3dff89cd322c66647942668faee0a2b1f88ea6cbb4d374b4a8d7e92528c" +dependencies = [ + "crossbeam-channel", + "dpi", + "gtk", + "keyboard-types", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.18", + "windows-sys 0.61.2", +] + +[[package]] +name = "native-tls" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.11.1", + "jni-sys 0.3.1", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nix" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" +dependencies = [ + "bitflags 2.11.1", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "notify" +version = "6.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" +dependencies = [ + "bitflags 2.11.1", + "crossbeam-channel", + "filetime", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "log", + "mio 0.8.11", + "walkdir", + "windows-sys 0.48.0", +] + +[[package]] +name = "notify-rust" +version = "4.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50ff2e74231b72c832d82982193b417f230945be6bdb5575b251d941d31adb00" +dependencies = [ + "futures-lite", + "log", + "mac-notification-sys", + "serde", + "tauri-winrt-notification", + "zbus", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", + "objc2-exception-helper", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.11.1", + "block2", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +dependencies = [ + "bitflags 2.11.1", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.11.1", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.11.1", + "dispatch2", + "objc2", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-location" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.11.1", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-exception-helper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" +dependencies = [ + "cc", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.11.1", + "block2", + "libc", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.11.1", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.11.1", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags 2.11.1", + "block2", + "objc2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-location", + "objc2-core-text", + "objc2-foundation", + "objc2-quartz-core", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-web-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" +dependencies = [ + "bitflags 2.11.1", + "block2", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "open" +version = "5.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fbaa89d2ddc8473c78a3adf69eea8cffa28c483b8e02a971ef31527cd0fc92c" +dependencies = [ + "dunce", + "is-wsl", + "libc", + "pathdiff", +] + +[[package]] +name = "openssl" +version = "0.10.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a45fa2aa886c42762255da344f0a0d313e254066c46aad76f300c3d3da62d967" +dependencies = [ + "bitflags 2.11.1", + "cfg-if", + "foreign-types 0.3.2", + "libc", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "openssl-sys" +version = "0.9.116" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28a22dc7140cda5f096e5e7724a6962ca81a7f8bfd2979f9b18c11af56318c4" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "os_pipe" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link 0.2.1", +] + +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros", + "phf_shared", + "serde", +] + +[[package]] +name = "phf_codegen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "piper" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "plist" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "092791278e026273c1b65bbdcfbba3a300f2994c896bd01ab01da613c29c46f1" +dependencies = [ + "base64 0.22.1", + "indexmap 2.14.0", + "quick-xml 0.39.4", + "serde", + "time", +] + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.11.1", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "portable-atomic-util" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "portable-pty" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4a596a2b3d2752d94f51fac2d4a96737b8705dddd311a32b9af47211f08671e" +dependencies = [ + "anyhow", + "bitflags 1.3.2", + "downcast-rs", + "filedescriptor", + "lazy_static", + "libc", + "log", + "nix", + "serde", + "serde_derive", + "serial2", + "shared_library", + "shell-words", + "winapi", + "winreg 0.10.1", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.117", +] + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +dependencies = [ + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.12+spec-1.1.0", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.37.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" +dependencies = [ + "memchr", +] + +[[package]] +name = "quick-xml" +version = "0.39.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.11.1", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 1.0.69", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 2.0.18", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64 0.22.1", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime", + "mime_guess", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams 0.4.2", + "web-sys", +] + +[[package]] +name = "reqwest" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams 0.5.0", + "web-sys", +] + +[[package]] +name = "rfd" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672" +dependencies = [ + "block2", + "dispatch2", + "glib-sys", + "gobject-sys", + "gtk-sys", + "js-sys", + "log", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.60.2", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rusqlite" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b838eba278d213a8beaf485bd313fd580ca4505a00d5871caeb1457c55322cae" +dependencies = [ + "bitflags 2.11.1", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.11.1", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "indexmap 1.9.3", + "schemars_derive", + "serde", + "serde_json", + "url", + "uuid", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.117", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.11.1", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "selectors" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" +dependencies = [ + "bitflags 2.11.1", + "cssparser", + "derive_more", + "log", + "new_debug_unreachable", + "phf", + "phf_codegen", + "precomputed-hash", + "rustc-hash", + "servo_arc", + "smallvec", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-untagged" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" +dependencies = [ + "erased-serde", + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e72c1c2cb7b223fafb600a619537a871c2818583d619401b785e7c0b746ccde2" +dependencies = [ + "base64 0.22.1", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.0", + "schemars 0.9.0", + "schemars 1.2.1", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b90c488738ecb4fb0262f41f43bc40efc5868d9fb744319ddf5f5317f417bfac" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_yml" +version = "0.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59e2dd588bf1597a252c3b920e0143eb99b0f76e4e082f4c92ce34fbc9e71ddd" +dependencies = [ + "indexmap 2.14.0", + "itoa", + "libyml", + "memchr", + "ryu", + "serde", + "version_check", +] + +[[package]] +name = "serial2" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9eb6ea5562eeaed6936b8b54e086aa0f88b9e5b1bef45beb038e2519fa1185b1" +dependencies = [ + "cfg-if", + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "serialize-to-javascript" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5" +dependencies = [ + "serde", + "serde_json", + "serialize-to-javascript-impl", +] + +[[package]] +name = "serialize-to-javascript-impl" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "servo_arc" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shared_child" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e362d9935bc50f019969e2f9ecd66786612daae13e8f277be7bfb66e8bed3f7" +dependencies = [ + "libc", + "sigchld", + "windows-sys 0.60.2", +] + +[[package]] +name = "shared_library" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a9e7e0f2bfae24d8a5b5a66c5b257a83c7412304311512a0c054cd5e619da11" +dependencies = [ + "lazy_static", + "libc", +] + +[[package]] +name = "shell-words" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77" + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "sigchld" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47106eded3c154e70176fc83df9737335c94ce22f821c32d17ed1db1f83badb1" +dependencies = [ + "libc", + "os_pipe", + "signal-hook", +] + +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "softbuffer" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" +dependencies = [ + "bytemuck", + "js-sys", + "ndk", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "objc2-quartz-core", + "raw-window-handle", + "redox_syscall", + "tracing", + "wasm-bindgen", + "web-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "soup3" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" +dependencies = [ + "futures-channel", + "gio", + "glib", + "libc", + "soup3-sys", +] + +[[package]] +name = "soup3-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "string_cache" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared", + "precomputed-hash", +] + +[[package]] +name = "string_cache_codegen" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "supercoder-agent-desktop" +version = "0.1.0" +dependencies = [ + "agent", + "async-trait", + "base64 0.22.1", + "chrono", + "dirs 5.0.1", + "dotenvy", + "env_logger", + "flate2", + "git-ops", + "ignore", + "log", + "notify", + "parking_lot", + "reqwest 0.12.28", + "rusqlite", + "serde", + "serde_json", + "sha2", + "tauri", + "tauri-build", + "tauri-plugin-dialog", + "tauri-plugin-notification", + "tauri-plugin-pty", + "tauri-plugin-shell", + "tauri-plugin-single-instance", + "tempfile", + "thiserror 2.0.18", + "tokio", + "tokio-util", + "url", + "uuid", + "wiremock", +] + +[[package]] +name = "swift-rs" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4057c98e2e852d51fdcfca832aac7b571f6b351ad159f9eda5db1655f8d0c4d7" +dependencies = [ + "base64 0.21.7", + "serde", + "serde_json", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags 2.11.1", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml 0.8.2", + "version-compare", +] + +[[package]] +name = "tao" +version = "0.35.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1c93047acf68669466a34690ac58cca7010bd1b201e1ec86f1fd0a75d3dd4a9" +dependencies = [ + "bitflags 2.11.1", + "block2", + "core-foundation 0.10.1", + "core-graphics", + "crossbeam-channel", + "dbus", + "dispatch2", + "dlopen2", + "dpi", + "gdkwayland-sys", + "gdkx11-sys", + "gtk", + "jni", + "libc", + "log", + "ndk", + "ndk-sys", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "once_cell", + "parking_lot", + "percent-encoding", + "raw-window-handle", + "tao-macros", + "unicode-segmentation", + "url", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "tao-macros" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "tauri" +version = "2.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "437404997acf375d85f1177afa7e11bb971f274ed6a7b83a2a3e339015f4cc28" +dependencies = [ + "anyhow", + "bytes", + "cookie", + "dirs 6.0.0", + "dunce", + "embed_plist", + "getrandom 0.3.4", + "glob", + "gtk", + "heck 0.5.0", + "http", + "jni", + "libc", + "log", + "mime", + "muda", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "percent-encoding", + "plist", + "raw-window-handle", + "reqwest 0.13.4", + "serde", + "serde_json", + "serde_repr", + "serialize-to-javascript", + "swift-rs", + "tauri-build", + "tauri-macros", + "tauri-runtime", + "tauri-runtime-wry", + "tauri-utils", + "thiserror 2.0.18", + "tokio", + "tray-icon", + "url", + "webkit2gtk", + "webview2-com", + "window-vibrancy", + "windows", +] + +[[package]] +name = "tauri-build" +version = "2.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aa1f9055fc23919a54e4e125052bed16ed04aef0487086e758fe01a67b451c7" +dependencies = [ + "anyhow", + "cargo_toml", + "dirs 6.0.0", + "glob", + "heck 0.5.0", + "json-patch", + "schemars 0.8.22", + "semver", + "serde", + "serde_json", + "tauri-utils", + "tauri-winres", + "walkdir", +] + +[[package]] +name = "tauri-codegen" +version = "2.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4a0319528a025a38c4078e7dae2c446f4e63620ddb0659a643ede1cb38f90e9" +dependencies = [ + "base64 0.22.1", + "brotli", + "ico", + "json-patch", + "plist", + "png 0.17.16", + "proc-macro2", + "quote", + "semver", + "serde", + "serde_json", + "sha2", + "syn 2.0.117", + "tauri-utils", + "thiserror 2.0.18", + "time", + "url", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-macros" +version = "2.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae6cb4e3896c21d2f6da5b31251d2faea0153bba56ed0e970f918115dbee4924" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", + "tauri-codegen", + "tauri-utils", +] + +[[package]] +name = "tauri-plugin" +version = "2.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e126abc9e84e35cdfd01596140a73a1850cdb0df0a23acf0185776c30b469a6e" +dependencies = [ + "anyhow", + "glob", + "plist", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri-utils", + "walkdir", +] + +[[package]] +name = "tauri-plugin-dialog" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65981abb771e74e571a38196c3baa11c459379164791eba0e67abc1a5fac9884" +dependencies = [ + "log", + "raw-window-handle", + "rfd", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "tauri-plugin-fs", + "thiserror 2.0.18", + "url", +] + +[[package]] +name = "tauri-plugin-fs" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7ecc274121aca0c036a2b42d1cbe83d368d348f54e0bb8a735c2b1548e8f371" +dependencies = [ + "anyhow", + "dunce", + "glob", + "log", + "objc2-foundation", + "percent-encoding", + "schemars 0.8.22", + "serde", + "serde_json", + "serde_repr", + "tauri", + "tauri-plugin", + "tauri-utils", + "thiserror 2.0.18", + "toml 1.1.2+spec-1.1.0", + "url", +] + +[[package]] +name = "tauri-plugin-notification" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01fc2c5ff41105bd1f7242d8201fdf3efd70749b82fa013a17f2126357d194cc" +dependencies = [ + "log", + "notify-rust", + "rand", + "serde", + "serde_json", + "serde_repr", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", + "time", + "url", +] + +[[package]] +name = "tauri-plugin-pty" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5da8233d3c7b3581ff7da274d32b9145396da9d68bd16cedbbcb9a6908438be6" +dependencies = [ + "portable-pty", + "serde", + "serde_json", + "tauri", + "tauri-plugin", +] + +[[package]] +name = "tauri-plugin-shell" +version = "2.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8457dbf9e2bab1edd8df22bb2c20857a59a9868e79cb3eac5ed639eec4d0c73b" +dependencies = [ + "encoding_rs", + "log", + "open", + "os_pipe", + "regex", + "schemars 0.8.22", + "serde", + "serde_json", + "shared_child", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", + "tokio", +] + +[[package]] +name = "tauri-plugin-single-instance" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c8f29386f5e9fdc699182388a33ee80a56de436d91b67459e86afef426282af" +dependencies = [ + "serde", + "serde_json", + "tauri", + "thiserror 2.0.18", + "tracing", + "windows-sys 0.60.2", + "zbus", +] + +[[package]] +name = "tauri-runtime" +version = "2.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48222d7116c8807eaa6fe2f372e023fae125084e61e6eca6d70b7961cdf129ef" +dependencies = [ + "cookie", + "dpi", + "gtk", + "http", + "jni", + "objc2", + "objc2-ui-kit", + "objc2-web-kit", + "raw-window-handle", + "serde", + "serde_json", + "tauri-utils", + "thiserror 2.0.18", + "url", + "webkit2gtk", + "webview2-com", + "windows", +] + +[[package]] +name = "tauri-runtime-wry" +version = "2.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b83849ee63ecb27a8e8d0fe51915ca215076914aca43f96db1179f0f415f6cd9" +dependencies = [ + "gtk", + "http", + "jni", + "log", + "objc2", + "objc2-app-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "softbuffer", + "tao", + "tauri-runtime", + "tauri-utils", + "url", + "webkit2gtk", + "webview2-com", + "windows", + "wry", +] + +[[package]] +name = "tauri-utils" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "092379df9a707631978e6c56b1bc2401d387f01e2d4a3c123360d167bbb9aa95" +dependencies = [ + "anyhow", + "brotli", + "cargo_metadata", + "ctor", + "dom_query", + "dunce", + "glob", + "http", + "infer", + "json-patch", + "log", + "memchr", + "phf", + "plist", + "proc-macro2", + "quote", + "regex", + "schemars 0.8.22", + "semver", + "serde", + "serde-untagged", + "serde_json", + "serde_with", + "swift-rs", + "thiserror 2.0.18", + "toml 1.1.2+spec-1.1.0", + "url", + "urlpattern", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-winres" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc65d45c68858bfe420dd29e834b5d15dbecf8a07a8a16cf4d532c7b1f69d4b6" +dependencies = [ + "dunce", + "embed-resource", + "toml 1.1.2+spec-1.1.0", +] + +[[package]] +name = "tauri-winrt-notification" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b1e66e07de489fe43a46678dd0b8df65e0c973909df1b60ba33874e297ba9b9" +dependencies = [ + "quick-xml 0.37.5", + "thiserror 2.0.18", + "windows", + "windows-version", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "tendril" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4790fc369d5a530f4b544b094e31388b9b3a37c0f4652ade4505945f5660d24" +dependencies = [ + "new_debug_unreachable", + "utf-8", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio 1.2.1", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + +[[package]] +name = "toml" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.3", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap 2.14.0", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.25.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.3", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.3", +] + +[[package]] +name = "toml_writer" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags 2.11.1", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tray-icon" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15edbb0d80583e85ee8df283410038e17314df5cba30da2087a54a85216c0773" +dependencies = [ + "crossbeam-channel", + "dirs 6.0.0", + "libappindicator", + "muda", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.18", + "windows-sys 0.61.2", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "uds_windows" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" +dependencies = [ + "memoffset", + "tempfile", + "windows-sys 0.61.2", +] + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-ucd-ident" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "urlpattern" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d" +dependencies = [ + "regex", + "serde", + "unic-ucd-ident", + "url", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d258b83ceec21034727ecee8c382cfa6c3e133699b0742c64571814fb420c9f7" +dependencies = [ + "getrandom 0.4.2", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9473dbd2991ae90b6291c3c32c30c6187ac49aa32f9905d1cce280ec1e110b0f" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.117", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap 2.14.0", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.1", + "hashbrown 0.15.5", + "indexmap 2.14.0", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621441cfc37b84979402712047321980c178f299193a3589d05b99e8763436" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web_atoms" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7cff6eef815df1834fd250e3a2ff436044d82a9f1bc1980ca1dbdf07effc538" +dependencies = [ + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", +] + +[[package]] +name = "webkit2gtk" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk", + "gdk-sys", + "gio", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "gtk", + "gtk-sys", + "javascriptcore-rs", + "libc", + "once_cell", + "soup3", + "webkit2gtk-sys", +] + +[[package]] +name = "webkit2gtk-sys" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5" +dependencies = [ + "bitflags 1.3.2", + "cairo-sys-rs", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk-sys", + "javascriptcore-rs-sys", + "libc", + "pkg-config", + "soup3-sys", + "system-deps", +] + +[[package]] +name = "webview2-com" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows", + "windows-core 0.61.2", + "windows-implement", + "windows-interface", +] + +[[package]] +name = "webview2-com-macros" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "webview2-com-sys" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" +dependencies = [ + "thiserror 2.0.18", + "windows", + "windows-core 0.61.2", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "window-vibrancy" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" +dependencies = [ + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "windows-sys 0.59.0", + "windows-version", +] + +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections", + "windows-core 0.61.2", + "windows-future", + "windows-link 0.1.3", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-version" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" + +[[package]] +name = "winnow" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +dependencies = [ + "winapi", +] + +[[package]] +name = "winreg" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" +dependencies = [ + "cfg-if", + "windows-sys 0.59.0", +] + +[[package]] +name = "wiremock" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08db1edfb05d9b3c1542e521aea074442088292f00b5f28e435c714a98f85031" +dependencies = [ + "assert-json-diff", + "base64 0.22.1", + "deadpool", + "futures", + "http", + "http-body-util", + "hyper", + "hyper-util", + "log", + "once_cell", + "regex", + "serde", + "serde_json", + "tokio", + "url", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck 0.5.0", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck 0.5.0", + "indexmap 2.14.0", + "prettyplease", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.1", + "indexmap 2.14.0", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 2.14.0", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "wry" +version = "0.55.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186f9871daa55fd9c016578b810d149de58367113db7fb72b462d2323ce19514" +dependencies = [ + "base64 0.22.1", + "block2", + "cookie", + "crossbeam-channel", + "dirs 6.0.0", + "dom_query", + "dpi", + "dunce", + "gdkx11", + "gtk", + "http", + "javascriptcore-rs", + "jni", + "libc", + "ndk", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "sha2", + "soup3", + "tao-macros", + "thiserror 2.0.18", + "url", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "yoke" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zbus" +version = "5.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eee682d202a77e4a9f3b2c2bdf48a7b28af5c08c34ddf66f98c93e5e39464285" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "libc", + "ordered-stream", + "rustix", + "serde", + "serde_repr", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.2", + "winnow 1.0.3", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adf1bd45a81a103745b1757754762a26e8cd01e4532e4d6c8ec431624b80d1d6" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7074f3e50b894eac91750142016d30d0a89be8e67dbfd9704fb875825760e52d" +dependencies = [ + "serde", + "winnow 1.0.3", + "zvariant", +] + +[[package]] +name = "zerocopy" +version = "0.8.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b065d4f0e55f82fae73202e189638116a87c55ab6b8e6c2721e13dd9d854ad1" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b631b19d36a892ab55420c92dbc83ccd79274f25be714855d3074aa71cab639" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zvariant" +version = "5.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a192a0bde63360d77a7523c833d4b4ce6070a927e2c53246e4c540b1a3e27be0" +dependencies = [ + "endi", + "enumflags2", + "serde", + "winnow 1.0.3", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bc6cde9c01c511074be97f7ccb6c19d0da89e3f8662e812e999dcfd4638737" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e8535915cfa75547e559d8c68e8139909a4aeee076831e4ef7fc59d8172c4d6" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 2.0.117", + "winnow 1.0.3", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 00000000..5f1d6286 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,15 @@ +[workspace] +resolver = "2" +members = ["crates/agent", "crates/git-ops", "apps/desktop/src-tauri"] + +[profile.dev] +debug = 0 +incremental = true +split-debuginfo = "off" + +[profile.release] +panic = "abort" +codegen-units = 1 +lto = true +opt-level = "s" +strip = true diff --git a/MODULE.bazel b/MODULE.bazel new file mode 100644 index 00000000..f014588b --- /dev/null +++ b/MODULE.bazel @@ -0,0 +1,18 @@ +"""SuperCoder — Bazel module (Go side). Rust crates build with native cargo.""" + +module( + name = "supercoder", + version = "0.1.0", +) + +bazel_dep(name = "rules_go", version = "0.59.0", repo_name = "io_bazel_rules_go") +bazel_dep(name = "gazelle", version = "0.47.0", repo_name = "bazel_gazelle") + +go_sdk = use_extension("@io_bazel_rules_go//go:extensions.bzl", "go_sdk") +go_sdk.download(version = "1.25.6") + +go_deps = use_extension("@bazel_gazelle//:extensions.bzl", "go_deps") +go_deps.from_file(go_mod = "//:go.mod") +use_repo(go_deps, "com_github_anthropics_anthropic_sdk_go", "com_github_aws_aws_sdk_go_v2", "com_github_aws_aws_sdk_go_v2_config", "com_github_aws_aws_sdk_go_v2_credentials", "com_github_aws_aws_sdk_go_v2_service_s3", "com_github_aws_smithy_go", "com_github_falkordb_falkordb_go_v2", "com_github_gin_contrib_cors", "com_github_gin_contrib_zap", "com_github_gin_gonic_gin", "com_github_go_gorm_caches_v4", "com_github_google_uuid", "com_github_hibiken_asynq", "com_github_hibiken_asynqmon", "com_github_knadh_koanf_parsers_dotenv", "com_github_knadh_koanf_providers_confmap", "com_github_knadh_koanf_providers_env", "com_github_knadh_koanf_providers_file", "com_github_knadh_koanf_v2", "com_github_lib_pq", "com_github_openai_openai_go", "com_github_pkoukk_tiktoken_go", "com_github_pkoukk_tiktoken_go_loader", "com_github_qdrant_go_client", "com_github_redis_go_redis_v9", "com_github_smacker_go_tree_sitter", "com_github_uptrace_opentelemetry_go_extra_otelgorm", "dev_resty_v3", "io_gorm_datatypes", "io_gorm_driver_postgres", "io_gorm_gorm", "io_opentelemetry_go_otel", "io_opentelemetry_go_otel_trace", "org_golang_google_grpc", "org_golang_x_sync", "org_golang_x_sys", "org_uber_go_dig", "org_uber_go_zap") + +# use_repo(go_deps, ...) entries are managed by `bazel mod tidy`. diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock new file mode 100644 index 00000000..1af6a74c --- /dev/null +++ b/MODULE.bazel.lock @@ -0,0 +1,557 @@ +{ + "lockFileVersion": 24, + "registryFileHashes": { + "https://bcr.bazel.build/bazel_registry.json": "8a28e4aff06ee60aed2a8c281907fb8bcbf3b753c91fb5a5c57da3215d5b3497", + "https://bcr.bazel.build/modules/abseil-cpp/20210324.2/MODULE.bazel": "7cd0312e064fde87c8d1cd79ba06c876bd23630c83466e9500321be55c96ace2", + "https://bcr.bazel.build/modules/abseil-cpp/20211102.0/MODULE.bazel": "70390338f7a5106231d20620712f7cccb659cd0e9d073d1991c038eb9fc57589", + "https://bcr.bazel.build/modules/abseil-cpp/20230125.1/MODULE.bazel": "89047429cb0207707b2dface14ba7f8df85273d484c2572755be4bab7ce9c3a0", + "https://bcr.bazel.build/modules/abseil-cpp/20230802.0.bcr.1/MODULE.bazel": "1c8cec495288dccd14fdae6e3f95f772c1c91857047a098fad772034264cc8cb", + "https://bcr.bazel.build/modules/abseil-cpp/20230802.0/MODULE.bazel": "d253ae36a8bd9ee3c5955384096ccb6baf16a1b1e93e858370da0a3b94f77c16", + "https://bcr.bazel.build/modules/abseil-cpp/20230802.1/MODULE.bazel": "fa92e2eb41a04df73cdabeec37107316f7e5272650f81d6cc096418fe647b915", + "https://bcr.bazel.build/modules/abseil-cpp/20240116.1/MODULE.bazel": "37bcdb4440fbb61df6a1c296ae01b327f19e9bb521f9b8e26ec854b6f97309ed", + "https://bcr.bazel.build/modules/abseil-cpp/20240116.1/source.json": "9be551b8d4e3ef76875c0d744b5d6a504a27e3ae67bc6b28f46415fd2d2957da", + "https://bcr.bazel.build/modules/bazel_features/1.1.0/MODULE.bazel": "cfd42ff3b815a5f39554d97182657f8c4b9719568eb7fded2b9135f084bf760b", + "https://bcr.bazel.build/modules/bazel_features/1.1.1/MODULE.bazel": "27b8c79ef57efe08efccbd9dd6ef70d61b4798320b8d3c134fd571f78963dbcd", + "https://bcr.bazel.build/modules/bazel_features/1.11.0/MODULE.bazel": "f9382337dd5a474c3b7d334c2f83e50b6eaedc284253334cf823044a26de03e8", + "https://bcr.bazel.build/modules/bazel_features/1.15.0/MODULE.bazel": "d38ff6e517149dc509406aca0db3ad1efdd890a85e049585b7234d04238e2a4d", + "https://bcr.bazel.build/modules/bazel_features/1.17.0/MODULE.bazel": "039de32d21b816b47bd42c778e0454217e9c9caac4a3cf8e15c7231ee3ddee4d", + "https://bcr.bazel.build/modules/bazel_features/1.18.0/MODULE.bazel": "1be0ae2557ab3a72a57aeb31b29be347bcdc5d2b1eb1e70f39e3851a7e97041a", + "https://bcr.bazel.build/modules/bazel_features/1.19.0/MODULE.bazel": "59adcdf28230d220f0067b1f435b8537dd033bfff8db21335ef9217919c7fb58", + "https://bcr.bazel.build/modules/bazel_features/1.28.0/MODULE.bazel": "4b4200e6cbf8fa335b2c3f43e1d6ef3e240319c33d43d60cc0fbd4b87ece299d", + "https://bcr.bazel.build/modules/bazel_features/1.30.0/MODULE.bazel": "a14b62d05969a293b80257e72e597c2da7f717e1e69fa8b339703ed6731bec87", + "https://bcr.bazel.build/modules/bazel_features/1.30.0/source.json": "b07e17f067fe4f69f90b03b36ef1e08fe0d1f3cac254c1241a1818773e3423bc", + "https://bcr.bazel.build/modules/bazel_features/1.4.1/MODULE.bazel": "e45b6bb2350aff3e442ae1111c555e27eac1d915e77775f6fdc4b351b758b5d7", + "https://bcr.bazel.build/modules/bazel_features/1.9.1/MODULE.bazel": "8f679097876a9b609ad1f60249c49d68bfab783dd9be012faf9d82547b14815a", + "https://bcr.bazel.build/modules/bazel_skylib/1.0.3/MODULE.bazel": "bcb0fd896384802d1ad283b4e4eb4d718eebd8cb820b0a2c3a347fb971afd9d8", + "https://bcr.bazel.build/modules/bazel_skylib/1.1.1/MODULE.bazel": "1add3e7d93ff2e6998f9e118022c84d163917d912f5afafb3058e3d2f1545b5e", + "https://bcr.bazel.build/modules/bazel_skylib/1.2.0/MODULE.bazel": "44fe84260e454ed94ad326352a698422dbe372b21a1ac9f3eab76eb531223686", + "https://bcr.bazel.build/modules/bazel_skylib/1.2.1/MODULE.bazel": "f35baf9da0efe45fa3da1696ae906eea3d615ad41e2e3def4aeb4e8bc0ef9a7a", + "https://bcr.bazel.build/modules/bazel_skylib/1.3.0/MODULE.bazel": "20228b92868bf5cfc41bda7afc8a8ba2a543201851de39d990ec957b513579c5", + "https://bcr.bazel.build/modules/bazel_skylib/1.4.1/MODULE.bazel": "a0dcb779424be33100dcae821e9e27e4f2901d9dfd5333efe5ac6a8d7ab75e1d", + "https://bcr.bazel.build/modules/bazel_skylib/1.4.2/MODULE.bazel": "3bd40978e7a1fac911d5989e6b09d8f64921865a45822d8b09e815eaa726a651", + "https://bcr.bazel.build/modules/bazel_skylib/1.5.0/MODULE.bazel": "32880f5e2945ce6a03d1fbd588e9198c0a959bb42297b2cfaf1685b7bc32e138", + "https://bcr.bazel.build/modules/bazel_skylib/1.6.1/MODULE.bazel": "8fdee2dbaace6c252131c00e1de4b165dc65af02ea278476187765e1a617b917", + "https://bcr.bazel.build/modules/bazel_skylib/1.7.0/MODULE.bazel": "0db596f4563de7938de764cc8deeabec291f55e8ec15299718b93c4423e9796d", + "https://bcr.bazel.build/modules/bazel_skylib/1.7.1/MODULE.bazel": "3120d80c5861aa616222ec015332e5f8d3171e062e3e804a2a0253e1be26e59b", + "https://bcr.bazel.build/modules/bazel_skylib/1.7.1/source.json": "f121b43eeefc7c29efbd51b83d08631e2347297c95aac9764a701f2a6a2bb953", + "https://bcr.bazel.build/modules/buildozer/7.1.2/MODULE.bazel": "2e8dd40ede9c454042645fd8d8d0cd1527966aa5c919de86661e62953cd73d84", + "https://bcr.bazel.build/modules/buildozer/7.1.2/source.json": "c9028a501d2db85793a6996205c8de120944f50a0d570438fcae0457a5f9d1f8", + "https://bcr.bazel.build/modules/gazelle/0.32.0/MODULE.bazel": "b499f58a5d0d3537f3cf5b76d8ada18242f64ec474d8391247438bf04f58c7b8", + "https://bcr.bazel.build/modules/gazelle/0.33.0/MODULE.bazel": "a13a0f279b462b784fb8dd52a4074526c4a2afe70e114c7d09066097a46b3350", + "https://bcr.bazel.build/modules/gazelle/0.34.0/MODULE.bazel": "abdd8ce4d70978933209db92e436deb3a8b737859e9354fb5fd11fb5c2004c8a", + "https://bcr.bazel.build/modules/gazelle/0.36.0/MODULE.bazel": "e375d5d6e9a6ca59b0cb38b0540bc9a05b6aa926d322f2de268ad267a2ee74c0", + "https://bcr.bazel.build/modules/gazelle/0.47.0/MODULE.bazel": "b61bb007c4efad134aa30ee7f4a8e2a39b22aa5685f005edaa022fbd1de43ebc", + "https://bcr.bazel.build/modules/gazelle/0.47.0/source.json": "aeb2e5df14b7fb298625d75d08b9c65bdb0b56014c5eb89da9e5dd0572280ae6", + "https://bcr.bazel.build/modules/google_benchmark/1.8.2/MODULE.bazel": "a70cf1bba851000ba93b58ae2f6d76490a9feb74192e57ab8e8ff13c34ec50cb", + "https://bcr.bazel.build/modules/googletest/1.11.0/MODULE.bazel": "3a83f095183f66345ca86aa13c58b59f9f94a2f81999c093d4eeaa2d262d12f4", + "https://bcr.bazel.build/modules/googletest/1.14.0.bcr.1/MODULE.bazel": "22c31a561553727960057361aa33bf20fb2e98584bc4fec007906e27053f80c6", + "https://bcr.bazel.build/modules/googletest/1.14.0.bcr.1/source.json": "41e9e129f80d8c8bf103a7acc337b76e54fad1214ac0a7084bf24f4cd924b8b4", + "https://bcr.bazel.build/modules/googletest/1.14.0/MODULE.bazel": "cfbcbf3e6eac06ef9d85900f64424708cc08687d1b527f0ef65aa7517af8118f", + "https://bcr.bazel.build/modules/jsoncpp/1.9.5/MODULE.bazel": "31271aedc59e815656f5736f282bb7509a97c7ecb43e927ac1a37966e0578075", + "https://bcr.bazel.build/modules/jsoncpp/1.9.5/source.json": "4108ee5085dd2885a341c7fab149429db457b3169b86eb081fa245eadf69169d", + "https://bcr.bazel.build/modules/libpfm/4.11.0/MODULE.bazel": "45061ff025b301940f1e30d2c16bea596c25b176c8b6b3087e92615adbd52902", + "https://bcr.bazel.build/modules/package_metadata/0.0.5/MODULE.bazel": "ef4f9439e3270fdd6b9fd4dbc3d2f29d13888e44c529a1b243f7a31dfbc2e8e4", + "https://bcr.bazel.build/modules/package_metadata/0.0.5/source.json": "2326db2f6592578177751c3e1f74786b79382cd6008834c9d01ec865b9126a85", + "https://bcr.bazel.build/modules/platforms/0.0.10/MODULE.bazel": "8cb8efaf200bdeb2150d93e162c40f388529a25852b332cec879373771e48ed5", + "https://bcr.bazel.build/modules/platforms/0.0.11/MODULE.bazel": "0daefc49732e227caa8bfa834d65dc52e8cc18a2faf80df25e8caea151a9413f", + "https://bcr.bazel.build/modules/platforms/0.0.4/MODULE.bazel": "9b328e31ee156f53f3c416a64f8491f7eb731742655a47c9eec4703a71644aee", + "https://bcr.bazel.build/modules/platforms/0.0.5/MODULE.bazel": "5733b54ea419d5eaf7997054bb55f6a1d0b5ff8aedf0176fef9eea44f3acda37", + "https://bcr.bazel.build/modules/platforms/0.0.6/MODULE.bazel": "ad6eeef431dc52aefd2d77ed20a4b353f8ebf0f4ecdd26a807d2da5aa8cd0615", + "https://bcr.bazel.build/modules/platforms/0.0.7/MODULE.bazel": "72fd4a0ede9ee5c021f6a8dd92b503e089f46c227ba2813ff183b71616034814", + "https://bcr.bazel.build/modules/platforms/0.0.8/MODULE.bazel": "9f142c03e348f6d263719f5074b21ef3adf0b139ee4c5133e2aa35664da9eb2d", + "https://bcr.bazel.build/modules/platforms/1.0.0/MODULE.bazel": "f05feb42b48f1b3c225e4ccf351f367be0371411a803198ec34a389fb22aa580", + "https://bcr.bazel.build/modules/platforms/1.0.0/source.json": "f4ff1fd412e0246fd38c82328eb209130ead81d62dcd5a9e40910f867f733d96", + "https://bcr.bazel.build/modules/protobuf/21.7/MODULE.bazel": "a5a29bb89544f9b97edce05642fac225a808b5b7be74038ea3640fae2f8e66a7", + "https://bcr.bazel.build/modules/protobuf/27.0/MODULE.bazel": "7873b60be88844a0a1d8f80b9d5d20cfbd8495a689b8763e76c6372998d3f64c", + "https://bcr.bazel.build/modules/protobuf/27.1/MODULE.bazel": "703a7b614728bb06647f965264967a8ef1c39e09e8f167b3ca0bb1fd80449c0d", + "https://bcr.bazel.build/modules/protobuf/29.0-rc2.bcr.1/MODULE.bazel": "52f4126f63a2f0bbf36b99c2a87648f08467a4eaf92ba726bc7d6a500bbf770c", + "https://bcr.bazel.build/modules/protobuf/29.0-rc2/MODULE.bazel": "6241d35983510143049943fc0d57937937122baf1b287862f9dc8590fc4c37df", + "https://bcr.bazel.build/modules/protobuf/29.0/MODULE.bazel": "319dc8bf4c679ff87e71b1ccfb5a6e90a6dbc4693501d471f48662ac46d04e4e", + "https://bcr.bazel.build/modules/protobuf/29.0/source.json": "b857f93c796750eef95f0d61ee378f3420d00ee1dd38627b27193aa482f4f981", + "https://bcr.bazel.build/modules/protobuf/3.19.0/MODULE.bazel": "6b5fbb433f760a99a22b18b6850ed5784ef0e9928a72668b66e4d7ccd47db9b0", + "https://bcr.bazel.build/modules/protobuf/3.19.2/MODULE.bazel": "532ffe5f2186b69fdde039efe6df13ba726ff338c6bc82275ad433013fa10573", + "https://bcr.bazel.build/modules/protobuf/3.19.6/MODULE.bazel": "9233edc5e1f2ee276a60de3eaa47ac4132302ef9643238f23128fea53ea12858", + "https://bcr.bazel.build/modules/pybind11_bazel/2.11.1/MODULE.bazel": "88af1c246226d87e65be78ed49ecd1e6f5e98648558c14ce99176da041dc378e", + "https://bcr.bazel.build/modules/pybind11_bazel/2.11.1/source.json": "be4789e951dd5301282729fe3d4938995dc4c1a81c2ff150afc9f1b0504c6022", + "https://bcr.bazel.build/modules/re2/2023-09-01/MODULE.bazel": "cb3d511531b16cfc78a225a9e2136007a48cf8a677e4264baeab57fe78a80206", + "https://bcr.bazel.build/modules/re2/2023-09-01/source.json": "e044ce89c2883cd957a2969a43e79f7752f9656f6b20050b62f90ede21ec6eb4", + "https://bcr.bazel.build/modules/rules_android/0.1.1/MODULE.bazel": "48809ab0091b07ad0182defb787c4c5328bd3a278938415c00a7b69b50c4d3a8", + "https://bcr.bazel.build/modules/rules_android/0.1.1/source.json": "e6986b41626ee10bdc864937ffb6d6bf275bb5b9c65120e6137d56e6331f089e", + "https://bcr.bazel.build/modules/rules_cc/0.0.1/MODULE.bazel": "cb2aa0747f84c6c3a78dad4e2049c154f08ab9d166b1273835a8174940365647", + "https://bcr.bazel.build/modules/rules_cc/0.0.10/MODULE.bazel": "ec1705118f7eaedd6e118508d3d26deba2a4e76476ada7e0e3965211be012002", + "https://bcr.bazel.build/modules/rules_cc/0.0.13/MODULE.bazel": "0e8529ed7b323dad0775ff924d2ae5af7640b23553dfcd4d34344c7e7a867191", + "https://bcr.bazel.build/modules/rules_cc/0.0.14/MODULE.bazel": "5e343a3aac88b8d7af3b1b6d2093b55c347b8eefc2e7d1442f7a02dc8fea48ac", + "https://bcr.bazel.build/modules/rules_cc/0.0.15/MODULE.bazel": "6704c35f7b4a72502ee81f61bf88706b54f06b3cbe5558ac17e2e14666cd5dcc", + "https://bcr.bazel.build/modules/rules_cc/0.0.16/MODULE.bazel": "7661303b8fc1b4d7f532e54e9d6565771fea666fbdf839e0a86affcd02defe87", + "https://bcr.bazel.build/modules/rules_cc/0.0.17/MODULE.bazel": "2ae1d8f4238ec67d7185d8861cb0a2cdf4bc608697c331b95bf990e69b62e64a", + "https://bcr.bazel.build/modules/rules_cc/0.0.2/MODULE.bazel": "6915987c90970493ab97393024c156ea8fb9f3bea953b2f3ec05c34f19b5695c", + "https://bcr.bazel.build/modules/rules_cc/0.0.6/MODULE.bazel": "abf360251023dfe3efcef65ab9d56beefa8394d4176dd29529750e1c57eaa33f", + "https://bcr.bazel.build/modules/rules_cc/0.0.8/MODULE.bazel": "964c85c82cfeb6f3855e6a07054fdb159aced38e99a5eecf7bce9d53990afa3e", + "https://bcr.bazel.build/modules/rules_cc/0.0.9/MODULE.bazel": "836e76439f354b89afe6a911a7adf59a6b2518fafb174483ad78a2a2fde7b1c5", + "https://bcr.bazel.build/modules/rules_cc/0.1.1/MODULE.bazel": "2f0222a6f229f0bf44cd711dc13c858dad98c62d52bd51d8fc3a764a83125513", + "https://bcr.bazel.build/modules/rules_cc/0.1.5/MODULE.bazel": "88dfc9361e8b5ae1008ac38f7cdfd45ad738e4fa676a3ad67d19204f045a1fd8", + "https://bcr.bazel.build/modules/rules_cc/0.1.5/source.json": "4bb4fed7f5499775d495739f785a5494a1f854645fa1bac5de131264f5acdf01", + "https://bcr.bazel.build/modules/rules_foreign_cc/0.9.0/MODULE.bazel": "c9e8c682bf75b0e7c704166d79b599f93b72cfca5ad7477df596947891feeef6", + "https://bcr.bazel.build/modules/rules_fuzzing/0.5.2/MODULE.bazel": "40c97d1144356f52905566c55811f13b299453a14ac7769dfba2ac38192337a8", + "https://bcr.bazel.build/modules/rules_fuzzing/0.5.2/source.json": "c8b1e2c717646f1702290959a3302a178fb639d987ab61d548105019f11e527e", + "https://bcr.bazel.build/modules/rules_go/0.41.0/MODULE.bazel": "55861d8e8bb0e62cbd2896f60ff303f62ffcb0eddb74ecb0e5c0cbe36fc292c8", + "https://bcr.bazel.build/modules/rules_go/0.42.0/MODULE.bazel": "8cfa875b9aa8c6fce2b2e5925e73c1388173ea3c32a0db4d2b4804b453c14270", + "https://bcr.bazel.build/modules/rules_go/0.46.0/MODULE.bazel": "3477df8bdcc49e698b9d25f734c4f3a9f5931ff34ee48a2c662be168f5f2d3fd", + "https://bcr.bazel.build/modules/rules_go/0.53.0/MODULE.bazel": "a4ed760d3ac0dbc0d7b967631a9a3fd9100d28f7d9fcf214b4df87d4bfff5f9a", + "https://bcr.bazel.build/modules/rules_go/0.59.0/MODULE.bazel": "b7e43e7414a3139a7547d1b4909b29085fbe5182b6c58cbe1ed4c6272815aeae", + "https://bcr.bazel.build/modules/rules_go/0.59.0/source.json": "1df17bb7865cfc029492c30163cee891d0dd8658ea0d5bfdf252c4b6db5c1ef6", + "https://bcr.bazel.build/modules/rules_java/4.0.0/MODULE.bazel": "5a78a7ae82cd1a33cef56dc578c7d2a46ed0dca12643ee45edbb8417899e6f74", + "https://bcr.bazel.build/modules/rules_java/5.3.5/MODULE.bazel": "a4ec4f2db570171e3e5eb753276ee4b389bae16b96207e9d3230895c99644b86", + "https://bcr.bazel.build/modules/rules_java/6.0.0/MODULE.bazel": "8a43b7df601a7ec1af61d79345c17b31ea1fedc6711fd4abfd013ea612978e39", + "https://bcr.bazel.build/modules/rules_java/6.4.0/MODULE.bazel": "e986a9fe25aeaa84ac17ca093ef13a4637f6107375f64667a15999f77db6c8f6", + "https://bcr.bazel.build/modules/rules_java/6.5.2/MODULE.bazel": "1d440d262d0e08453fa0c4d8f699ba81609ed0e9a9a0f02cd10b3e7942e61e31", + "https://bcr.bazel.build/modules/rules_java/7.10.0/MODULE.bazel": "530c3beb3067e870561739f1144329a21c851ff771cd752a49e06e3dc9c2e71a", + "https://bcr.bazel.build/modules/rules_java/7.12.2/MODULE.bazel": "579c505165ee757a4280ef83cda0150eea193eed3bef50b1004ba88b99da6de6", + "https://bcr.bazel.build/modules/rules_java/7.2.0/MODULE.bazel": "06c0334c9be61e6cef2c8c84a7800cef502063269a5af25ceb100b192453d4ab", + "https://bcr.bazel.build/modules/rules_java/7.3.2/MODULE.bazel": "50dece891cfdf1741ea230d001aa9c14398062f2b7c066470accace78e412bc2", + "https://bcr.bazel.build/modules/rules_java/7.6.1/MODULE.bazel": "2f14b7e8a1aa2f67ae92bc69d1ec0fa8d9f827c4e17ff5e5f02e91caa3b2d0fe", + "https://bcr.bazel.build/modules/rules_java/8.14.0/MODULE.bazel": "717717ed40cc69994596a45aec6ea78135ea434b8402fb91b009b9151dd65615", + "https://bcr.bazel.build/modules/rules_java/8.14.0/source.json": "8a88c4ca9e8759da53cddc88123880565c520503321e2566b4e33d0287a3d4bc", + "https://bcr.bazel.build/modules/rules_jvm_external/4.4.2/MODULE.bazel": "a56b85e418c83eb1839819f0b515c431010160383306d13ec21959ac412d2fe7", + "https://bcr.bazel.build/modules/rules_jvm_external/5.1/MODULE.bazel": "33f6f999e03183f7d088c9be518a63467dfd0be94a11d0055fe2d210f89aa909", + "https://bcr.bazel.build/modules/rules_jvm_external/5.2/MODULE.bazel": "d9351ba35217ad0de03816ef3ed63f89d411349353077348a45348b096615036", + "https://bcr.bazel.build/modules/rules_jvm_external/5.3/MODULE.bazel": "bf93870767689637164657731849fb887ad086739bd5d360d90007a581d5527d", + "https://bcr.bazel.build/modules/rules_jvm_external/6.1/MODULE.bazel": "75b5fec090dbd46cf9b7d8ea08cf84a0472d92ba3585b476f44c326eda8059c4", + "https://bcr.bazel.build/modules/rules_jvm_external/6.3/MODULE.bazel": "c998e060b85f71e00de5ec552019347c8bca255062c990ac02d051bb80a38df0", + "https://bcr.bazel.build/modules/rules_jvm_external/6.3/source.json": "6f5f5a5a4419ae4e37c35a5bb0a6ae657ed40b7abc5a5189111b47fcebe43197", + "https://bcr.bazel.build/modules/rules_kotlin/1.9.0/MODULE.bazel": "ef85697305025e5a61f395d4eaede272a5393cee479ace6686dba707de804d59", + "https://bcr.bazel.build/modules/rules_kotlin/1.9.6/MODULE.bazel": "d269a01a18ee74d0335450b10f62c9ed81f2321d7958a2934e44272fe82dcef3", + "https://bcr.bazel.build/modules/rules_kotlin/1.9.6/source.json": "2faa4794364282db7c06600b7e5e34867a564ae91bda7cae7c29c64e9466b7d5", + "https://bcr.bazel.build/modules/rules_license/0.0.3/MODULE.bazel": "627e9ab0247f7d1e05736b59dbb1b6871373de5ad31c3011880b4133cafd4bd0", + "https://bcr.bazel.build/modules/rules_license/0.0.7/MODULE.bazel": "088fbeb0b6a419005b89cf93fe62d9517c0a2b8bb56af3244af65ecfe37e7d5d", + "https://bcr.bazel.build/modules/rules_license/1.0.0/MODULE.bazel": "a7fda60eefdf3d8c827262ba499957e4df06f659330bbe6cdbdb975b768bb65c", + "https://bcr.bazel.build/modules/rules_license/1.0.0/source.json": "a52c89e54cc311196e478f8382df91c15f7a2bfdf4c6cd0e2675cc2ff0b56efb", + "https://bcr.bazel.build/modules/rules_pkg/0.7.0/MODULE.bazel": "df99f03fc7934a4737122518bb87e667e62d780b610910f0447665a7e2be62dc", + "https://bcr.bazel.build/modules/rules_pkg/1.0.1/MODULE.bazel": "5b1df97dbc29623bccdf2b0dcd0f5cb08e2f2c9050aab1092fd39a41e82686ff", + "https://bcr.bazel.build/modules/rules_pkg/1.0.1/source.json": "bd82e5d7b9ce2d31e380dd9f50c111d678c3bdaca190cb76b0e1c71b05e1ba8a", + "https://bcr.bazel.build/modules/rules_proto/4.0.0/MODULE.bazel": "a7a7b6ce9bee418c1a760b3d84f83a299ad6952f9903c67f19e4edd964894e06", + "https://bcr.bazel.build/modules/rules_proto/5.3.0-21.7/MODULE.bazel": "e8dff86b0971688790ae75528fe1813f71809b5afd57facb44dad9e8eca631b7", + "https://bcr.bazel.build/modules/rules_proto/6.0.0/MODULE.bazel": "b531d7f09f58dce456cd61b4579ce8c86b38544da75184eadaf0a7cb7966453f", + "https://bcr.bazel.build/modules/rules_proto/6.0.2/MODULE.bazel": "ce916b775a62b90b61888052a416ccdda405212b6aaeb39522f7dc53431a5e73", + "https://bcr.bazel.build/modules/rules_proto/7.0.2/MODULE.bazel": "bf81793bd6d2ad89a37a40693e56c61b0ee30f7a7fdbaf3eabbf5f39de47dea2", + "https://bcr.bazel.build/modules/rules_proto/7.0.2/source.json": "1e5e7260ae32ef4f2b52fd1d0de8d03b606a44c91b694d2f1afb1d3b28a48ce1", + "https://bcr.bazel.build/modules/rules_python/0.10.2/MODULE.bazel": "cc82bc96f2997baa545ab3ce73f196d040ffb8756fd2d66125a530031cd90e5f", + "https://bcr.bazel.build/modules/rules_python/0.23.1/MODULE.bazel": "49ffccf0511cb8414de28321f5fcf2a31312b47c40cc21577144b7447f2bf300", + "https://bcr.bazel.build/modules/rules_python/0.25.0/MODULE.bazel": "72f1506841c920a1afec76975b35312410eea3aa7b63267436bfb1dd91d2d382", + "https://bcr.bazel.build/modules/rules_python/0.28.0/MODULE.bazel": "cba2573d870babc976664a912539b320cbaa7114cd3e8f053c720171cde331ed", + "https://bcr.bazel.build/modules/rules_python/0.31.0/MODULE.bazel": "93a43dc47ee570e6ec9f5779b2e64c1476a6ce921c48cc9a1678a91dd5f8fd58", + "https://bcr.bazel.build/modules/rules_python/0.4.0/MODULE.bazel": "9208ee05fd48bf09ac60ed269791cf17fb343db56c8226a720fbb1cdf467166c", + "https://bcr.bazel.build/modules/rules_python/0.40.0/MODULE.bazel": "9d1a3cd88ed7d8e39583d9ffe56ae8a244f67783ae89b60caafc9f5cf318ada7", + "https://bcr.bazel.build/modules/rules_python/0.40.0/source.json": "939d4bd2e3110f27bfb360292986bb79fd8dcefb874358ccd6cdaa7bda029320", + "https://bcr.bazel.build/modules/rules_shell/0.2.0/MODULE.bazel": "fda8a652ab3c7d8fee214de05e7a9916d8b28082234e8d2c0094505c5268ed3c", + "https://bcr.bazel.build/modules/rules_shell/0.3.0/MODULE.bazel": "de4402cd12f4cc8fda2354fce179fdb068c0b9ca1ec2d2b17b3e21b24c1a937b", + "https://bcr.bazel.build/modules/rules_shell/0.3.0/source.json": "c55ed591aa5009401ddf80ded9762ac32c358d2517ee7820be981e2de9756cf3", + "https://bcr.bazel.build/modules/stardoc/0.5.1/MODULE.bazel": "1a05d92974d0c122f5ccf09291442580317cdd859f07a8655f1db9a60374f9f8", + "https://bcr.bazel.build/modules/stardoc/0.5.3/MODULE.bazel": "c7f6948dae6999bf0db32c1858ae345f112cacf98f174c7a8bb707e41b974f1c", + "https://bcr.bazel.build/modules/stardoc/0.5.6/MODULE.bazel": "c43dabc564990eeab55e25ed61c07a1aadafe9ece96a4efabb3f8bf9063b71ef", + "https://bcr.bazel.build/modules/stardoc/0.7.0/MODULE.bazel": "05e3d6d30c099b6770e97da986c53bd31844d7f13d41412480ea265ac9e8079c", + "https://bcr.bazel.build/modules/stardoc/0.7.1/MODULE.bazel": "3548faea4ee5dda5580f9af150e79d0f6aea934fc60c1cc50f4efdd9420759e7", + "https://bcr.bazel.build/modules/stardoc/0.7.1/source.json": "b6500ffcd7b48cd72c29bb67bcac781e12701cc0d6d55d266a652583cfcdab01", + "https://bcr.bazel.build/modules/upb/0.0.0-20220923-a547704/MODULE.bazel": "7298990c00040a0e2f121f6c32544bab27d4452f80d9ce51349b1a28f3005c43", + "https://bcr.bazel.build/modules/zlib/1.2.11/MODULE.bazel": "07b389abc85fdbca459b69e2ec656ae5622873af3f845e1c9d80fe179f3effa0", + "https://bcr.bazel.build/modules/zlib/1.2.12/MODULE.bazel": "3b1a8834ada2a883674be8cbd36ede1b6ec481477ada359cd2d3ddc562340b27", + "https://bcr.bazel.build/modules/zlib/1.3.1.bcr.5/MODULE.bazel": "eec517b5bbe5492629466e11dae908d043364302283de25581e3eb944326c4ca", + "https://bcr.bazel.build/modules/zlib/1.3.1.bcr.5/source.json": "22bc55c47af97246cfc093d0acf683a7869377de362b5d1c552c2c2e16b7a806", + "https://bcr.bazel.build/modules/zlib/1.3.1/MODULE.bazel": "751c9940dcfe869f5f7274e1295422a34623555916eb98c174c1e945594bf198" + }, + "selectedYankedVersions": {}, + "moduleExtensions": { + "@@rules_kotlin+//src/main/starlark/core/repositories:bzlmod_setup.bzl%rules_kotlin_extensions": { + "general": { + "bzlTransitiveDigest": "rL/34P1aFDq2GqVC2zCFgQ8nTuOC6ziogocpvG50Qz8=", + "usagesDigest": "QI2z8ZUR+mqtbwsf2fLqYdJAkPOHdOV+tF2yVAUgRzw=", + "recordedFileInputs": {}, + "recordedDirentsInputs": {}, + "envVariables": {}, + "generatedRepoSpecs": { + "com_github_jetbrains_kotlin_git": { + "repoRuleId": "@@rules_kotlin+//src/main/starlark/core/repositories:compiler.bzl%kotlin_compiler_git_repository", + "attributes": { + "urls": [ + "https://github.com/JetBrains/kotlin/releases/download/v1.9.23/kotlin-compiler-1.9.23.zip" + ], + "sha256": "93137d3aab9afa9b27cb06a824c2324195c6b6f6179d8a8653f440f5bd58be88" + } + }, + "com_github_jetbrains_kotlin": { + "repoRuleId": "@@rules_kotlin+//src/main/starlark/core/repositories:compiler.bzl%kotlin_capabilities_repository", + "attributes": { + "git_repository_name": "com_github_jetbrains_kotlin_git", + "compiler_version": "1.9.23" + } + }, + "com_github_google_ksp": { + "repoRuleId": "@@rules_kotlin+//src/main/starlark/core/repositories:ksp.bzl%ksp_compiler_plugin_repository", + "attributes": { + "urls": [ + "https://github.com/google/ksp/releases/download/1.9.23-1.0.20/artifacts.zip" + ], + "sha256": "ee0618755913ef7fd6511288a232e8fad24838b9af6ea73972a76e81053c8c2d", + "strip_version": "1.9.23-1.0.20" + } + }, + "com_github_pinterest_ktlint": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_file", + "attributes": { + "sha256": "01b2e0ef893383a50dbeb13970fe7fa3be36ca3e83259e01649945b09d736985", + "urls": [ + "https://github.com/pinterest/ktlint/releases/download/1.3.0/ktlint" + ], + "executable": true + } + }, + "rules_android": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "sha256": "cd06d15dd8bb59926e4d65f9003bfc20f9da4b2519985c27e190cddc8b7a7806", + "strip_prefix": "rules_android-0.1.1", + "urls": [ + "https://github.com/bazelbuild/rules_android/archive/v0.1.1.zip" + ] + } + } + }, + "recordedRepoMappingEntries": [ + [ + "rules_kotlin+", + "bazel_tools", + "bazel_tools" + ] + ] + } + } + }, + "facts": { + "@@rules_go+//go:extensions.bzl%go_sdk": { + "1.25.0": { + "aix_ppc64": [ + "go1.25.0.aix-ppc64.tar.gz", + "e5234a7dac67bc86c528fe9752fc9d63557918627707a733ab4cac1a6faed2d4" + ], + "darwin_amd64": [ + "go1.25.0.darwin-amd64.tar.gz", + "5bd60e823037062c2307c71e8111809865116714d6f6b410597cf5075dfd80ef" + ], + "darwin_arm64": [ + "go1.25.0.darwin-arm64.tar.gz", + "544932844156d8172f7a28f77f2ac9c15a23046698b6243f633b0a0b00c0749c" + ], + "dragonfly_amd64": [ + "go1.25.0.dragonfly-amd64.tar.gz", + "5ed3cf9a810a1483822538674f1336c06b51aa1b94d6d545a1a0319a48177120" + ], + "freebsd_386": [ + "go1.25.0.freebsd-386.tar.gz", + "abea5d5c6697e6b5c224731f2158fe87c602996a2a233ac0c4730cd57bf8374e" + ], + "freebsd_amd64": [ + "go1.25.0.freebsd-amd64.tar.gz", + "86e6fe0a29698d7601c4442052dac48bd58d532c51cccb8f1917df648138730b" + ], + "freebsd_arm": [ + "go1.25.0.freebsd-arm.tar.gz", + "d90b78e41921f72f30e8bbc81d9dec2cff7ff384a33d8d8debb24053e4336bfe" + ], + "freebsd_arm64": [ + "go1.25.0.freebsd-arm64.tar.gz", + "451d0da1affd886bfb291b7c63a6018527b269505db21ce6e14724f22ab0662e" + ], + "freebsd_riscv64": [ + "go1.25.0.freebsd-riscv64.tar.gz", + "7b565f76bd8bda46549eeaaefe0e53b251e644c230577290c0f66b1ecdb3cdbe" + ], + "illumos_amd64": [ + "go1.25.0.illumos-amd64.tar.gz", + "b1e1fdaab1ad25aa1c08d7a36c97d45d74b98b89c3f78c6d2145f77face54a2c" + ], + "linux_386": [ + "go1.25.0.linux-386.tar.gz", + "8c602dd9d99bc9453b3995d20ce4baf382cc50855900a0ece5de9929df4a993a" + ], + "linux_amd64": [ + "go1.25.0.linux-amd64.tar.gz", + "2852af0cb20a13139b3448992e69b868e50ed0f8a1e5940ee1de9e19a123b613" + ], + "linux_arm64": [ + "go1.25.0.linux-arm64.tar.gz", + "05de75d6994a2783699815ee553bd5a9327d8b79991de36e38b66862782f54ae" + ], + "linux_armv6l": [ + "go1.25.0.linux-armv6l.tar.gz", + "a5a8f8198fcf00e1e485b8ecef9ee020778bf32a408a4e8873371bfce458cd09" + ], + "linux_loong64": [ + "go1.25.0.linux-loong64.tar.gz", + "cab86b1cf761b1cb3bac86a8877cfc92e7b036fc0d3084123d77013d61432afc" + ], + "linux_mips": [ + "go1.25.0.linux-mips.tar.gz", + "d66b6fb74c3d91b9829dc95ec10ca1f047ef5e89332152f92e136cf0e2da5be1" + ], + "linux_mips64": [ + "go1.25.0.linux-mips64.tar.gz", + "4082e4381a8661bc2a839ff94ba3daf4f6cde20f8fb771b5b3d4762dc84198a2" + ], + "linux_mips64le": [ + "go1.25.0.linux-mips64le.tar.gz", + "70002c299ec7f7175ac2ef673b1b347eecfa54ae11f34416a6053c17f855afcc" + ], + "linux_mipsle": [ + "go1.25.0.linux-mipsle.tar.gz", + "b00a3a39eff099f6df9f1c7355bf28e4589d0586f42d7d4a394efb763d145a73" + ], + "linux_ppc64": [ + "go1.25.0.linux-ppc64.tar.gz", + "df166f33bd98160662560a72ff0b4ba731f969a80f088922bddcf566a88c1ec1" + ], + "linux_ppc64le": [ + "go1.25.0.linux-ppc64le.tar.gz", + "0f18a89e7576cf2c5fa0b487a1635d9bcbf843df5f110e9982c64df52a983ad0" + ], + "linux_riscv64": [ + "go1.25.0.linux-riscv64.tar.gz", + "c018ff74a2c48d55c8ca9b07c8e24163558ffec8bea08b326d6336905d956b67" + ], + "linux_s390x": [ + "go1.25.0.linux-s390x.tar.gz", + "34e5a2e19f2292fbaf8783e3a241e6e49689276aef6510a8060ea5ef54eee408" + ], + "netbsd_386": [ + "go1.25.0.netbsd-386.tar.gz", + "f8586cdb7aa855657609a5c5f6dbf523efa00c2bbd7c76d3936bec80aa6c0aba" + ], + "netbsd_amd64": [ + "go1.25.0.netbsd-amd64.tar.gz", + "ae8dc1469385b86a157a423bb56304ba45730de8a897615874f57dd096db2c2a" + ], + "netbsd_arm": [ + "go1.25.0.netbsd-arm.tar.gz", + "1ff7e4cc764425fc9dd6825eaee79d02b3c7cafffbb3691687c8d672ade76cb7" + ], + "netbsd_arm64": [ + "go1.25.0.netbsd-arm64.tar.gz", + "e1b310739f26724216aa6d7d7208c4031f9ff54c9b5b9a796ddc8bebcb4a5f16" + ], + "openbsd_386": [ + "go1.25.0.openbsd-386.tar.gz", + "4802a9b20e533da91adb84aab42e94aa56cfe3e5475d0550bed3385b182e69d8" + ], + "openbsd_amd64": [ + "go1.25.0.openbsd-amd64.tar.gz", + "c016cd984bebe317b19a4f297c4f50def120dc9788490540c89f28e42f1dabe1" + ], + "openbsd_arm": [ + "go1.25.0.openbsd-arm.tar.gz", + "a1e31d0bf22172ddde42edf5ec811ef81be43433df0948ece52fecb247ccfd8d" + ], + "openbsd_arm64": [ + "go1.25.0.openbsd-arm64.tar.gz", + "343ea8edd8c218196e15a859c6072d0dd3246fbbb168481ab665eb4c4140458d" + ], + "openbsd_ppc64": [ + "go1.25.0.openbsd-ppc64.tar.gz", + "694c14da1bcaeb5e3332d49bdc2b6d155067648f8fe1540c5de8f3cf8e157154" + ], + "openbsd_riscv64": [ + "go1.25.0.openbsd-riscv64.tar.gz", + "aa510ad25cf54c06cd9c70b6d80ded69cb20188ac6e1735655eef29ff7e7885f" + ], + "plan9_386": [ + "go1.25.0.plan9-386.tar.gz", + "46f8cef02086cf04bf186c5912776b56535178d4cb319cd19c9fdbdd29231986" + ], + "plan9_amd64": [ + "go1.25.0.plan9-amd64.tar.gz", + "29b34391d84095e44608a228f63f2f88113a37b74a79781353ec043dfbcb427b" + ], + "plan9_arm": [ + "go1.25.0.plan9-arm.tar.gz", + "0a047107d13ebe7943aaa6d54b1d7bbd2e45e68ce449b52915a818da715799c2" + ], + "solaris_amd64": [ + "go1.25.0.solaris-amd64.tar.gz", + "9977f9e4351984364a3b2b78f8b88bfd1d339812356d5237678514594b7d3611" + ], + "windows_386": [ + "go1.25.0.windows-386.zip", + "df9f39db82a803af0db639e3613a36681ab7a42866b1384b3f3a1045663961a7" + ], + "windows_amd64": [ + "go1.25.0.windows-amd64.zip", + "89efb4f9b30812eee083cc1770fdd2913c14d301064f6454851428f9707d190b" + ], + "windows_arm64": [ + "go1.25.0.windows-arm64.zip", + "27bab004c72b3d7bd05a69b6ec0fc54a309b4b78cc569dd963d8b3ec28bfdb8c" + ] + }, + "1.25.6": { + "aix_ppc64": [ + "go1.25.6.aix-ppc64.tar.gz", + "13c8bca505dd902091304da8abfacaf3512f40c3faefae70db33337d9a42c90e" + ], + "darwin_amd64": [ + "go1.25.6.darwin-amd64.tar.gz", + "e2b5b237f5c262931b8e280ac4b8363f156e19bfad5270c099998932819670b7" + ], + "darwin_arm64": [ + "go1.25.6.darwin-arm64.tar.gz", + "984521ae978a5377c7d782fd2dd953291840d7d3d0bd95781a1f32f16d94a006" + ], + "dragonfly_amd64": [ + "go1.25.6.dragonfly-amd64.tar.gz", + "6fdcdd4f769fe73a9c5602eb25533954903520f2a2a1953415ec4f8abf5bda52" + ], + "freebsd_386": [ + "go1.25.6.freebsd-386.tar.gz", + "be22b65ded1d4015d7d9d328284c985932771d120a371c7df41b2d4d1a91e943" + ], + "freebsd_amd64": [ + "go1.25.6.freebsd-amd64.tar.gz", + "61e1d50e332359474ff6dcf4bc0bd34ba2d2cf4ef649593a5faa527f0ab84e2b" + ], + "freebsd_arm": [ + "go1.25.6.freebsd-arm.tar.gz", + "546c2c6e325e72531bf6c8122a2360db8f8381e2dc1e8d147ecb0cb49b5f5f93" + ], + "freebsd_arm64": [ + "go1.25.6.freebsd-arm64.tar.gz", + "648484146702dd58db0e2c3d15bda3560340d149ed574936e63285a823116b77" + ], + "freebsd_riscv64": [ + "go1.25.6.freebsd-riscv64.tar.gz", + "663d7a9532bb4ac03c7a36b13b677b36d71031cd757b8acaee085e36c9ec8bc2" + ], + "illumos_amd64": [ + "go1.25.6.illumos-amd64.tar.gz", + "c6adb151f8f50a25ef5a3f7b1be67155045daa766261e686ea210b93b46bbbd5" + ], + "linux_386": [ + "go1.25.6.linux-386.tar.gz", + "59fe62eee3cca65332acef3ebe9b6ff3272467e0a08bf7f68f96334902bf23b9" + ], + "linux_amd64": [ + "go1.25.6.linux-amd64.tar.gz", + "f022b6aad78e362bcba9b0b94d09ad58c5a70c6ba3b7582905fababf5fe0181a" + ], + "linux_arm64": [ + "go1.25.6.linux-arm64.tar.gz", + "738ef87d79c34272424ccdf83302b7b0300b8b096ed443896089306117943dd5" + ], + "linux_armv6l": [ + "go1.25.6.linux-armv6l.tar.gz", + "679f0e70b27c637116791e3c98afbf8c954deb2cd336364944d014f8e440e2ae" + ], + "linux_loong64": [ + "go1.25.6.linux-loong64.tar.gz", + "433fe54d8797700b44fc4f1d085f9cd50ab3511b9b484fdfbb7b6c32a2be2486" + ], + "linux_mips": [ + "go1.25.6.linux-mips.tar.gz", + "a5beaf2d135b8e9a2f3d91fa7e7d3761ffc97630484168bbc9a21f3901119c11" + ], + "linux_mips64": [ + "go1.25.6.linux-mips64.tar.gz", + "f2d72c1ac315d453f429f48900f43cd8d0aa296a2b82fa90dba7dfb907483fd8" + ], + "linux_mips64le": [ + "go1.25.6.linux-mips64le.tar.gz", + "9b808ef978fd6414edd16736daa4a601c7e2dadff3bd640ade8a976535c974d4" + ], + "linux_mipsle": [ + "go1.25.6.linux-mipsle.tar.gz", + "4e0b190b05c8359455d96d379c751d403554dcadf6765932845b2886e555bfd6" + ], + "linux_ppc64": [ + "go1.25.6.linux-ppc64.tar.gz", + "5d0f479023b1481c9188cc066eca1293e6f8a67a882a6d93afafccfb51981476" + ], + "linux_ppc64le": [ + "go1.25.6.linux-ppc64le.tar.gz", + "bee02dbe034b12b839ae7807a85a61c13bee09ee38f2eeba2074bd26c0c0ab73" + ], + "linux_riscv64": [ + "go1.25.6.linux-riscv64.tar.gz", + "82a6b989afda1681ecb1f4fa96f1006484f42643eb5e76bed58f7f97316bf84b" + ], + "linux_s390x": [ + "go1.25.6.linux-s390x.tar.gz", + "3d97cc5670a0da9cb177037782129f0bf499ecb47abc40488248548abd2c2c35" + ], + "netbsd_386": [ + "go1.25.6.netbsd-386.tar.gz", + "eb526fff2568fc9938d6eda6f0f50449661c693fcd89ab6f84e5e77e0a98d99b" + ], + "netbsd_amd64": [ + "go1.25.6.netbsd-amd64.tar.gz", + "959d786e3384403ac9d957c04d71da905b02f457406ca123662cbd4688f9ce6e" + ], + "netbsd_arm": [ + "go1.25.6.netbsd-arm.tar.gz", + "fe6c3957f7feaf17ac72ca27590cc4914c19162fc0912869048cb3dc92f5c3fd" + ], + "netbsd_arm64": [ + "go1.25.6.netbsd-arm64.tar.gz", + "ddb5ec67fc4a0510b23560b7c01413bd9dde513cebfb5441a93e934f7e0c6853" + ], + "openbsd_386": [ + "go1.25.6.openbsd-386.tar.gz", + "167a18ff7db53f1652f3a65c905056bc14e7ab4319357498d0af998a83f457a9" + ], + "openbsd_amd64": [ + "go1.25.6.openbsd-amd64.tar.gz", + "06ec42383ff1e17abc0472e0a92eb028cb40b16ea09e2a86f80fbe60912d62de" + ], + "openbsd_arm": [ + "go1.25.6.openbsd-arm.tar.gz", + "751df8eadd0f3d7be8ea6cda3af1e2e942099f6c97abcc0cfb5c8a0ac8e0cf3f" + ], + "openbsd_arm64": [ + "go1.25.6.openbsd-arm64.tar.gz", + "d9828a6162c0c0fdb2d7e9dc8285c43b18a3dab62bf5e83b5891a4384f3157ad" + ], + "openbsd_ppc64": [ + "go1.25.6.openbsd-ppc64.tar.gz", + "73090f93dc861f2be9dc06d8209f32cd7ce7864b9b3e28f0cd54a9e031672699" + ], + "openbsd_riscv64": [ + "go1.25.6.openbsd-riscv64.tar.gz", + "6d4932cb639c1172cf5861b031bd0a24f7341ef579aac15b392779e10c69343b" + ], + "plan9_386": [ + "go1.25.6.plan9-386.tar.gz", + "b9db67922a94abe580e7bde9172eee2c223ade914cd12790d955a24554c134d5" + ], + "plan9_amd64": [ + "go1.25.6.plan9-amd64.tar.gz", + "aa1ff9aa3e1ed09ecb21d09d736997d2de9f373fea9402815b3221946d17dcd5" + ], + "plan9_arm": [ + "go1.25.6.plan9-arm.tar.gz", + "94ec04501527876a542960096f0199495cbd9f9103b229d5299382aa51d9cc32" + ], + "solaris_amd64": [ + "go1.25.6.solaris-amd64.tar.gz", + "9a1e89979be591b44e63be766c6571f5dc27b5fc3b79965c943186fcdaca0386" + ], + "windows_386": [ + "go1.25.6.windows-386.zip", + "873da5cec02b6657ecd5b85e562a38fb5faf1b6e9ea81b2eb0b9a9b5aea5cb35" + ], + "windows_amd64": [ + "go1.25.6.windows-amd64.zip", + "19b4733b727ba5c611b5656187f3ac367d278d64c3d4199a845e39c0fdac5335" + ], + "windows_arm64": [ + "go1.25.6.windows-arm64.zip", + "8f2d8e6dd0849a2ec0ade1683bcfb7809e64d264a4273d8437841000a28ffb60" + ] + } + } + } +} diff --git a/README.md b/README.md index 30e0ba5c..dcf9c81c 100644 --- a/README.md +++ b/README.md @@ -1,120 +1,116 @@ -

- - SuperCoder Dark logo - - - SuperCoder Light logo - +# SuperCoder -

+A local-first, open-source coding agent for your desktop. Bring your own LLM +key; your code stays on your machine and only ever leaves to the model provider +*you* choose — no middleman service, no lock-in. -

Open Source Autonomous Software Development System

- +Turn on the optional **Context Engine** and the agent navigates large +codebases structurally — tree-sitter → vector + call-graph + BM25 retrieval — +instead of guessing. -

- +> **SuperCoder has been reimagined from the ground up.** The original (2024) +> autonomous-dev pipeline is frozen under [`v1/`](./v1) — preserved, not +> maintained or built. +--- -

- -SuperCoder Forks - +## Why SuperCoder - -SuperCoder stars - +- **Local-first & fully open.** A desktop app, not a cloud product. Your source + never transits a vendor backend — requests go straight from your machine to + the provider whose key you configured. +- **Bring your own model.** The agent speaks the **OpenAI chat-completions** and + **Anthropic Messages** APIs natively — no translation proxy. +- **Graph-aware code understanding (optional).** The Context Engine indexes your + repo into vector + call-graph + lexical search so the agent can locate code by + structure, not just text similarity. +- **A real harness underneath.** The core is a pure-Rust agent crate with + Ask / Plan / Coding modes, subagents, skills, tool approval, and prompt + caching. The desktop app is one adapter over it — see + [ARCHITECTURE.md](./ARCHITECTURE.md). -

+## Two ways to run -

Follow SuperAGI

+SuperCoder works the moment you add an LLM key — in-place edits, +Ask / Plan / Coding modes, checkpoints and rewind, diff review, an interactive +terminal, and a file explorer. **Zero backend required.** -

- -Follow _superAGI - - +Flip on the **Context Engine** (Settings → Context engine) for graph-aware, +repo-scale retrieval. It runs locally via `docker compose` and the agent's +`codebase_search` / `codebase_graph` tools query it. See +[`services/context-engine/README.md`](./services/context-engine/README.md). - -Join SuperAGI Discord Community - - -

+## Getting started -

Connect with the Creator

+> Prebuilt downloadable binaries are coming. For now, build from source. -

- -Follow ishaanbhola - - -Follow mukundns - -

+**Prerequisites** -

Share SuperCoder Repository

- -

- - -Follow _superAGI -Share on Telegram - -Share on Reddit - - -

- -
- -# SuperCoder: Open Source Autonomous Software Development System - -## What is SuperCoder? - -SuperCoder is an autonomous software development system that leverages advanced AI tools and agents to streamline and automate coding, testing, and deployment tasks, enhancing efficiency and reliability. - -## 🛠 Supported Languages & Frameworks - -SuperCoder 2.0 supports a variety of languages and frameworks for diverse development needs. - -Flask -Django -NextJS +- [Rust](https://rustup.rs/) (stable) and the + [Tauri 2 system prerequisites](https://v2.tauri.app/start/prerequisites/) for + your OS (WebView / build tooling). +- [Node.js](https://nodejs.org/) 20+ and npm. +- (Optional, for the Context Engine) [Docker](https://docs.docker.com/get-docker/) + with Compose. +**Run the app** +```bash +cd apps/desktop +npm install +npm run tauri:dev # development +# or +npm run tauri:build # produce a release bundle +``` +On first launch, open **Settings** and add an LLM provider (`base_url` + +`api_key` + `model`). Then create a session, pick a folder and a mode, and go. -## Prerequisites -Before you proceed, ensure that you have the following installed on your system: -- [Docker and Docker Compose](https://docs.docker.com/get-docker/) +**(Optional) Run the Context Engine** -## Setup +```bash +cd services/context-engine +cp .env.example .env # set SUPERCODER_OPENAI_API_KEY (server-side embedding key) +docker compose up -d --build +``` -### Build and Run the Go Server, Asynq worker, and Postgres +Then enable **Settings → Context engine** in the app. Full instructions: +[`services/context-engine/README.md`](./services/context-engine/README.md). -To build and run the Go server, Asynq worker, and Postgres, execute the following command: +## Repository layout -```bash -docker-compose up --build +``` +crates/ + agent/ Rust agent core — the harness (loop, tools, modes, subagents) + git-ops/ Checkpoint / diff / restore over the working tree +apps/ + desktop/ Tauri 2 + React desktop app (thin adapter over the core) +services/ + context-engine/ Optional Go indexing service (tree-sitter → Qdrant + FalkorDB + BM25) +v1/ Legacy 2024 codegen pipeline — frozen, not built ``` -You can now access the UI at http://localhost:3000. - -### 📚 Resources +See [ARCHITECTURE.md](./ARCHITECTURE.md) for how these fit together. -* [How to get started](https://superagi.com/get-started-with-supercoder/) -* [SuperCoder Blogs](https://superagi.com/blog/) -* [YouTube Channel](https://www.youtube.com/@_SuperAGI/videos) +## Roadmap -### 📖 Need Help? +Present-tense — what works today — is above. Next: -Join our [Discord community](https://discord.gg/pmFVyCDDyH) for support and discussions. +- **Prebuilt releases & installers** (the CI to produce them lands next). +- **Benchmarking the harness.** A headless runner over the *same* agent core, + with reproducible per-task execution sandboxes, to measure the harness as an + equalizer across models and to validate the graph-retrieval localization claim. +- **Broader provider support** (the provider abstraction is built to grow). -[![Join us on Discord](https://invidget.switchblade.xyz/pmFVyCDDyH)](https://discord.gg/pmFVyCDDyH) +## Contributing -If you have questions or encounter issues, please don't hesitate to [create a new issue](https://github.com/TransformerOptimus/SuperCoder/issues/new/choose) to get support or reach out to support@superagi.com. +Contributions are welcome — see [CONTRIBUTING.md](./CONTRIBUTING.md) for dev +setup and repo conventions, and [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md). +- **Bugs & features:** GitHub Issues. +- **Questions & ideas:** GitHub Discussions. +- **Security:** please report privately — see [SECURITY.md](./SECURITY.md). +## License -### ⚠️ Under Development! -This project is under active development and may still have issues. We appreciate your understanding and patience. If you encounter any problems, please check the open issues first. If your issue is not listed, kindly create a new issue detailing the error or problem you experienced. Thank you for your support! +[MIT](./LICENSE) © TransformerOptimus. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..5964f125 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,55 @@ +# Security Policy + +## Reporting a vulnerability + +Please report security vulnerabilities **privately** — do not open a public +issue, discussion, or pull request for a suspected vulnerability. + +Use GitHub's private vulnerability reporting: + +1. Go to the repository's **Security** tab. +2. Click **Report a vulnerability** (under "Advisories"). +3. Provide a description, affected component, reproduction steps, and impact. + +We'll acknowledge your report, investigate, and keep you updated on a fix and +disclosure timeline. + +## Scope + +SuperCoder is a local-first desktop application. Useful things to include in a +report: + +- The component: agent core (`crates/agent`), git-ops, the desktop bridge + (`apps/desktop/src-tauri`), or the Context Engine (`services/context-engine`). +- Whether it's reachable in the default zero-backend configuration or only with + the optional Context Engine enabled. +- The trust boundary crossed (e.g. path traversal outside the project, + unexpected outbound requests, command execution). + +The legacy `v1/` tree is frozen and unmaintained; please do not report issues +against it. + +## Handling secrets + +Never include real API keys, tokens, or other credentials in a report, issue, or +PR. Configure provider keys only via the app's Settings or environment variables. + +## Security model notes + +SuperCoder is a single-user, local-first desktop app. A few behaviors are +intentional given that model — they assume an attacker who can already run code +as your user has no boundary left to cross, so they are not treated as +vulnerabilities: + +- **Context-engine embedding key in the process environment.** In app-managed + mode the embedding key is passed to the local Docker stack via the + `SUPERCODER_OPENAI_API_KEY` environment variable. Same-UID processes can read + it (e.g. `/proc//environ`), as they can already read the app's local + SQLite store. The key is never logged or written into the compose file. +- **`SUPERCODER_CE_*` environment overrides** (compose-file path, image refs, + mode, port) are trusted developer conveniences for local testing. Setting them + requires control of the launch environment, which already implies user-level + code execution. +- **User-mode backend URL probe.** In user mode the app probes the backend URL + *you* configure (`/api/health`) with no host allowlist — that is the feature + (connect to your own self-run engine), not an SSRF sink. diff --git a/gui/public/fonts/proxima-nova-2/Mark-Simonson-Proxima-Nova-Black.otf b/apps/.gitkeep similarity index 100% rename from gui/public/fonts/proxima-nova-2/Mark-Simonson-Proxima-Nova-Black.otf rename to apps/.gitkeep diff --git a/apps/desktop/index.html b/apps/desktop/index.html new file mode 100644 index 00000000..1f1d29f2 --- /dev/null +++ b/apps/desktop/index.html @@ -0,0 +1,13 @@ + + + + + + SuperCoder + + + +
+ + + diff --git a/apps/desktop/package-lock.json b/apps/desktop/package-lock.json new file mode 100644 index 00000000..ae82a31e --- /dev/null +++ b/apps/desktop/package-lock.json @@ -0,0 +1,6752 @@ +{ + "name": "supercoder-desktop", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "supercoder-desktop", + "version": "1.0.0", + "dependencies": { + "@tauri-apps/api": "^2.0.0", + "@tauri-apps/plugin-dialog": "^2.6.0", + "@tauri-apps/plugin-notification": "^2.3.3", + "@tauri-apps/plugin-shell": "^2.0.0", + "@xterm/addon-fit": "^0.11.0", + "antd": "^6.3.2", + "buffer": "^6.0.3", + "dayjs": "^1.11.20", + "gitdiff-parser": "^0.3.1", + "lucide-react": "^0.577.0", + "mermaid": "^11.13.0", + "path-browserify": "^1.0.1", + "react": "^18.3.1", + "react-diff-view": "^3.3.3", + "react-dom": "^18.3.1", + "react-markdown": "^10.1.0", + "react-router-dom": "^7.13.1", + "refractor": "^3.6.0", + "remark-gfm": "^4.0.1", + "tauri-pty": "^0.2.1", + "xterm": "^5.3.0", + "zustand": "^5.0.11" + }, + "devDependencies": { + "@tauri-apps/cli": "^2.0.0", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.1", + "autoprefixer": "^10.4.19", + "postcss": "^8.4.39", + "tailwindcss": "^3.4.4", + "typescript": "^5.5.3", + "vite": "^5.3.3", + "vitest": "^2.1.9" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ant-design/colors": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-8.0.1.tgz", + "integrity": "sha512-foPVl0+SWIslGUtD/xBr1p9U4AKzPhNYEseXYRRo5QSzGACYZrQbe11AYJbYfAWnWSpGBx6JjBmSeugUsD9vqQ==", + "dependencies": { + "@ant-design/fast-color": "^3.0.0" + } + }, + "node_modules/@ant-design/cssinjs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@ant-design/cssinjs/-/cssinjs-2.1.2.tgz", + "integrity": "sha512-2Hy8BnCEH31xPeSLbhhB2ctCPXE2ZnASdi+KbSeS79BNbUhL9hAEe20SkUk+BR8aKTmqb6+FKFruk7w8z0VoRQ==", + "dependencies": { + "@babel/runtime": "^7.11.1", + "@emotion/hash": "^0.8.0", + "@emotion/unitless": "^0.7.5", + "@rc-component/util": "^1.4.0", + "clsx": "^2.1.1", + "csstype": "^3.1.3", + "stylis": "^4.3.4" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/@ant-design/cssinjs-utils": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@ant-design/cssinjs-utils/-/cssinjs-utils-2.1.2.tgz", + "integrity": "sha512-5fTHQ158jJJ5dC/ECeyIdZUzKxE/mpEMRZxthyG1sw/AKRHKgJBg00Yi6ACVXgycdje7KahRNvNET/uBccwCnA==", + "dependencies": { + "@ant-design/cssinjs": "^2.1.2", + "@babel/runtime": "^7.23.2", + "@rc-component/util": "^1.4.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/@ant-design/fast-color": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@ant-design/fast-color/-/fast-color-3.0.1.tgz", + "integrity": "sha512-esKJegpW4nckh0o6kV3Tkb7NPIZYbPnnFxmQDUmL08ukXZAvV85TZBr70eGuke/CIArLaP6aw8lt9KILjnWuOw==", + "engines": { + "node": ">=8.x" + } + }, + "node_modules/@ant-design/icons": { + "version": "6.2.5", + "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-6.2.5.tgz", + "integrity": "sha512-0hKtoKqTjGFOndUyJLJmC9Cg6k4rEO7rLo6xmgbNJH+/ZX1C57RVals2v1j1knHl9n7Q+sBOveTvn931wLOCKw==", + "dependencies": { + "@ant-design/colors": "^8.0.1", + "@ant-design/icons-svg": "^4.4.2", + "@rc-component/util": "^1.11.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/@ant-design/icons-svg": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@ant-design/icons-svg/-/icons-svg-4.4.2.tgz", + "integrity": "sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA==" + }, + "node_modules/@ant-design/react-slick": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@ant-design/react-slick/-/react-slick-2.0.0.tgz", + "integrity": "sha512-HMS9sRoEmZey8LsE/Yo6+klhlzU12PisjrVcydW3So7RdklyEd2qehyU6a7Yp+OYN72mgsYs3NFCyP2lCPFVqg==", + "dependencies": { + "@babel/runtime": "^7.28.4", + "clsx": "^2.1.1", + "json2mq": "^0.2.0", + "throttle-debounce": "^5.0.0" + }, + "peerDependencies": { + "react": "^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@antfu/install-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.1.0.tgz", + "integrity": "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==", + "dependencies": { + "package-manager-detector": "^1.3.0", + "tinyexec": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@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" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@braintree/sanitize-url": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.2.tgz", + "integrity": "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==" + }, + "node_modules/@chevrotain/types": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.1.2.tgz", + "integrity": "sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==" + }, + "node_modules/@emotion/hash": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", + "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==" + }, + "node_modules/@emotion/unitless": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", + "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@iconify/types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==" + }, + "node_modules/@iconify/utils": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-3.1.3.tgz", + "integrity": "sha512-LPKOXPn/zV+zis1oOfGWogaXVpqUybF3ZS6SCZIsz8vg0ivVp9+fVqyYB7xq0aiST/VhUQYGO1qo6uoYSiEJqw==", + "dependencies": { + "@antfu/install-pkg": "^1.1.0", + "@iconify/types": "^2.0.0", + "import-meta-resolve": "^4.2.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mermaid-js/parser": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-1.1.1.tgz", + "integrity": "sha512-VuHdsYMK1bT6X2JbcAaWAhugTRvRBRyuZgd+c22swUeI9g/ntaxF7CY7dYarhZovofCbUNO0G7JesfmNtjYOCw==", + "dependencies": { + "@chevrotain/types": "~11.1.1" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@rc-component/async-validator": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@rc-component/async-validator/-/async-validator-5.1.0.tgz", + "integrity": "sha512-n4HcR5siNUXRX23nDizbZBQPO0ZM/5oTtmKZ6/eqL0L2bo747cklFdZGRN2f+c9qWGICwDzrhW0H7tE9PptdcA==", + "dependencies": { + "@babel/runtime": "^7.24.4" + }, + "engines": { + "node": ">=14.x" + } + }, + "node_modules/@rc-component/cascader": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@rc-component/cascader/-/cascader-1.15.0.tgz", + "integrity": "sha512-ZzpMtwFCRo3fbXHuDnncARJMZQjdqA2w7aDuPofNQt+aDx39st1hgfIpEwTBLhe2Hqsvs/zOr8RTtgxTkCPySw==", + "dependencies": { + "@rc-component/select": "~1.6.0", + "@rc-component/tree": "~1.3.0", + "@rc-component/util": "^1.4.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/checkbox": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@rc-component/checkbox/-/checkbox-2.0.0.tgz", + "integrity": "sha512-3CXGPpAR9gsPKeO2N78HAPOzU30UdemD6HGJoWVJOpa6WleaGB5kzZj3v6bdTZab31YuWgY/RxV3VKPctn0DwQ==", + "dependencies": { + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/collapse": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@rc-component/collapse/-/collapse-1.2.0.tgz", + "integrity": "sha512-ZRYSKSS39qsFx93p26bde7JUZJshsUBEQRlRXPuJYlAiNX0vyYlF5TsAm8JZN3LcF8XvKikdzPbgAtXSbkLUkw==", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/motion": "^1.1.4", + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/color-picker": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@rc-component/color-picker/-/color-picker-3.1.1.tgz", + "integrity": "sha512-OHaCHLHszCegdXmIq2ZRIZBN/EtpT6Wm8SG/gpzLATHbVKc/avvuKi+zlOuk05FTWvgaMmpxAko44uRJ3M+2pg==", + "dependencies": { + "@ant-design/fast-color": "^3.0.1", + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/context": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@rc-component/context/-/context-2.0.1.tgz", + "integrity": "sha512-HyZbYm47s/YqtP6pKXNMjPEMaukyg7P0qVfgMLzr7YiFNMHbK2fKTAGzms9ykfGHSfyf75nBbgWw+hHkp+VImw==", + "dependencies": { + "@rc-component/util": "^1.3.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/dialog": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@rc-component/dialog/-/dialog-1.9.0.tgz", + "integrity": "sha512-zbAAogkg4kkKum79sLE6M+vq1jSAW25zdkafrahgcTP9t9S//SD634Znd1A4c8F2Gc12ZKnehGLsVaaOvZzD2A==", + "dependencies": { + "@rc-component/motion": "^1.1.3", + "@rc-component/portal": "^2.1.0", + "@rc-component/util": "^1.9.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/drawer": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@rc-component/drawer/-/drawer-1.4.2.tgz", + "integrity": "sha512-1ib+fZEp6FBu+YvcIktm+nCQ+Q+qIpwpoaJH6opGr4ofh2QMq+qdr5DLC4oCf5qf3pcWX9lUWPYX652k4ini8Q==", + "dependencies": { + "@rc-component/motion": "^1.1.4", + "@rc-component/portal": "^2.1.3", + "@rc-component/util": "^1.9.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/dropdown": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rc-component/dropdown/-/dropdown-1.0.2.tgz", + "integrity": "sha512-6PY2ecUSYhDPhkNHHb4wfeAya04WhpmUSKzdR60G+kMNVUCX2vjT/AgTS0Lz0I/K6xrPMJ3enQbwVpeN3sHCgg==", + "dependencies": { + "@rc-component/trigger": "^3.0.0", + "@rc-component/util": "^1.2.1", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.11.0", + "react-dom": ">=16.11.0" + } + }, + "node_modules/@rc-component/form": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/@rc-component/form/-/form-1.8.2.tgz", + "integrity": "sha512-ZidCvOLmM9Xr+3vzk4UAoR7Aj1W/5IHyrzlBB7sNkygpTeRVrohQSo4TN7W/nARTH+nt8zSAPsn4BEl4zLEO2g==", + "dependencies": { + "@rc-component/async-validator": "^5.1.0", + "@rc-component/util": "^1.11.1", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/image": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@rc-component/image/-/image-1.9.0.tgz", + "integrity": "sha512-khF7w7xkBH5B1bsBcI1FSUZdkyd1aqpl2eYyILCqCzzQH3XdfehGUaZTnptyaJJfs09/R5hv9jXWyazOMFIClQ==", + "dependencies": { + "@rc-component/motion": "^1.0.0", + "@rc-component/portal": "^2.1.2", + "@rc-component/util": "^1.10.1", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/input": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@rc-component/input/-/input-1.3.1.tgz", + "integrity": "sha512-iFvTUT9W+JC/MSin2aGAk8NqsVlTzcExNC9DZariON1IWirju9NoNeEk47an4Q8iHazkoVI/y1LnDi88+CPcig==", + "dependencies": { + "@rc-component/resize-observer": "^1.1.1", + "@rc-component/util": "^1.11.1", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/@rc-component/input-number": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@rc-component/input-number/-/input-number-1.6.2.tgz", + "integrity": "sha512-Gjcq7meZlCOiWN1t1xCC+7/s85humHVokTBI7PJgTfoyw5OWF74y3e6P8PHX104g9+b54jsodFIzyaj6p8LI9w==", + "dependencies": { + "@rc-component/mini-decimal": "^1.0.1", + "@rc-component/util": "^1.4.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/mentions": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@rc-component/mentions/-/mentions-1.9.0.tgz", + "integrity": "sha512-WUwfFKDSOF5S9UPsNsXcLYtzjTxBGsftTXWRbZuxX6BYrsySISTnujfJNgaaQ6qVzaCDJ35QUkZKvsYxip1C5g==", + "dependencies": { + "@rc-component/input": "~1.3.0", + "@rc-component/menu": "~1.3.0", + "@rc-component/trigger": "^3.0.0", + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/menu": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@rc-component/menu/-/menu-1.3.1.tgz", + "integrity": "sha512-pSZl9nBPgKgxN0aaW7NilIBEwWsc+43S+ulGdWAg9afak96dNOGWsGx0DLLBB1VQsAJvo6bQMTDzXoPlEHsBEw==", + "dependencies": { + "@rc-component/motion": "^1.1.4", + "@rc-component/overflow": "^1.0.0", + "@rc-component/trigger": "^3.0.0", + "@rc-component/util": "^1.11.1", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/mini-decimal": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rc-component/mini-decimal/-/mini-decimal-1.1.3.tgz", + "integrity": "sha512-bk/FJ09fLf+NLODMAFll6CfYrHPBioTedhW6lxDBuuWucJEqFUd4l/D/5JgIi3dina6sYahB8iuPAZTNz2pMxw==", + "dependencies": { + "@babel/runtime": "^7.18.0" + }, + "engines": { + "node": ">=8.x" + } + }, + "node_modules/@rc-component/motion": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@rc-component/motion/-/motion-1.3.2.tgz", + "integrity": "sha512-itfd+GztzJYAb04Z4RkEub1TbJAfZc2Iuy8p44U44xD1F5+fNYFKI3897ijlbIyfvXkTmMm+KGcjkQQGMHywEQ==", + "dependencies": { + "@rc-component/util": "^1.2.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/mutate-observer": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@rc-component/mutate-observer/-/mutate-observer-2.0.1.tgz", + "integrity": "sha512-AyarjoLU5YlxuValRi+w8JRH2Z84TBbFO2RoGWz9d8bSu0FqT8DtugH3xC3BV7mUwlmROFauyWuXFuq4IFbH+w==", + "dependencies": { + "@rc-component/util": "^1.2.0" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/notification": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@rc-component/notification/-/notification-2.0.7.tgz", + "integrity": "sha512-nqZzpf6BPdaj+3ILx7si79LLmqPKyUmQoXa+/9gg0SkH0v1DbD66oJgRMSBEVnd/zUT3D4gwxWIHUKebYf2ZXQ==", + "dependencies": { + "@rc-component/motion": "^1.1.4", + "@rc-component/util": "^1.11.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/overflow": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rc-component/overflow/-/overflow-1.0.1.tgz", + "integrity": "sha512-syfmgAABaHCnCDzPwHZ/2tuvIcpOO3jefYZMmfkN+pmo8HKTzsfhS57vxo4ksPdN0By+uWVJhJWNFozNBxi2eA==", + "dependencies": { + "@babel/runtime": "^7.11.1", + "@rc-component/resize-observer": "^1.0.1", + "@rc-component/util": "^1.4.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/pagination": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@rc-component/pagination/-/pagination-1.2.0.tgz", + "integrity": "sha512-YcpUFE8dMLfSo6OARJlK6DbHHvrxz7pMGPGmC/caZSJJz6HRKHC1RPP001PRHCvG9Z/veD039uOQmazVuLJzlw==", + "dependencies": { + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/picker": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@rc-component/picker/-/picker-1.10.0.tgz", + "integrity": "sha512-vVOXP2RVWozwpERGUFAehVH1Jz6o/uRrAb9qSZm1LC+iJs8rvEwFo1bzz2jlOYV+uWwu0dIuG86tnDui14Ea0w==", + "dependencies": { + "@rc-component/overflow": "^1.0.0", + "@rc-component/resize-observer": "^1.0.0", + "@rc-component/trigger": "^3.6.15", + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=12.x" + }, + "peerDependencies": { + "date-fns": ">= 2.x", + "dayjs": ">= 1.x", + "luxon": ">= 3.x", + "moment": ">= 2.x", + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + }, + "peerDependenciesMeta": { + "date-fns": { + "optional": true + }, + "dayjs": { + "optional": true + }, + "luxon": { + "optional": true + }, + "moment": { + "optional": true + } + } + }, + "node_modules/@rc-component/portal": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@rc-component/portal/-/portal-2.2.0.tgz", + "integrity": "sha512-oc6FlA+uXCMiwArHsJyHcIkX4q6uKyndrPol2eWX8YPkAnztHOPsFIRtmWG4BMlGE5h7YIRE3NiaJ5VS8Lb1QQ==", + "dependencies": { + "@rc-component/util": "^1.2.1", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=12.x" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/progress": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rc-component/progress/-/progress-1.0.2.tgz", + "integrity": "sha512-WZUnH9eGxH1+xodZKqdrHke59uyGZSWgj5HBM5Kwk5BrTMuAORO7VJ2IP5Qbm9aH3n9x3IcesqHHR0NWPBC7fQ==", + "dependencies": { + "@rc-component/util": "^1.2.1", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/qrcode": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@rc-component/qrcode/-/qrcode-1.1.1.tgz", + "integrity": "sha512-LfLGNymzKdUPjXUbRP+xOhIWY4jQ+YMj5MmWAcgcAq1Ij8XP7tRmAXqyuv96XvLUBE/5cA8hLFl9eO1JQMujrA==", + "dependencies": { + "@babel/runtime": "^7.24.7" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/rate": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rc-component/rate/-/rate-1.0.1.tgz", + "integrity": "sha512-bkXxeBqDpl5IOC7yL7GcSYjQx9G8H+6kLYQnNZWeBYq2OYIv1MONd6mqKTjnnJYpV0cQIU2z3atdW0j1kttpTw==", + "dependencies": { + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/resize-observer": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@rc-component/resize-observer/-/resize-observer-1.1.2.tgz", + "integrity": "sha512-t/Bb0W8uvL4PYKAB3YcChC+DlHh0Wt5kM7q/J+0qpVEUMLe7Hk5zuvc9km0hMnTFPSx5Z7Wu/fzCLN6erVLE8Q==", + "dependencies": { + "@rc-component/util": "^1.2.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/segmented": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@rc-component/segmented/-/segmented-1.3.0.tgz", + "integrity": "sha512-5J/bJ01mbDnoA6P/FW8SxUvKn+OgUSTZJPzCNnTBntG50tzoP7DydGhqxp7ggZXZls7me3mc2EQDXakU3iTVFg==", + "dependencies": { + "@babel/runtime": "^7.11.1", + "@rc-component/motion": "^1.1.4", + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/@rc-component/select": { + "version": "1.6.15", + "resolved": "https://registry.npmjs.org/@rc-component/select/-/select-1.6.15.tgz", + "integrity": "sha512-SyVCWnqxCQZZcQvQJ/CxSjx2bGma6ds/HtnpkIfZVnt6RoEgbqUmHgD6vrzNarNXwbLXerwVzWwq8F3d1sst7g==", + "dependencies": { + "@rc-component/overflow": "^1.0.0", + "@rc-component/trigger": "^3.0.0", + "@rc-component/util": "^1.3.0", + "@rc-component/virtual-list": "^1.0.1", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/@rc-component/slider": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rc-component/slider/-/slider-1.0.1.tgz", + "integrity": "sha512-uDhEPU1z3WDfCJhaL9jfd2ha/Eqpdfxsn0Zb0Xcq1NGQAman0TWaR37OWp2vVXEOdV2y0njSILTMpTfPV1454g==", + "dependencies": { + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/steps": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@rc-component/steps/-/steps-1.2.2.tgz", + "integrity": "sha512-/yVIZ00gDYYPHSY0JP+M+s3ZvuXLu2f9rEjQqiUDs7EcYsUYrpJ/1bLj9aI9R7MBR3fu/NGh6RM9u2qGfqp+Nw==", + "dependencies": { + "@rc-component/util": "^1.2.1", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/switch": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rc-component/switch/-/switch-1.0.3.tgz", + "integrity": "sha512-Jgi+EbOBquje/XNdofr7xbJQZPYJP+BlPfR0h+WN4zFkdtB2EWqEfvkXJWeipflwjWip0/17rNbxEAqs8hVHfw==", + "dependencies": { + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/table": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/@rc-component/table/-/table-1.10.2.tgz", + "integrity": "sha512-b3PjqB9Gp25p5t/zq+9QrbXbodkptT8/zvLmwgd2FNPUUtaYyDnQqfxeD5a7ao8E8lpinLHsi2u2vdfPhyNvAw==", + "dependencies": { + "@rc-component/context": "^2.0.1", + "@rc-component/resize-observer": "^1.0.0", + "@rc-component/util": "^1.11.1", + "@rc-component/virtual-list": "^1.0.1", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/tabs": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@rc-component/tabs/-/tabs-1.9.1.tgz", + "integrity": "sha512-6mY08Fce6aNOHuGsxbzT+f2ekgL9mg1cGGHkittMlVGymjGg+kGupu5v90sRxcUd/paRU9jclLLXtF/PkK1FUA==", + "dependencies": { + "@rc-component/dropdown": "~1.0.0", + "@rc-component/menu": "~1.3.0", + "@rc-component/motion": "^1.1.3", + "@rc-component/resize-observer": "^1.0.0", + "@rc-component/util": "^1.11.1", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/tooltip": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@rc-component/tooltip/-/tooltip-1.4.0.tgz", + "integrity": "sha512-8Rx5DCctIlLI4raR0I0xHjVTf1aF48+gKCNeAAo5bmF5VoR5YED+A/XEqzXv9KKqrJDRcd3Wndpxh2hyzrTtSg==", + "dependencies": { + "@rc-component/trigger": "^3.7.1", + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/tour": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@rc-component/tour/-/tour-2.4.0.tgz", + "integrity": "sha512-aui4r4TqmTzwaBgcQxHYep8kM8PTjZFufjokObpy35KfFeZ0k9ArquWFZqegQlH24P14t+F0qO0mGTgzlav1yg==", + "dependencies": { + "@rc-component/portal": "^2.2.0", + "@rc-component/trigger": "^3.0.0", + "@rc-component/util": "^1.7.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/tree": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@rc-component/tree/-/tree-1.3.2.tgz", + "integrity": "sha512-bJFj46wEkpBPnWyTm18XmgAgNQ/4YvprxMOPPY2a6rmhGJYxLuNKEFiL5Qej4Qctu9wHJm8WW+v2SYskafE0kA==", + "dependencies": { + "@rc-component/motion": "^1.0.0", + "@rc-component/util": "^1.11.1", + "@rc-component/virtual-list": "^1.2.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=10.x" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/@rc-component/tree-select": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@rc-component/tree-select/-/tree-select-1.9.0.tgz", + "integrity": "sha512-GXcFe15a+trUl1/J3OHWQhsVWFpwFpGFK2cqYWZ1sK22Zs3KZTvMwDpzr75PIo1s6QVioVxpE/pRwRopkeDQ6w==", + "dependencies": { + "@rc-component/select": "~1.6.0", + "@rc-component/tree": "~1.3.0", + "@rc-component/util": "^1.4.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/@rc-component/trigger": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/@rc-component/trigger/-/trigger-3.9.1.tgz", + "integrity": "sha512-LNsYvz60mrLJ/kRvKcHE7boUvcQfVMCfRqZ71x3Fo9AOiZ1KKIEqkzMA8DNvz2V3Bcvir/vwQNn7JF1NPODQ7Q==", + "dependencies": { + "@rc-component/motion": "^1.1.4", + "@rc-component/portal": "^2.2.0", + "@rc-component/resize-observer": "^1.1.1", + "@rc-component/util": "^1.2.1", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/upload": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@rc-component/upload/-/upload-1.1.1.tgz", + "integrity": "sha512-GvYWSKeaJTOxxC5p6+nOSadzfvXA1h8C/iHFPFZX+szH3JUXrvs+DLiW8YUTBgvMh8m63mJeHrlYlJzAlg+pDA==", + "dependencies": { + "@rc-component/util": "^1.11.1", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/util": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@rc-component/util/-/util-1.11.1.tgz", + "integrity": "sha512-awVlI3ub2vqfqkYxOBc/uQ0efm3jw0wcrhtO/YWLyZfxiKXczKwNbVuhlnyxytDt7H9pbbVQiqr+O6MLATtRYg==", + "dependencies": { + "is-mobile": "^5.0.0", + "react-is": "^18.2.0" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/virtual-list": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@rc-component/virtual-list/-/virtual-list-1.2.0.tgz", + "integrity": "sha512-iavRm1Jo4GDbASQwdGa7jFyk93RvSOo9xHyBT4QL1pgFJj/Fdf1G+3RErH7/7BmAMvx2AkF62mjGYxDbXsK9TQ==", + "dependencies": { + "@babel/runtime": "^7.20.0", + "@rc-component/resize-observer": "^1.0.1", + "@rc-component/util": "^1.4.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.61.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.61.0.tgz", + "integrity": "sha512-dnxczajOqt0gesZlN5pGQ1s1imQVrsmCw5G2Ci4oM+0WvNz3pyRnlWrT7McoZIb8VlFwCawdmbWRmxRn7HI+VQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.61.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.61.0.tgz", + "integrity": "sha512-Bp3JpGP00Vu3f238ivRrjf7z3xSzVPXqCmaJYA9t2c+c8vKYvOzmXF7LkkeUalTEGd6cZcSWe+PFIP3Vy48fRg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.61.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.61.0.tgz", + "integrity": "sha512-zaYIpr670mUmmZ1tVzUFplbQbG7h3Gugx3L5FoqhsC2m/YnLlR1a7zVLmXNPy+iY1tFPEbNG+HHBXZGyId0G5w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.61.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.61.0.tgz", + "integrity": "sha512-+P49fvkv2dSoeevUW+lgZ/I2JHSsJCK1Lyjj7Cu6E4UHG4tS9XIefzIjo5qhgELjAclnen1rLzK2PMKJdo+Dyg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.61.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.61.0.tgz", + "integrity": "sha512-l3FAAOyKJXH2ea6KNFN+MMgC/rnE94YGLXs2ehYqDcCoHt1DpvgWX75BhUJxN38XojP7Ul+4H8PRn7EdyqSDrw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.61.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.61.0.tgz", + "integrity": "sha512-VokPN3TSctKj65cyCNPaUh4vMFA8awxOot/0sp+4J7ZlNRKQEhXhawqPwajoi8H5ZFt61i0ugZJuTKXBjGJ17Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.61.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.61.0.tgz", + "integrity": "sha512-DxH0P3wxm+Yzs/p3zrk9dw1rURu8p0Nv5+MRK/L7OtnLNg5rLZraSBFZ8iUXOd9f2BlhJyEpIZUH/emjq4UJ4g==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.61.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.61.0.tgz", + "integrity": "sha512-T6ZvMNe84kAz6TBWHC7hGAoEtzP1LWYw/AqayGWEF6uISt3Abk/st06LqRD9THd7Xz3NxzurUpzAuEAUbZf+nw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.61.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.61.0.tgz", + "integrity": "sha512-q/4hzvQkDs8b4jIBab1pnLiiM0ayTZsN2amBFPDzuyZxjEd4wDwx0UJFYM3cOZzSf5Kw8fnWSprJzIBMkcR44Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.61.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.61.0.tgz", + "integrity": "sha512-vvYWX3akdEAY6km+9wAqFDnk6pQsbJKVnj7xawcvs/+fdlYBGp+U+Qq/lLfpIxYIZvZLHMAKD9HLdacSx/r3dw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.61.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.61.0.tgz", + "integrity": "sha512-DePa5cqOxDP/Zp0VOXpeWaGew5iIv5DXp9NYbzkX5PFQyWVX9184WCTh3hvr/7lhXo8ZVlbFLkz8+o/q1dU6gA==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.61.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.61.0.tgz", + "integrity": "sha512-LV8aWMB8UChglMCEzs7RkN0GsH29RJaLLqwm9fCIjlqwxQTiWAqNcc7wjBkH31hV0PU/yVxGYvrYsgfea2qw6g==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.61.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.61.0.tgz", + "integrity": "sha512-QoNSnwQtaeNu5grdBbsL0tt1uyl5EnS8DA8Mr3nluMXbhdQNyhN+G4tBax7VCdxLKj8YJ0/4OO9Ho84jMnJtKA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.61.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.61.0.tgz", + "integrity": "sha512-/zZp5MKapIIApE8trN8qLGNSiRN9TUoaUZ1cmVu4XnVdd5LQLOXTtyi+vtfUbNnT3iyjzpPqYeKXmvJ+gJGYWw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.61.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.61.0.tgz", + "integrity": "sha512-RbrzcD3aJ1k3UbtMRRBNwojdVVyXjuVAFTfn/xPa6EEl6GE9Sm/akPgFTb9aAC9pMKGJ6CtWxaGrqWcabH+ySg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.61.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.61.0.tgz", + "integrity": "sha512-ZF+onDsBso8PJf1XaG9lB+O9RnBpKGnY6OrzC4CSHrtC1jb6jWLTKK4bRqdoCXHd22gyr2hiYmEAm8Wns/BOCw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.61.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.61.0.tgz", + "integrity": "sha512-Atk0aSIk5Zx2Wuh9dgRQgLP0Koc8hOeYpbWryMXyk8G8/HmPkwPPkMqIIDhrXHHYqfUzSJA/I7IWSBv8xSmRBA==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.61.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.61.0.tgz", + "integrity": "sha512-0uMOcf3eZ5K+K4cYHkdxShFMPlPXCOdfDFEFn9dNYAEEd2cVvmOfH7zFgRVoDgmtQ1m9k5q7qfrHzyMAubKYUA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.61.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.61.0.tgz", + "integrity": "sha512-mvFtE4A/t/7hRJ7X8Ozmu8FsIkAUat2nzl12pgU337BRmq87AQUJztwHz2Zv5/tjo9/C95E66CK03SI/ToEDJw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.61.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.61.0.tgz", + "integrity": "sha512-z9b9+aTxvt8n2rNltMPvyaUfB8NJ+CVyOrGK/MdIKHx7B+lXmZpm/XbRsU7Rpf3fRqJ2uS6mBJiJveCtq8LHDg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.61.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.61.0.tgz", + "integrity": "sha512-jXaXFqKMehsOc+g8R6oo33RRC6w07G9jDBxAE5eAKX7mOcCbZloYIPNhfG9Wl+P9O9IWHFO4OJgPi1Ml2qkt7w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.61.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.61.0.tgz", + "integrity": "sha512-OXNWVFocS2IA4+QplhTZZ2a+8hPZR7T8KuozsNmJKK8y7cp83StHvGksfHzPG3wczWTczyWHVQuqeiTUbjiyBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.61.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.61.0.tgz", + "integrity": "sha512-AlAbNtBO637LxSldqV43z0FfXoGfl2TW1DgAg/bs7aQswFbDewz2SJm3BUhiGfbOVtW571xbc9p+REdxhyN/Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.61.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.61.0.tgz", + "integrity": "sha512-QRSrQXyJ1M4tjNXdR0/G/IgV6lzfQQJYBjlWIEYkY2Xs86DRl/iEpQ4blMDjJxSl7n19eDKKXMg0AmuBVYy8pQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.61.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.61.0.tgz", + "integrity": "sha512-tkuFxhvKO/HlGd0VsINF6vHSYH8AF8W0TcNxKDK6JZmrehngFj78pToc8iemtnvwilDjs2G/qSzYFhe9U8q+fw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tauri-apps/api": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.11.0.tgz", + "integrity": "sha512-7CinYODhky9lmO23xHnUFv0Xt43fbtWMyxZcLcRBlFkcgXKuEirBvHpmtJ89YMhyeGcq20Wuc47Fa4XjyniywA==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + } + }, + "node_modules/@tauri-apps/cli": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.11.2.tgz", + "integrity": "sha512-bk3HemqvGRoy+5D/dVMUQHKMYLglD0jVnMm/0iGMH6ufZ+p8r14m6BpIixwij3PBvZdvORUp1YifTD8QxVZ1Nw==", + "dev": true, + "bin": { + "tauri": "tauri.js" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + }, + "optionalDependencies": { + "@tauri-apps/cli-darwin-arm64": "2.11.2", + "@tauri-apps/cli-darwin-x64": "2.11.2", + "@tauri-apps/cli-linux-arm-gnueabihf": "2.11.2", + "@tauri-apps/cli-linux-arm64-gnu": "2.11.2", + "@tauri-apps/cli-linux-arm64-musl": "2.11.2", + "@tauri-apps/cli-linux-riscv64-gnu": "2.11.2", + "@tauri-apps/cli-linux-x64-gnu": "2.11.2", + "@tauri-apps/cli-linux-x64-musl": "2.11.2", + "@tauri-apps/cli-win32-arm64-msvc": "2.11.2", + "@tauri-apps/cli-win32-ia32-msvc": "2.11.2", + "@tauri-apps/cli-win32-x64-msvc": "2.11.2" + } + }, + "node_modules/@tauri-apps/cli-darwin-arm64": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.11.2.tgz", + "integrity": "sha512-+4UZzLt+eOAEQCwgd+TqKgyUJMrvx+BgdXLLaqJYmPqzP+nE6YZr/hY6CWLYGQb8jFn99jEkmC6uA3tNvamA1w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-darwin-x64": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.11.2.tgz", + "integrity": "sha512-VjYYtZUPqDMLutSfJEyxFE3Bz+DPi7c8wC3imckgvciLDZLq4qwKJxBicg0BXGhXjJsl8vKWgWRFNMPELQ+Xyg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm-gnueabihf": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.11.2.tgz", + "integrity": "sha512-yMemD6f4i95AQriS8EazyOFzbE34yjnP16i3IOzpHGQvBoy2DjypFMFBq0NtPuITURv/cOGguRtHR5d79/9CSA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-gnu": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.11.2.tgz", + "integrity": "sha512-cgI91D2wL8GSgoWwZXDqt+DwnuZCP2/bz03QAE4TrhgAKIsrB4hX26W/H1EONPUUNkqrsgeCD0wU6pcNjV/5kw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-musl": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.11.2.tgz", + "integrity": "sha512-X1rm0BERqAAggtYTESSgXrS3sz4Sb/OiPiz54UqISlXW+GkR3vNIGnsy/lejNmoXGVqri3Q53BCfQiclOIyRPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-riscv64-gnu": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.11.2.tgz", + "integrity": "sha512-usbMLJbT3KtkOrBMDVeGYNM35aTHXx38SJSzTMSqqjeUIOQ+iVPjb2yAGNAE+KqmBbAx4FOFIyMeKXx2M/JKGQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-gnu": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.11.2.tgz", + "integrity": "sha512-Ru4gwJKPG0ctVGchRGpRup4Y4lW2SSfFnrbQcyHhCliKy4g8Qz97TrUgCur4CbWyAgKxvGh3SjrkA0LDYzDGiw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-musl": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.11.2.tgz", + "integrity": "sha512-eUm7T6clN1MMmNSRQ9gaWsQdyehQx2Gmn5hht/QUlqZQI/qcP2OJK5dnaxqwFzCr2HdsEo9ydxaqcS1oJzMvUw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-arm64-msvc": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.11.2.tgz", + "integrity": "sha512-HeeZW80jU+gVTOEX4X/hC6NVSAdDVXajwP5fxIZ/3z9WvUC7qrudX2GMTilYq6Dg0e0sk0XgsAJD1hZ5wPBXUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-ia32-msvc": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.11.2.tgz", + "integrity": "sha512-YhjQNZcXfbkCLyazSv1nPnJ9iRFE1wm6kc51FDbU10/Dk09io+6PAGMLjkxnX2GdM0qMnDmTjstY8mTDVvtKeA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-x64-msvc": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.11.2.tgz", + "integrity": "sha512-d2JchlFIpZevZVReyqhQOekJmb1UH3rhZ5VX6sH3ty9ETE0TKQavpihvoScUXfKKpW6HZC0MrFGRU0ZtD+w3gA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/plugin-dialog": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.7.1.tgz", + "integrity": "sha512-OK1UBXYt+ojcmxMktzzuyonYIFta8CmAASpX+CA+DTGK24KlHjhYI6x2iOJ/TjZF4N7/ACK1oFmEOjIY9IhzOQ==", + "dependencies": { + "@tauri-apps/api": "^2.11.0" + } + }, + "node_modules/@tauri-apps/plugin-notification": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-notification/-/plugin-notification-2.3.3.tgz", + "integrity": "sha512-Zw+ZH18RJb41G4NrfHgIuofJiymusqN+q8fGUIIV7vyCH+5sSn5coqRv/MWB9qETsUs97vmU045q7OyseCV3Qg==", + "dependencies": { + "@tauri-apps/api": "^2.8.0" + } + }, + "node_modules/@tauri-apps/plugin-shell": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-shell/-/plugin-shell-2.3.5.tgz", + "integrity": "sha512-jewtULhiQ7lI7+owCKAjc8tYLJr92U16bPOeAa472LHJdgaibLP83NcfAF2e+wkEcA53FxKQAZ7byDzs2eeizg==", + "dependencies": { + "@tauri-apps/api": "^2.10.1" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/d3": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==" + }, + "node_modules/@types/d3-axis": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-brush": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-chord": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==" + }, + "node_modules/@types/d3-contour": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==" + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz", + "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-dsv": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==" + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==" + }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==" + }, + "node_modules/@types/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==" + }, + "node_modules/@types/d3-polygon": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==" + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==" + }, + "node_modules/@types/d3-random": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz", + "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==" + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==" + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==" + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==" + }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==" + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==" + }, + "node_modules/@types/react": { + "version": "18.3.30", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.30.tgz", + "integrity": "sha512-3ek6mwJL5/VBewBcY4S66cqlCtK3qi4WIq37Z0m/NHw1hjhI7274Mx1qz/+ggSzyBCOEf7eHjBN6INjPAWYfYw==", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "optional": true + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.1.tgz", + "integrity": "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==" + }, + "node_modules/@upsetjs/venn.js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@upsetjs/venn.js/-/venn.js-2.0.0.tgz", + "integrity": "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==", + "optionalDependencies": { + "d3-selection": "^3.0.0", + "d3-transition": "^3.0.1" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@vitest/expect": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", + "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "dev": true, + "dependencies": { + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", + "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "dev": true, + "dependencies": { + "@vitest/spy": "2.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "dev": true, + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", + "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "dev": true, + "dependencies": { + "@vitest/utils": "2.1.9", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "dev": true, + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", + "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "dev": true, + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "dev": true, + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@xterm/addon-fit": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.11.0.tgz", + "integrity": "sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g==" + }, + "node_modules/antd": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/antd/-/antd-6.4.3.tgz", + "integrity": "sha512-6H2avkxCGfxcF67r3J2mwm9Ck50el1pks/73vfM1wDsPL/tPtj5vHuauMgJFnrqmq7CH3g8aoZ0VBQbt+jpAsw==", + "dependencies": { + "@ant-design/colors": "^8.0.1", + "@ant-design/cssinjs": "^2.1.2", + "@ant-design/cssinjs-utils": "^2.1.2", + "@ant-design/fast-color": "^3.0.1", + "@ant-design/icons": "^6.2.3", + "@ant-design/react-slick": "~2.0.0", + "@babel/runtime": "^7.29.2", + "@rc-component/cascader": "~1.15.0", + "@rc-component/checkbox": "~2.0.0", + "@rc-component/collapse": "~1.2.0", + "@rc-component/color-picker": "~3.1.1", + "@rc-component/dialog": "~1.9.0", + "@rc-component/drawer": "~1.4.2", + "@rc-component/dropdown": "~1.0.2", + "@rc-component/form": "~1.8.1", + "@rc-component/image": "~1.9.0", + "@rc-component/input": "~1.3.0", + "@rc-component/input-number": "~1.6.2", + "@rc-component/mentions": "~1.9.0", + "@rc-component/menu": "~1.3.0", + "@rc-component/motion": "^1.3.2", + "@rc-component/mutate-observer": "^2.0.1", + "@rc-component/notification": "~2.0.7", + "@rc-component/pagination": "~1.2.0", + "@rc-component/picker": "~1.10.0", + "@rc-component/progress": "~1.0.2", + "@rc-component/qrcode": "~1.1.1", + "@rc-component/rate": "~1.0.1", + "@rc-component/resize-observer": "^1.1.2", + "@rc-component/segmented": "~1.3.0", + "@rc-component/select": "~1.6.15", + "@rc-component/slider": "~1.0.1", + "@rc-component/steps": "~1.2.2", + "@rc-component/switch": "~1.0.3", + "@rc-component/table": "~1.10.0", + "@rc-component/tabs": "~1.9.0", + "@rc-component/tooltip": "~1.4.0", + "@rc-component/tour": "~2.4.0", + "@rc-component/tree": "~1.3.1", + "@rc-component/tree-select": "~1.9.0", + "@rc-component/trigger": "^3.9.0", + "@rc-component/upload": "~1.1.0", + "@rc-component/util": "^1.11.0", + "clsx": "^2.1.1", + "dayjs": "^1.11.11", + "scroll-into-view-if-needed": "^3.1.0", + "throttle-debounce": "^5.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ant-design" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/autoprefixer": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.0.tgz", + "integrity": "sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "browserslist": "^4.28.2", + "caniuse-lite": "^1.0.30001787", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.33", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.33.tgz", + "integrity": "sha512-bA6+tcSLpz2tIEdDXZPpPTIuxBcC4+w6SieaYyfigIa4h8GlFxbA17v22Vx3JUtuZQj9SgOsnbK+aTBzyDyEuw==", + "dev": true, + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "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" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001793", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz", + "integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "engines": { + "node": ">= 16" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/compute-scroll-into-view": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.1.tgz", + "integrity": "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cose-base": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz", + "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==", + "dependencies": { + "layout-base": "^1.0.0" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==" + }, + "node_modules/cytoscape": { + "version": "3.33.4", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.4.tgz", + "integrity": "sha512-HIN5Pmd9MrX9BkV7tDwnOcEJCSFvCpc8X97h3f508J6I5FsqAY65wKOCvgH2CuP42CaahWaz4tuh32SOOIH7ww==", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/cytoscape-cose-bilkent": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz", + "integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==", + "dependencies": { + "cose-base": "^1.0.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz", + "integrity": "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==", + "dependencies": { + "cose-base": "^2.2.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/cose-base": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-2.2.0.tgz", + "integrity": "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==", + "dependencies": { + "layout-base": "^2.0.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/layout-base": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-2.0.1.tgz", + "integrity": "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==" + }, + "node_modules/d3": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-sankey": { + "version": "0.12.3", + "resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz", + "integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==", + "dependencies": { + "d3-array": "1 - 2", + "d3-shape": "^1.2.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-array": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", + "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", + "dependencies": { + "internmap": "^1.0.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-path": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==" + }, + "node_modules/d3-sankey/node_modules/d3-shape": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "dependencies": { + "d3-path": "1" + } + }, + "node_modules/d3-sankey/node_modules/internmap": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", + "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==" + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dagre-d3-es": { + "version": "7.0.14", + "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.14.tgz", + "integrity": "sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg==", + "dependencies": { + "d3": "^7.9.0", + "lodash-es": "^4.17.21" + } + }, + "node_modules/dayjs": { + "version": "1.11.21", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.21.tgz", + "integrity": "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decode-named-character-reference": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", + "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/delaunator": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.1.0.tgz", + "integrity": "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true + }, + "node_modules/diff-match-patch": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz", + "integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true + }, + "node_modules/dompurify": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.7.tgz", + "integrity": "sha512-2jBxDJY4RR06tQNy4w5FlFH7kfxsQZlufd0sbv+chfHCxeJwrFw2baUDsSwvBISD4K4RDbd0PTfy3uNXsR6siA==", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.364", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.364.tgz", + "integrity": "sha512-G/dYE3+AYhyHwzTwg8UbnXf7zqMERYh7l2jJ3QujhFsH8agSYwtnGAR2aZ7f0AakIKJXd5En/Hre4igIUrdlYw==", + "dev": true + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true + }, + "node_modules/es-toolkit": { + "version": "1.47.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.47.0.tgz", + "integrity": "sha512-n1GuoD0WEQZMBk5tttoZSqwgyLx01oqa5XsBmCHwPyNe1S9jPBEmtR2pSgp2kJuWE3ciFZ6yRHmY4pM4C3OOkw==" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/gitdiff-parser": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/gitdiff-parser/-/gitdiff-parser-0.3.1.tgz", + "integrity": "sha512-YQJnY8aew65id8okGxKCksH3efDCJ9HzV7M9rsvd65habf39Pkh4cgYJ27AaoDMqo1X98pgNJhNMrm/kpV7UVQ==" + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/hachure-fill": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/hachure-fill/-/hachure-fill-0.5.2.tgz", + "integrity": "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==" + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hast-util-parse-selector": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz", + "integrity": "sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", + "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" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-6.0.0.tgz", + "integrity": "sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==", + "dependencies": { + "@types/hast": "^2.0.0", + "comma-separated-tokens": "^1.0.0", + "hast-util-parse-selector": "^2.0.0", + "property-information": "^5.0.0", + "space-separated-tokens": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript/node_modules/@types/hast": { + "version": "2.3.10", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.10.tgz", + "integrity": "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/hastscript/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==" + }, + "node_modules/hastscript/node_modules/comma-separated-tokens": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz", + "integrity": "sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/hastscript/node_modules/property-information": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-5.6.0.tgz", + "integrity": "sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==", + "dependencies": { + "xtend": "^4.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/hastscript/node_modules/space-separated-tokens": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz", + "integrity": "sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/html-url-attributes": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", + "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/import-meta-resolve": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz", + "integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/inline-style-parser": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", + "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==" + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "engines": { + "node": ">=12" + } + }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-mobile": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-mobile/-/is-mobile-5.0.0.tgz", + "integrity": "sha512-Tz/yndySvLAEXh+Uk8liFCxOwVH6YutuR74utvOcu7I9Di+DwM0mtdPVZNaVvvBUM2OXxne/NhOs1zAO7riusQ==" + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json2mq": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/json2mq/-/json2mq-0.2.0.tgz", + "integrity": "sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA==", + "dependencies": { + "string-convert": "^0.2.0" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/katex": { + "version": "0.16.47", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.47.tgz", + "integrity": "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/katex/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "engines": { + "node": ">= 12" + } + }, + "node_modules/khroma": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz", + "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==" + }, + "node_modules/layout-base": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz", + "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==" + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==" + }, + "node_modules/lodash-es": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==" + }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.577.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.577.0.tgz", + "integrity": "sha512-4LjoFv2eEPwYDPg/CUdBJQSDfPyzXCRrVW1X7jrx/trgxnxkHFjnVZINbzvzxjN70dxychOfg+FTYwBiS3pQ5A==", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/marked": { + "version": "16.4.2", + "resolved": "https://registry.npmjs.org/marked/-/marked-16.4.2.tgz", + "integrity": "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", + "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", + "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" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", + "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", + "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" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", + "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", + "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" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", + "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" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", + "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" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "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" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "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" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "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" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "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" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "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" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "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" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/mermaid": { + "version": "11.15.0", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.15.0.tgz", + "integrity": "sha512-pTMbcf3rWdtLiYGpmoTjHEpeY8seiy6sR+9nD7LOs8KfUbHE4lOUAprTRqRAcWSQ6MQpdX+YEsxShtGsINtPtw==", + "dependencies": { + "@braintree/sanitize-url": "^7.1.1", + "@iconify/utils": "^3.0.2", + "@mermaid-js/parser": "^1.1.1", + "@types/d3": "^7.4.3", + "@upsetjs/venn.js": "^2.0.0", + "cytoscape": "^3.33.1", + "cytoscape-cose-bilkent": "^4.1.0", + "cytoscape-fcose": "^2.2.0", + "d3": "^7.9.0", + "d3-sankey": "^0.12.3", + "dagre-d3-es": "7.0.14", + "dayjs": "^1.11.19", + "dompurify": "^3.3.1", + "es-toolkit": "^1.45.1", + "katex": "^0.16.25", + "khroma": "^2.1.0", + "marked": "^16.3.0", + "roughjs": "^4.6.6", + "stylis": "^4.3.6", + "ts-dedent": "^2.2.0", + "uuid": "^11.1.0 || ^12 || ^13 || ^14.0.0" + } + }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "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" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "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" + } + }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "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" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "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" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "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" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "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" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", + "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", + "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" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "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" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "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" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "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" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "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" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.47", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.47.tgz", + "integrity": "sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/package-manager-detector": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.6.0.tgz", + "integrity": "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==" + }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "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" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==" + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==" + }, + "node_modules/path-data-parser": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz", + "integrity": "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==" + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/points-on-curve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/points-on-curve/-/points-on-curve-0.2.0.tgz", + "integrity": "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==" + }, + "node_modules/points-on-path": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/points-on-path/-/points-on-path-0.2.1.tgz", + "integrity": "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==", + "dependencies": { + "path-data-parser": "0.1.0", + "points-on-curve": "0.2.0" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, + "node_modules/prismjs": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.27.0.tgz", + "integrity": "sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/property-information": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.2.0.tgz", + "integrity": "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-diff-view": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/react-diff-view/-/react-diff-view-3.3.3.tgz", + "integrity": "sha512-CPveApk6n7ZbkW7T6PoptR7LWAvD9hohTHZ7WnKnu3GZkTfUB5rvg486apPo94iYVi4fZd3Nt+rtBZ5877exoQ==", + "dependencies": { + "classnames": "^2.3.2", + "diff-match-patch": "^1.0.5", + "gitdiff-parser": "^0.3.1", + "lodash": "^4.17.21", + "shallow-equal": "^3.1.0", + "warning": "^4.0.3" + }, + "peerDependencies": { + "react": ">=16.14.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" + }, + "node_modules/react-markdown": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz", + "integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==", + "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" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=18", + "react": ">=18" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.16.0.tgz", + "integrity": "sha512-wArC8lVyJb3+jM9OpDyW6hLCizACWkvQR/sSGqSs+o5uEXEtGlqdZ4v8hENR3Jad6i+LRkK93q/+bQAcvl6V1A==", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.16.0.tgz", + "integrity": "sha512-kMUAbimWB5FVbF4Bce4bJsiKJWLIUHq/mEG8+CFDnCSgltptBiG5nguducmsJeGKytlCvQud9Qhzpn49iduTlA==", + "dependencies": { + "react-router": "7.16.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/refractor": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/refractor/-/refractor-3.6.0.tgz", + "integrity": "sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA==", + "dependencies": { + "hastscript": "^6.0.0", + "parse-entities": "^2.0.0", + "prismjs": "~1.27.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/character-entities": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz", + "integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/character-entities-legacy": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz", + "integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/character-reference-invalid": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz", + "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/is-alphabetical": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz", + "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/is-alphanumerical": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz", + "integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==", + "dependencies": { + "is-alphabetical": "^1.0.0", + "is-decimal": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/is-decimal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz", + "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/is-hexadecimal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz", + "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/parse-entities": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz", + "integrity": "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==", + "dependencies": { + "character-entities": "^1.0.0", + "character-entities-legacy": "^1.0.0", + "character-reference-invalid": "^1.0.0", + "is-alphanumerical": "^1.0.0", + "is-decimal": "^1.0.0", + "is-hexadecimal": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/remark-gfm": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", + "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", + "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" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "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" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/robust-predicates": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz", + "integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==" + }, + "node_modules/rollup": { + "version": "4.61.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.61.0.tgz", + "integrity": "sha512-T9mWdbWfQtp0B5lv/HX+wrhYsmXRlcWnXXmJbXqKJhlRaoS6KMhq0gpyzW4UJfclcxrEdLnTgjT2NjruLONu0g==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.61.0", + "@rollup/rollup-android-arm64": "4.61.0", + "@rollup/rollup-darwin-arm64": "4.61.0", + "@rollup/rollup-darwin-x64": "4.61.0", + "@rollup/rollup-freebsd-arm64": "4.61.0", + "@rollup/rollup-freebsd-x64": "4.61.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.61.0", + "@rollup/rollup-linux-arm-musleabihf": "4.61.0", + "@rollup/rollup-linux-arm64-gnu": "4.61.0", + "@rollup/rollup-linux-arm64-musl": "4.61.0", + "@rollup/rollup-linux-loong64-gnu": "4.61.0", + "@rollup/rollup-linux-loong64-musl": "4.61.0", + "@rollup/rollup-linux-ppc64-gnu": "4.61.0", + "@rollup/rollup-linux-ppc64-musl": "4.61.0", + "@rollup/rollup-linux-riscv64-gnu": "4.61.0", + "@rollup/rollup-linux-riscv64-musl": "4.61.0", + "@rollup/rollup-linux-s390x-gnu": "4.61.0", + "@rollup/rollup-linux-x64-gnu": "4.61.0", + "@rollup/rollup-linux-x64-musl": "4.61.0", + "@rollup/rollup-openbsd-x64": "4.61.0", + "@rollup/rollup-openharmony-arm64": "4.61.0", + "@rollup/rollup-win32-arm64-msvc": "4.61.0", + "@rollup/rollup-win32-ia32-msvc": "4.61.0", + "@rollup/rollup-win32-x64-gnu": "4.61.0", + "@rollup/rollup-win32-x64-msvc": "4.61.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/roughjs": { + "version": "4.6.6", + "resolved": "https://registry.npmjs.org/roughjs/-/roughjs-4.6.6.tgz", + "integrity": "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==", + "dependencies": { + "hachure-fill": "^0.5.2", + "path-data-parser": "^0.1.0", + "points-on-curve": "^0.2.0", + "points-on-path": "^0.2.1" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/scroll-into-view-if-needed": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz", + "integrity": "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==", + "dependencies": { + "compute-scroll-into-view": "^3.0.2" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==" + }, + "node_modules/shallow-equal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/shallow-equal/-/shallow-equal-3.1.0.tgz", + "integrity": "sha512-pfVOw8QZIXpMbhBWvzBISicvToTiM5WBF1EeAUZDDSb5Dt29yl4AYbyywbJFSEsRUMr7gJaxqCdr4L3tQf9wVg==" + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true + }, + "node_modules/string-convert": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/string-convert/-/string-convert-0.2.1.tgz", + "integrity": "sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==" + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/style-to-js": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", + "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==", + "dependencies": { + "style-to-object": "1.0.14" + } + }, + "node_modules/style-to-object": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", + "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", + "dependencies": { + "inline-style-parser": "0.2.7" + } + }, + "node_modules/stylis": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.4.0.tgz", + "integrity": "sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==" + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/sucrase/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.19", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", + "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "dev": true, + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tauri-pty": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tauri-pty/-/tauri-pty-0.2.1.tgz", + "integrity": "sha512-h2/uBWkgzO1fpdTS4hDz5LcDYjgmPdy2Renj9m2bukht1rxmHKmt/ciUo9wenK3uPrGBHrr2/QKGYrEmyAYwcw==", + "dependencies": { + "@tauri-apps/api": "^2.9.1" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/throttle-debounce": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.2.tgz", + "integrity": "sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==", + "engines": { + "node": ">=12.22" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true + }, + "node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/ts-dedent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz", + "integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==", + "engines": { + "node": ">=6.10" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "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" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "node_modules/uuid": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.0.tgz", + "integrity": "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", + "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", + "dev": true, + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", + "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "dev": true, + "dependencies": { + "@vitest/expect": "2.1.9", + "@vitest/mocker": "2.1.9", + "@vitest/pretty-format": "^2.1.9", + "@vitest/runner": "2.1.9", + "@vitest/snapshot": "2.1.9", + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.9", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.9", + "@vitest/ui": "2.1.9", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true + }, + "node_modules/warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/xterm": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/xterm/-/xterm-5.3.0.tgz", + "integrity": "sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg==", + "deprecated": "This package is now deprecated. Move to @xterm/xterm instead." + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/zustand": { + "version": "5.0.14", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.14.tgz", + "integrity": "sha512-/8tAspM5LMPr28b3fwLYrtdj77ECpfZviaP75CMTnwO8ISyaE4GDIG/9rDDYq/cH9D2Xw2A2RXglLInmVBQB/g==", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/apps/desktop/package.json b/apps/desktop/package.json new file mode 100644 index 00000000..26424417 --- /dev/null +++ b/apps/desktop/package.json @@ -0,0 +1,52 @@ +{ + "name": "supercoder-desktop", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build --mode production", + "preview": "vite preview", + "tauri": "npx tauri", + "tauri:dev": "APP_ENV=development npx tauri dev", + "tauri:build": "APP_ENV=production npx tauri build --bundles app", + "test": "vitest run", + "test:watch": "vitest" + }, + "dependencies": { + "@tauri-apps/api": "^2.0.0", + "@tauri-apps/plugin-dialog": "^2.6.0", + "@tauri-apps/plugin-notification": "^2.3.3", + "@tauri-apps/plugin-shell": "^2.0.0", + "@xterm/addon-fit": "^0.11.0", + "antd": "^6.3.2", + "buffer": "^6.0.3", + "dayjs": "^1.11.20", + "gitdiff-parser": "^0.3.1", + "lucide-react": "^0.577.0", + "mermaid": "^11.13.0", + "path-browserify": "^1.0.1", + "react": "^18.3.1", + "react-diff-view": "^3.3.3", + "react-dom": "^18.3.1", + "react-markdown": "^10.1.0", + "react-router-dom": "^7.13.1", + "refractor": "^3.6.0", + "remark-gfm": "^4.0.1", + "tauri-pty": "^0.2.1", + "xterm": "^5.3.0", + "zustand": "^5.0.11" + }, + "devDependencies": { + "@tauri-apps/cli": "^2.0.0", + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.1", + "autoprefixer": "^10.4.19", + "postcss": "^8.4.39", + "tailwindcss": "^3.4.4", + "typescript": "^5.5.3", + "vite": "^5.3.3", + "vitest": "^2.1.9" + } +} diff --git a/apps/desktop/postcss.config.js b/apps/desktop/postcss.config.js new file mode 100644 index 00000000..2aa7205d --- /dev/null +++ b/apps/desktop/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml new file mode 100644 index 00000000..a5ed6427 --- /dev/null +++ b/apps/desktop/src-tauri/Cargo.toml @@ -0,0 +1,54 @@ +[package] +name = "supercoder-agent-desktop" +version = "0.1.0" +description = "SuperCoder" +authors = [] +edition = "2021" +rust-version = "1.70" + +[lib] +name = "supercoder_lib" +crate-type = ["staticlib", "cdylib", "rlib"] + +[build-dependencies] +tauri-build = { version = "2.0", features = [] } +dotenvy = "0.15" + +[dependencies] +tauri = { version = "2.10", features = ["macos-private-api", "devtools"] } +tauri-plugin-shell = "2.0" +tauri-plugin-pty = "0.2" +tauri-plugin-notification = "2" +tauri-plugin-dialog = "2" +tauri-plugin-single-instance = "2" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +reqwest = { version = "0.12", features = ["json", "multipart", "stream"] } +base64 = "0.22" +tokio = { version = "1", features = ["fs", "rt", "time", "macros", "test-util"] } +uuid = { version = "1", features = ["v4"] } +rusqlite = { version = "0.31", features = ["bundled"] } +parking_lot = "0.12" +log = "0.4" +dirs = "5" +url = "2" +env_logger = "0.11" +git-ops = { path = "../../../crates/git-ops" } +agent = { path = "../../../crates/agent" } +dotenvy = "0.15" +thiserror = "2" +chrono = "0.4" +async-trait = "0.1" +tokio-util = "0.7" +tempfile = "3" +sha2 = "0.10" +flate2 = "1" +ignore = "0.4" +notify = "6" + +[dev-dependencies] +wiremock = "0.6" + +[features] +default = ["custom-protocol"] +custom-protocol = ["tauri/custom-protocol"] diff --git a/apps/desktop/src-tauri/Entitlements.plist b/apps/desktop/src-tauri/Entitlements.plist new file mode 100644 index 00000000..4ed79d96 --- /dev/null +++ b/apps/desktop/src-tauri/Entitlements.plist @@ -0,0 +1,14 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.network.client + + com.apple.security.network.server + + com.apple.security.cs.allow-unsigned-executable-memory + + + diff --git a/apps/desktop/src-tauri/Info.plist b/apps/desktop/src-tauri/Info.plist new file mode 100644 index 00000000..42b53b7f --- /dev/null +++ b/apps/desktop/src-tauri/Info.plist @@ -0,0 +1,8 @@ + + + + + NSAccessibilityUsageDescription + SuperCoder needs accessibility access to detect active editor and terminal windows. + + diff --git a/apps/desktop/src-tauri/build.rs b/apps/desktop/src-tauri/build.rs new file mode 100644 index 00000000..f30f9af9 --- /dev/null +++ b/apps/desktop/src-tauri/build.rs @@ -0,0 +1,28 @@ +fn main() { + // Load environment-specific .env file (shared with Vite frontend). + // APP_ENV is set by npm scripts or defaults to "development". + let mode = std::env::var("APP_ENV").unwrap_or_else(|_| "development".to_string()); + let env_file = format!("../.env.{}", mode); + + // Load mode-specific file first, then base .env as fallback + let _ = dotenvy::from_filename(&env_file); + let _ = dotenvy::from_filename("../.env"); + + // Forward VITE_* config vars to the Rust compiler so they're + // available via env!() macro at compile time + let config_keys = ["VITE_APP_ENV", "LLM_BASE_URL"]; + + for key in &config_keys { + if let Ok(val) = std::env::var(key) { + println!("cargo:rustc-env={}={}", key, val); + } + } + + // Re-run build.rs if env files change + println!("cargo:rerun-if-changed=../.env"); + println!("cargo:rerun-if-changed=../.env.{}", mode); + println!("cargo:rerun-if-changed=../.env.local"); + println!("cargo:rerun-if-env-changed=APP_ENV"); + + tauri_build::build() +} diff --git a/apps/desktop/src-tauri/capabilities/default.json b/apps/desktop/src-tauri/capabilities/default.json new file mode 100644 index 00000000..8a3f9b0f --- /dev/null +++ b/apps/desktop/src-tauri/capabilities/default.json @@ -0,0 +1,14 @@ +{ + "identifier": "default", + "description": "Default capabilities for SuperCoder", + "windows": ["main"], + "permissions": [ + "core:default", + "shell:allow-open", + "shell:allow-stdin-write", + "shell:allow-kill", + "pty:default", + "notification:default", + "dialog:default" + ] +} diff --git a/apps/desktop/src-tauri/examples/ce_smoke.rs b/apps/desktop/src-tauri/examples/ce_smoke.rs new file mode 100644 index 00000000..4acac650 --- /dev/null +++ b/apps/desktop/src-tauri/examples/ce_smoke.rs @@ -0,0 +1,103 @@ +//! Headless end-to-end smoke test for the context-engine integration. +//! +//! Runs the REAL app code against a running stack: +//! 1. context_watcher::streamer::Streamer.full_sync — index a repo (the same +//! streaming code the file watcher runs), +//! 2. ContextEngineClient.{index_status,search,graph_query} — the exact calls +//! the agent's codebase_search / codebase_graph tools make. +//! +//! Usage: cargo run --example ce_smoke -- [base_url] +//! Requires the stack up (docker compose up -d) with an embedding key set. + +use std::path::Path; +use std::sync::Arc; + +use agent::context_engine::{ContextEngineApi, ContextEngineClient, ContextEngineConfig}; +use supercoder_lib::context_watcher::ignore_filter::IgnoreFilter; +use supercoder_lib::context_watcher::streamer::{Streamer, StreamerConfig}; +use tokio_util::sync::CancellationToken; + +#[tokio::main] +async fn main() { + let mut args = std::env::args().skip(1); + let repo_arg = args.next().expect("usage: ce_smoke [base_url]"); + let base_url = args.next().unwrap_or_else(|| "http://127.0.0.1:8106".to_string()); + + // Canonicalize so index + query use the identical repo_path key. + let repo_path = std::fs::canonicalize(&repo_arg) + .expect("repo path must exist") + .to_string_lossy() + .to_string(); + + let machine_id = "smoke-harness".to_string(); + println!("== full_sync {repo_path} via {base_url} =="); + + // The streamer talks to {url}/index/... — pass the /api/v1 base (same as + // WatcherManager does when it builds the streamer). + let streamer = Streamer::new(StreamerConfig { + context_engine_url: format!("{base_url}/api/v1"), + user_id: "local".to_string(), + workspace_id: 0, + machine_id: machine_id.clone(), + repo_path: repo_path.clone(), + auth_token: String::new(), + }); + let filter = Arc::new(IgnoreFilter::new(Path::new(&repo_path))); + match streamer + .full_sync(Path::new(&repo_path), filter, CancellationToken::new()) + .await + { + Ok(stats) => println!( + " indexed: hashed={} uploaded={} deleted={} bytes={} dup_batches={} in {:?}", + stats.files_hashed, + stats.files_uploaded, + stats.files_deleted, + stats.bytes_uploaded, + stats.duplicate_batches, + stats.duration, + ), + Err(e) => { + println!(" full_sync ERROR: {e}"); + return; + } + } + + let client = ContextEngineClient::new(ContextEngineConfig { + base_url, + user_id: "local".to_string(), + workspace_id: 0, + machine_id, + repo_path, + auth_token: String::new(), + }); + + println!("\n== index_status =="); + match client.index_status().await { + Ok(s) => println!(" exists={} empty={} collection={:?} repo_id={:?}", s.exists, s.empty, s.collection_name, s.repo_id), + Err(e) => println!(" ERROR: {e}"), + } + + for q in ["create a git branch", "parse remote url", "restore checkpoint"] { + println!("\n== search: {q:?} =="); + match client.search(q, Some("multi"), Some(5)).await { + Ok(r) => { + println!(" total={} indexing={} message={:?}", r.total, r.indexing, r.message); + for (i, item) in r.results.iter().take(5).enumerate() { + println!(" [{i}] {} (lang={}, score={:.3}, src={})", item.file_path, item.language, item.score, item.source); + } + } + Err(e) => println!(" ERROR: {e}"), + } + } + + println!("\n== graph_query: dependencies of open_diff =="); + match client.graph_query(None, Some("open_diff"), None, Some("dependencies")).await { + Ok(r) => { + println!(" total={} indexing={} message={:?}", r.total, r.indexing, r.message); + for (i, item) in r.results.iter().take(8).enumerate() { + println!(" [{i}] {} @ {} (depth={}, dir={:?})", item.name, item.file_path, item.depth, item.direction); + } + } + Err(e) => println!(" ERROR: {e}"), + } +} diff --git a/apps/desktop/src-tauri/icons/128x128.png b/apps/desktop/src-tauri/icons/128x128.png new file mode 100644 index 00000000..e197936a Binary files /dev/null and b/apps/desktop/src-tauri/icons/128x128.png differ diff --git a/apps/desktop/src-tauri/icons/128x128@2x.png b/apps/desktop/src-tauri/icons/128x128@2x.png new file mode 100644 index 00000000..5e3fe10f Binary files /dev/null and b/apps/desktop/src-tauri/icons/128x128@2x.png differ diff --git a/apps/desktop/src-tauri/icons/32x32.png b/apps/desktop/src-tauri/icons/32x32.png new file mode 100644 index 00000000..42b17cc5 Binary files /dev/null and b/apps/desktop/src-tauri/icons/32x32.png differ diff --git a/apps/desktop/src-tauri/icons/64x64.png b/apps/desktop/src-tauri/icons/64x64.png new file mode 100644 index 00000000..9ff53f4f Binary files /dev/null and b/apps/desktop/src-tauri/icons/64x64.png differ diff --git a/apps/desktop/src-tauri/icons/Square107x107Logo.png b/apps/desktop/src-tauri/icons/Square107x107Logo.png new file mode 100644 index 00000000..b234aa04 Binary files /dev/null and b/apps/desktop/src-tauri/icons/Square107x107Logo.png differ diff --git a/apps/desktop/src-tauri/icons/Square142x142Logo.png b/apps/desktop/src-tauri/icons/Square142x142Logo.png new file mode 100644 index 00000000..e0335a49 Binary files /dev/null and b/apps/desktop/src-tauri/icons/Square142x142Logo.png differ diff --git a/apps/desktop/src-tauri/icons/Square150x150Logo.png b/apps/desktop/src-tauri/icons/Square150x150Logo.png new file mode 100644 index 00000000..525dc677 Binary files /dev/null and b/apps/desktop/src-tauri/icons/Square150x150Logo.png differ diff --git a/apps/desktop/src-tauri/icons/Square284x284Logo.png b/apps/desktop/src-tauri/icons/Square284x284Logo.png new file mode 100644 index 00000000..ef7a19e0 Binary files /dev/null and b/apps/desktop/src-tauri/icons/Square284x284Logo.png differ diff --git a/apps/desktop/src-tauri/icons/Square30x30Logo.png b/apps/desktop/src-tauri/icons/Square30x30Logo.png new file mode 100644 index 00000000..d25d7891 Binary files /dev/null and b/apps/desktop/src-tauri/icons/Square30x30Logo.png differ diff --git a/apps/desktop/src-tauri/icons/Square310x310Logo.png b/apps/desktop/src-tauri/icons/Square310x310Logo.png new file mode 100644 index 00000000..bafc540e Binary files /dev/null and b/apps/desktop/src-tauri/icons/Square310x310Logo.png differ diff --git a/apps/desktop/src-tauri/icons/Square44x44Logo.png b/apps/desktop/src-tauri/icons/Square44x44Logo.png new file mode 100644 index 00000000..9eedcbf2 Binary files /dev/null and b/apps/desktop/src-tauri/icons/Square44x44Logo.png differ diff --git a/apps/desktop/src-tauri/icons/Square71x71Logo.png b/apps/desktop/src-tauri/icons/Square71x71Logo.png new file mode 100644 index 00000000..d41849eb Binary files /dev/null and b/apps/desktop/src-tauri/icons/Square71x71Logo.png differ diff --git a/apps/desktop/src-tauri/icons/Square89x89Logo.png b/apps/desktop/src-tauri/icons/Square89x89Logo.png new file mode 100644 index 00000000..76662ba5 Binary files /dev/null and b/apps/desktop/src-tauri/icons/Square89x89Logo.png differ diff --git a/apps/desktop/src-tauri/icons/StoreLogo.png b/apps/desktop/src-tauri/icons/StoreLogo.png new file mode 100644 index 00000000..d80913ee Binary files /dev/null and b/apps/desktop/src-tauri/icons/StoreLogo.png differ diff --git a/apps/desktop/src-tauri/icons/group.svg b/apps/desktop/src-tauri/icons/group.svg new file mode 100644 index 00000000..7b5fc869 --- /dev/null +++ b/apps/desktop/src-tauri/icons/group.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/apps/desktop/src-tauri/icons/icon.icns b/apps/desktop/src-tauri/icons/icon.icns new file mode 100644 index 00000000..3ba4abb3 Binary files /dev/null and b/apps/desktop/src-tauri/icons/icon.icns differ diff --git a/apps/desktop/src-tauri/icons/icon.ico b/apps/desktop/src-tauri/icons/icon.ico new file mode 100644 index 00000000..99beb26a Binary files /dev/null and b/apps/desktop/src-tauri/icons/icon.ico differ diff --git a/apps/desktop/src-tauri/icons/icon.png b/apps/desktop/src-tauri/icons/icon.png new file mode 100644 index 00000000..79ee9bc8 Binary files /dev/null and b/apps/desktop/src-tauri/icons/icon.png differ diff --git a/apps/desktop/src-tauri/icons/new_chat.svg b/apps/desktop/src-tauri/icons/new_chat.svg new file mode 100644 index 00000000..3f3ec541 --- /dev/null +++ b/apps/desktop/src-tauri/icons/new_chat.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/apps/desktop/src-tauri/icons/superagilogo.svg b/apps/desktop/src-tauri/icons/superagilogo.svg new file mode 100644 index 00000000..0021b31b --- /dev/null +++ b/apps/desktop/src-tauri/icons/superagilogo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/desktop/src-tauri/src/agent_bridge/commands.rs b/apps/desktop/src-tauri/src/agent_bridge/commands.rs new file mode 100644 index 00000000..d7607259 --- /dev/null +++ b/apps/desktop/src-tauri/src/agent_bridge/commands.rs @@ -0,0 +1,2302 @@ +use std::collections::HashSet; +use std::path::PathBuf; +use std::sync::Arc; + +use serde::{Deserialize, Serialize}; +use tauri::{AppHandle, Emitter, Manager, State}; +use tokio::sync::RwLock; + +use agent::agent::config::AgentConfig; +use agent::llm::{ChatMessage, LlmClient, LlmClientConfig, Provider}; +use agent::persistence::MessagePersister; +use agent::session::SessionManager; +use agent::tool::ToolMode; + +use crate::agent_bridge::db::{ + reconstruct_context, AgentDb, SessionRow, SqliteMessagePersister, StoredMessage, +}; +use crate::agent_bridge::events::{ + spawn_event_relay, PermissionAwareApprovalHandler, TauriApprovalHandler, + TauriApprovalHandlerFactory, +}; +use crate::agent_bridge::permissions::PermissionConfig; +use crate::agent_bridge::traits::{EventEmitter, TauriEventEmitter}; +use crate::AppState; + +// ── AgentState ───────────────────────────────────────────────────────────── + +pub struct AgentState { + pub db: Arc, + /// App-managed snapshot checkpoint root: `/.supercoder/checkpoints`. + /// Per-session captures land in `//turn-N`. + pub checkpoint_root: PathBuf, + pub(crate) session_manager: RwLock>>, + /// Folders with a currently-running agent loop — enforces one active + /// session per folder. Cleared by each session's monitor task on completion. + pub(crate) running_folders: Arc>>, + pub(crate) approval_handlers: RwLock>>, + pub(crate) model_registry: RwLock, + pub(crate) write_lock_registry: Arc, +} + +impl AgentState { + pub fn new(db: Arc, checkpoint_root: PathBuf) -> Self { + Self { + db, + checkpoint_root, + session_manager: RwLock::new(None), + running_folders: Arc::new(RwLock::new(HashSet::new())), + approval_handlers: RwLock::new(std::collections::HashMap::new()), + model_registry: RwLock::new(agent::agent::model_profile::ModelRegistry::with_defaults()), + write_lock_registry: Arc::new(agent::subagents::WriteLockRegistry::new()), + } + } + + async fn get_or_create_manager(&self) -> Arc { + { + let guard = self.session_manager.read().await; + if let Some(ref mgr) = *guard { + return Arc::clone(mgr); + } + } + let mut guard = self.session_manager.write().await; + if let Some(ref mgr) = *guard { + return Arc::clone(mgr); + } + // Production always passes a per-session persister_override, so the + // manager's default is a Noop that is never exercised. + let default: Arc = Arc::new(agent::persistence::NoopPersister); + let manager = Arc::new(SessionManager::new(default)); + *guard = Some(Arc::clone(&manager)); + manager + } + + fn checkpoint_dir(&self) -> PathBuf { + self.checkpoint_root.clone() + } +} + +// ── Attachments / user message ───────────────────────────────────────────── + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct AttachmentPayload { + pub url: String, + pub file_name: String, + pub media_type: String, +} + +async fn fetch_image_as_base64(url: &str, media_type: &str) -> String { + // Local attachments (pasted/picked images) arrive as data: URLs already + // base64-encoded by the frontend — pass them through untouched. + if url.starts_with("data:") { + return url.to_string(); + } + let client = reqwest::Client::builder() + .timeout(std::time::Duration::from_secs(15)) + .build() + .unwrap_or_default(); + match client.get(url).header("User-Agent", "SuperCoder/1.0").send().await { + Ok(resp) if resp.status().is_success() => match resp.bytes().await { + Ok(bytes) => { + use base64::Engine; + let encoded = base64::engine::general_purpose::STANDARD.encode(&bytes); + format!("data:{};base64,{}", media_type, encoded) + } + Err(_) => url.to_string(), + }, + _ => url.to_string(), + } +} + +async fn build_user_message(text: &str, attachments: Option>) -> ChatMessage { + let image_attachments: Vec<&AttachmentPayload> = attachments + .as_ref() + .map(|atts| atts.iter().filter(|a| a.media_type.starts_with("image/")).collect()) + .unwrap_or_default(); + + if image_attachments.is_empty() { + return ChatMessage::user(text); + } + + use agent::llm::types::{ContentBlock, ImageUrlContent}; + let mut blocks = vec![ContentBlock::Text { text: text.to_string(), cache_control: None }]; + for att in image_attachments { + let data_url = fetch_image_as_base64(&att.url, &att.media_type).await; + blocks.push(ContentBlock::ImageUrl { + image_url: ImageUrlContent { url: data_url, detail: Some("auto".to_string()) }, + }); + } + ChatMessage::user_with_images(blocks) +} + +// ── Providers (endpoints) + model selections ──────────────────────────────── + +/// A saved LLM provider = an *endpoint* (no model bundled). Persisted as a JSON +/// array under `llm_providers`. `kind` maps to a wire format: +/// openai/openai_compatible → OpenAI; anthropic → Anthropic. OpenAI and Anthropic +/// are built-in singletons (ids "openai"/"anthropic"); openai_compatible can be added. +/// Per-model metadata discovered from a provider's `/models` endpoint or edited +/// by the user. Optional & defaulted so older stored providers deserialize. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ModelMeta { + /// Discovered context length (e.g. OpenRouter `context_length`). `None` = + /// unknown → context bar shows raw count, auto-compaction disabled. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub context_length: Option, + /// Whether this model accepts image inputs. + #[serde(default)] + pub supports_images: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ProviderConfig { + pub id: String, + pub kind: String, + /// Display name (shown for openai_compatible providers; built-ins use their kind name). + #[serde(default)] + pub label: String, + pub base_url: String, + #[serde(default)] + pub api_key: String, + /// Available model ids (populated by "Fetch models" or typed). Feeds the pickers. + #[serde(default)] + pub models: Vec, + /// Per-model discovered/edited metadata, keyed by model id. + #[serde(default)] + pub model_meta: std::collections::HashMap, + /// Provider-level vision fallback for custom providers when a model advertises + /// no per-model flag. Built-ins resolve vision from the model registry instead. + #[serde(default)] + pub supports_images: bool, +} + +impl ProviderConfig { + fn provider(&self) -> Provider { + match self.kind.as_str() { + "anthropic" => Provider::Anthropic, + _ => Provider::OpenAI, + } + } + + fn is_builtin(&self) -> bool { + self.kind == "openai" || self.kind == "anthropic" + } +} + +/// A reference to a specific model on a specific provider. Used for the global +/// active/compaction/title selections and recorded on each session. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ModelRef { + pub provider_id: String, + pub model: String, +} + +/// Global, outer-level model selections — each picks a model from across the +/// configured providers. `active` = main coding model for new sessions. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ModelSelection { + #[serde(default)] + pub active: Option, + #[serde(default)] + pub compaction: Option, + #[serde(default)] + pub title: Option, +} + +const PROVIDERS_KEY: &str = "llm_providers"; +const SELECTION_KEY: &str = "llm_selection"; + +fn default_providers() -> Vec { + vec![ + ProviderConfig { + id: "openai".to_string(), + kind: "openai".to_string(), + label: String::new(), + base_url: "https://api.openai.com/v1".to_string(), + api_key: String::new(), + models: Vec::new(), + model_meta: std::collections::HashMap::new(), + supports_images: false, + }, + ProviderConfig { + id: "anthropic".to_string(), + kind: "anthropic".to_string(), + label: String::new(), + base_url: "https://api.anthropic.com".to_string(), + api_key: String::new(), + models: Vec::new(), + model_meta: std::collections::HashMap::new(), + supports_images: false, + }, + ] +} + +/// Read saved providers. Always guarantees the built-in OpenAI + Anthropic rows +/// exist (self-heals older stores that predate one of them), keeping built-ins +/// first in a stable order, then user-added OpenAI-compatible providers. +fn read_providers(app_state: &AppState) -> Vec { + let stored: Vec = app_state + .db + .get_setting(PROVIDERS_KEY) + .ok() + .flatten() + .and_then(|raw| serde_json::from_str(&raw).ok()) + .unwrap_or_default(); + + let mut list: Vec = Vec::new(); + let mut changed = false; + for builtin in default_providers() { + match stored.iter().find(|p| p.id == builtin.id) { + Some(existing) => list.push(existing.clone()), + None => { + list.push(builtin); + changed = true; + } + } + } + for p in &stored { + if !p.is_builtin() && !list.iter().any(|x| x.id == p.id) { + list.push(p.clone()); + } + } + if changed { + let _ = write_providers(app_state, &list); + } + list +} + +fn write_providers(app_state: &AppState, providers: &[ProviderConfig]) -> Result<(), String> { + let raw = serde_json::to_string(providers).map_err(|e| e.to_string())?; + app_state.db.set_setting(PROVIDERS_KEY, &raw) +} + +fn read_selection(app_state: &AppState) -> ModelSelection { + app_state + .db + .get_setting(SELECTION_KEY) + .ok() + .flatten() + .and_then(|raw| serde_json::from_str(&raw).ok()) + .unwrap_or_default() +} + +fn write_selection(app_state: &AppState, sel: &ModelSelection) -> Result<(), String> { + let raw = serde_json::to_string(sel).map_err(|e| e.to_string())?; + app_state.db.set_setting(SELECTION_KEY, &raw) +} + +// ── Context engine (opt-in semantic/graph search) ───────────────────────────── + +const CONTEXT_ENGINE_KEY: &str = "context_engine"; +const MACHINE_ID_KEY: &str = "machine_id"; + +/// Persisted context-engine connection. `base_url` points at a user-managed +/// backend (the docker-compose stack); `enabled` gates whether coding sessions +/// register the search/graph tools and stream the repo up on open. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ContextEngineSettings { + pub enabled: bool, + pub base_url: String, +} + +impl Default for ContextEngineSettings { + fn default() -> Self { + Self { enabled: false, base_url: "http://127.0.0.1:8106".to_string() } + } +} + +/// Live connection probe result for the Settings panel. +#[derive(Debug, Clone, Serialize)] +pub struct ContextEngineStatus { + pub connected: bool, + pub error: Option, +} + +/// One repository known to the app and its index state on the backend. +#[derive(Debug, Clone, Serialize)] +pub struct IndexedRepo { + pub path: String, + pub exists: bool, + pub empty: bool, + pub repo_id: Option, + /// Live watcher status tag (`not_indexed`/`indexing`/`indexed`/`error:`), + /// or `None` if this repo isn't currently being watched. + pub status: Option, + /// File count from the last full sync, when known. + pub file_count: Option, +} + +/// Flatten an `IndexWatcherStatus` into the `(status tag, file_count)` pair the +/// Settings list renders. +fn watcher_status_parts( + s: &crate::context_watcher::IndexWatcherStatus, +) -> (Option, Option) { + use crate::context_watcher::IndexWatcherStatus as S; + match s { + S::NotIndexed => (Some("not_indexed".to_string()), None), + S::Indexing => (Some("indexing".to_string()), None), + S::Indexed { file_count } => (Some("indexed".to_string()), *file_count), + S::Error { reason, .. } => (Some(format!("error:{reason}")), None), + } +} + +#[derive(Deserialize)] +struct IndexStatusResponse { + #[serde(default)] + exists: bool, + #[serde(default)] + empty: bool, + #[serde(default)] + repo_id: Option, +} + +pub(crate) fn normalize_base_url(raw: &str) -> String { + raw.trim().trim_end_matches('/').to_string() +} + +/// Read the context-engine settings straight off the settings DB. Used by the +/// watcher (which holds an `Arc`, not an `AppState`). +pub(crate) fn read_context_engine_db(db: &crate::Database) -> ContextEngineSettings { + db.get_setting(CONTEXT_ENGINE_KEY) + .ok() + .flatten() + .and_then(|raw| serde_json::from_str(&raw).ok()) + .unwrap_or_default() +} + +fn read_context_engine(app_state: &AppState) -> ContextEngineSettings { + read_context_engine_db(&app_state.db) +} + +/// Stable per-machine UUID for context-engine tenancy headers. Generated and +/// persisted to the settings DB on first use (no accounts in v1). +fn machine_id(app_state: &AppState) -> String { + if let Ok(Some(id)) = app_state.db.get_setting(MACHINE_ID_KEY) { + if !id.is_empty() { + return id; + } + } + let id = uuid::Uuid::new_v4().to_string(); + let _ = app_state.db.set_setting(MACHINE_ID_KEY, &id); + id +} + +#[tauri::command] +pub async fn agent_get_context_engine( + app_state: State<'_, AppState>, +) -> Result { + Ok(read_context_engine(&app_state)) +} + +#[tauri::command] +pub async fn agent_set_context_engine( + settings: ContextEngineSettings, + app_state: State<'_, AppState>, + watcher: State<'_, Arc>, + controller: State<'_, Arc>, +) -> Result<(), String> { + let raw = serde_json::to_string(&settings).map_err(|e| e.to_string())?; + app_state.db.set_setting(CONTEXT_ENGINE_KEY, &raw)?; + + let app_mode = controller.mode() == crate::engine_control::EngineMode::App; + + if settings.enabled { + // User mode: connecting to a self-run backend, so start watching now. + // App mode: only reveal the panel — the explicit Start button (gated on + // the embedding key) owns bringing the stack up, then starts watchers. + if !app_mode { + let wm = watcher.inner().clone(); + tokio::spawn(async move { wm.auto_start().await }); + } + } else { + watcher.stop_all().await; + if app_mode { + let _ = controller.stop().await; + } + } + Ok(()) +} + +/// Probe a backend URL by hitting /api/health. Used by the Settings "Connect" +/// button — does not change saved settings. +#[tauri::command] +pub async fn agent_context_engine_status(base_url: String) -> Result { + let url = format!("{}/api/health", normalize_base_url(&base_url)); + let client = reqwest::Client::builder() + .timeout(std::time::Duration::from_secs(5)) + .build() + .map_err(|e| e.to_string())?; + match client.get(&url).send().await { + Ok(resp) if resp.status().is_success() => Ok(ContextEngineStatus { connected: true, error: None }), + Ok(resp) => Ok(ContextEngineStatus { + connected: false, + error: Some(format!("HTTP {}", resp.status().as_u16())), + }), + Err(e) => Ok(ContextEngineStatus { connected: false, error: Some(e.to_string()) }), + } +} + +/// List watched repos (from the `watched_repos` table) annotated with the +/// watcher's live in-memory status and a one-shot `/index/status` probe for +/// exists/empty/repo_id. Backends that can't be reached are reported not-indexed. +#[tauri::command] +pub async fn agent_context_engine_repos( + app_state: State<'_, AppState>, + watcher: State<'_, Arc>, +) -> Result, String> { + let base_url = normalize_base_url(&read_context_engine(&app_state).base_url); + let mid = machine_id(&app_state); + + // Watched repos, most-recently-used first. + let folders: Vec = { + let conn = app_state.db.conn.lock(); + let mut stmt = conn + .prepare("SELECT repo_path FROM watched_repos ORDER BY last_used_at DESC") + .map_err(|e| e.to_string())?; + let rows = stmt + .query_map([], |r| r.get::<_, String>(0)) + .map_err(|e| e.to_string())?; + rows.filter_map(|r| r.ok()).collect() + }; + + let client = reqwest::Client::builder() + .timeout(std::time::Duration::from_secs(5)) + .build() + .map_err(|e| e.to_string())?; + + let mut out = Vec::with_capacity(folders.len()); + for folder in folders { + let (status, watcher_file_count) = match watcher.get_status(&folder).await { + Some(s) => watcher_status_parts(&s), + None => (None, None), + }; + let resp = client + .get(format!("{base_url}/api/v1/index/status")) + .query(&[ + ("repo_path", folder.as_str()), + ("user_id", "local"), + ("workspace_id", "0"), + ("machine_id", mid.as_str()), + ]) + .send() + .await; + let (exists, empty, repo_id) = match resp { + Ok(r) if r.status().is_success() => match r.json::().await { + Ok(s) => (s.exists, s.empty, s.repo_id), + Err(_) => (false, true, None), + }, + _ => (false, true, None), + }; + out.push(IndexedRepo { + path: folder, + exists, + empty, + repo_id, + status, + file_count: watcher_file_count, + }); + } + Ok(out) +} + +/// Delete a repo's index (vectors + graph + merkle tree) on the backend. Stops +/// the live watcher and forgets the `watched_repos` row first. +#[tauri::command] +pub async fn agent_context_engine_delete_repo( + path: String, + app_state: State<'_, AppState>, + watcher: State<'_, Arc>, +) -> Result<(), String> { + watcher.stop_watching(&path).await; + { + let conn = app_state.db.conn.lock(); + let _ = conn.execute( + "DELETE FROM watched_repos WHERE repo_path = ?1", + [path.as_str()], + ); + } + + let base_url = normalize_base_url(&read_context_engine(&app_state).base_url); + let mid = machine_id(&app_state); + let client = reqwest::Client::builder() + .timeout(std::time::Duration::from_secs(15)) + .build() + .map_err(|e| e.to_string())?; + let resp = client + .delete(format!("{base_url}/api/v1/index")) + .json(&serde_json::json!({ + "repo_path": path, + "user_id": "local", + "workspace_id": 0, + "machine_id": mid, + })) + .send() + .await + .map_err(|e| e.to_string())?; + if resp.status().is_success() { + Ok(()) + } else { + Err(format!("HTTP {}: {}", resp.status().as_u16(), resp.text().await.unwrap_or_default())) + } +} + +fn provider_by_id(app_state: &AppState, id: &str) -> Option { + read_providers(app_state).into_iter().find(|p| p.id == id) +} + +/// Resolve a `ModelRef` to its provider + model id. +fn resolve_ref(app_state: &AppState, r: &ModelRef) -> Option<(ProviderConfig, String)> { + provider_by_id(app_state, &r.provider_id).map(|p| (p, r.model.clone())) +} + +/// The main (provider, model) a NEW session should use: the active selection, +/// else the first provider that has at least one model. +fn default_session_model(app_state: &AppState) -> Option { + let sel = read_selection(app_state); + if let Some(ref a) = sel.active { + if provider_by_id(app_state, &a.provider_id).is_some() { + return sel.active; + } + } + read_providers(app_state) + .into_iter() + .find(|p| !p.models.is_empty()) + .map(|p| ModelRef { provider_id: p.id, model: p.models[0].clone() }) +} + +/// Resolve the (provider, model) a session runs with: what it was created with +/// (`provider_id` + `model`), falling back to the active selection. +fn session_model(app_state: &AppState, session: &SessionRow) -> Result<(ProviderConfig, String), String> { + if let (Some(pid), Some(model)) = (session.provider_id.as_ref(), session.model.as_ref()) { + if let Some(p) = provider_by_id(app_state, pid) { + return Ok((p, model.clone())); + } + } + default_session_model(app_state) + .and_then(|r| resolve_ref(app_state, &r)) + .ok_or_else(|| "No model selected. Pick one in Settings / the model picker.".to_string()) +} + +pub fn provider_to_llm_config(p: &ProviderConfig, model: &str) -> LlmClientConfig { + LlmClientConfig { + provider: p.provider(), + base_url: p.base_url.clone(), + model: model.to_string(), + api_key: p.api_key.clone(), + temperature: None, + max_completion_tokens: None, + extra_headers: vec![], + thinking: None, + disable_cache_control: false, + } +} + +/// Resolved capability for a (provider, model) pair. Single source of truth for +/// the context bar's tri-state and the image-attach gating on the frontend. +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ModelCapability { + /// `Some(n)` = known/discovered limit → show used/max + %, auto-compact on. + /// `None` = unknown → show raw token count, auto-compaction disabled. + pub context_limit: Option, + pub supports_images: bool, + /// "known" (registry) | "discovered" (provider /models) | "unknown". + pub source: String, +} + +/// Resolve context limit + vision capability: known registry model (built-ins) → +/// discovered per-model provider metadata → unknown. +async fn resolve_capability( + agent_state: &AgentState, + provider: &ProviderConfig, + model: &str, +) -> ModelCapability { + if let Some(p) = agent_state.model_registry.read().await.get(model) { + return ModelCapability { + context_limit: Some(p.context_window), + supports_images: p.supports_images, + source: "known".into(), + }; + } + let meta = provider.model_meta.get(model); + let context_limit = meta.and_then(|m| m.context_length); + let supports_images = meta.map(|m| m.supports_images).unwrap_or(false) || provider.supports_images; + ModelCapability { + context_limit, + supports_images, + source: if context_limit.is_some() { "discovered".into() } else { "unknown".into() }, + } +} + +/// A built-in model from the registry, for the Settings model picker. Carries +/// the context window + vision flag so the UI never has to duplicate that data. +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CuratedModel { + pub id: String, + pub display_name: String, + /// "openai" | "anthropic" — matches the built-in provider `kind`. + pub provider: String, + pub context_window: usize, + pub supports_images: bool, +} + +/// List the built-in model registry — the single source of truth for the +/// Settings picker (replaces the old hardcoded frontend `CURATED_MODELS`). +#[tauri::command] +pub async fn agent_list_models(agent_state: State<'_, AgentState>) -> Result, String> { + let registry = agent_state.model_registry.read().await; + Ok(registry + .list() + .into_iter() + .map(|p| CuratedModel { + id: p.id.clone(), + display_name: p.display_name.clone(), + provider: p.provider.clone(), + context_window: p.context_window, + supports_images: p.supports_images, + }) + .collect()) +} + +/// Frontend-facing resolver: the context bar and attach UI call this for the +/// active (provider, model) to learn the limit and vision support in one shot. +#[tauri::command] +pub async fn agent_resolve_model_capability( + provider_id: String, + model: String, + agent_state: State<'_, AgentState>, + app_state: State<'_, AppState>, +) -> Result { + let provider = provider_by_id(&app_state, &provider_id) + .ok_or_else(|| format!("Unknown provider: {provider_id}"))?; + Ok(resolve_capability(&agent_state, &provider, &model).await) +} + +#[allow(clippy::too_many_arguments)] +fn build_agent_config( + main_provider: &ProviderConfig, + main_model: &str, + compaction: Option<(&ProviderConfig, &str)>, + working_dir: PathBuf, + mode: ToolMode, + context_limit: Option, + project_note: Option<&str>, + checkpoint_dir: PathBuf, + skills: Option>, + subagents: Option>, + context_engine: Option, +) -> AgentConfig { + let mut config = AgentConfig::new(provider_to_llm_config(main_provider, main_model), working_dir); + config.mode = mode; + // Known/discovered limit → auto-compact at threshold × n. Unknown → disable + // token-based compaction (the message-count guard still applies). + match context_limit { + Some(n) => { + config.compaction_config.context_limit = n; + config.compaction_config.auto_compact = true; + } + None => config.compaction_config.auto_compact = false, + } + config.skills = skills; + config.subagents = subagents; + config.checkpoint_dir = Some(checkpoint_dir); + + // When the context engine is enabled, wire the search/graph client so the + // crate auto-registers codebase_search / codebase_graph. The repo path the + // index is keyed by is the session working dir. + if let Some(ce_cfg) = context_engine { + config.context_engine_repo_path = Some(PathBuf::from(&ce_cfg.repo_path)); + let client: Arc = + Arc::new(agent::context_engine::ContextEngineClient::new(ce_cfg)); + config.context_engine = Some(client); + } + + // Compaction runs on the globally-selected compaction model (any provider), + // falling back to the session's own model when none is set. + let mut compaction_llm = match compaction { + Some((p, m)) => provider_to_llm_config(p, m), + None => provider_to_llm_config(main_provider, main_model), + }; + compaction_llm.disable_cache_control = true; + config.compaction_llm = Some(compaction_llm); + + config.system_prompt = Some(agent::agent::prompt::build_system_prompt( + mode, + &config.working_dir, + None, + project_note, + config.skills.as_deref(), + config.subagents.as_deref(), + )); + config +} + +/// Generate a concise session title from the first message using the configured +/// title model, off the turn's hot path. Updates the DB and emits +/// `agent:session_title` so the sidebar refreshes. Best-effort: any failure (bad +/// key, empty reply) silently leaves the substring fallback in place. +fn spawn_title_generation( + app_handle: AppHandle, + db: Arc, + session_id: String, + first_message: String, + provider: ProviderConfig, + model: String, +) { + tokio::spawn(async move { + let mut cfg = provider_to_llm_config(&provider, &model); + cfg.max_completion_tokens = Some(32); + cfg.disable_cache_control = true; + let client = LlmClient::new(cfg); + // Few-shot: a labeling function, not a chat. The examples lock the model + // into emitting a bare Title-Case title (3–6 words) even for vague input, + // and demonstrate that it must never ask a question. + let msgs = vec![ + ChatMessage::system( + "You are a function that turns a developer's first message into a short coding-session \ + title. Output ONLY the title: 3–6 words, Title Case, no quotes, no punctuation, no \ + preamble. Never ask a question or request clarification — if the message is vague, \ + title it literally from its words.", + ), + ChatMessage::user("fix the flaky auth test and add retries"), + ChatMessage::assistant(Some("Fix Flaky Auth Test".to_string()), None, None), + ChatMessage::user("add a dark mode toggle to the settings page"), + ChatMessage::assistant(Some("Add Dark Mode Toggle".to_string()), None, None), + ChatMessage::user("why is my build failing with a linker error"), + ChatMessage::assistant(Some("Debug Linker Build Error".to_string()), None, None), + ChatMessage::user("hey"), + ChatMessage::assistant(Some("New Coding Session".to_string()), None, None), + ChatMessage::user(first_message), + ]; + let (tx, _rx) = tokio::sync::mpsc::channel(8); + let probe_sid = format!("title-{}", uuid::Uuid::new_v4()); + let Ok(resp) = client.chat_completion(&msgs, &[], &tx, &probe_sid, None).await else { + return; + }; + // Reject refusals/questions/sentences — keep the substring fallback in that case. + let Some(title) = resp.content.as_deref().and_then(clean_title) else { return }; + let title_db = title.clone(); + let sid_db = session_id.clone(); + let _ = tokio::task::spawn_blocking(move || db.set_session_title(&sid_db, &title_db)).await; + let _ = app_handle.emit( + "agent:session_title", + serde_json::json!({ "session_id": session_id, "title": title }), + ); + }); +} + +/// Sanitize an LLM title reply; returns `None` if it looks like a refusal, +/// a question, or a full sentence rather than a title. +fn clean_title(raw: &str) -> Option { + let mut t = raw.trim().trim_matches('"').trim(); + t = t.lines().next().unwrap_or("").trim(); + for prefix in ["Title:", "title:", "Title -", "Session:"] { + if let Some(rest) = t.strip_prefix(prefix) { + t = rest.trim(); + } + } + t = t.trim_matches('"').trim(); + if t.is_empty() { + return None; + } + let lower = t.to_lowercase(); + let looks_bad = t.ends_with('?') + || t.split_whitespace().count() > 10 + || lower.starts_with("i ") + || lower.starts_with("i'm") + || lower.starts_with("i need") + || lower.starts_with("sorry") + || lower.starts_with("could you") + || lower.starts_with("can you") + || lower.starts_with("please") + || lower.contains("more context") + || lower.contains("provide more"); + if looks_bad { + return None; + } + Some(t.chars().take(60).collect()) +} + +fn load_permission_config(app_state: &AppState, project_path: Option<&str>) -> PermissionConfig { + tokio::task::block_in_place(|| { + let conn = app_state.db.conn.lock(); + let _ = crate::agent_bridge::permissions::ensure_permissions_table(&conn); + crate::agent_bridge::permissions::get_permission(&conn, project_path) + }) +} + +async fn create_approval_handler( + agent_state: &AgentState, + emitter: Arc, + session_id: &str, + perm_config: PermissionConfig, +) -> Option> { + let tauri_handler = Arc::new(TauriApprovalHandler::new(emitter, session_id.to_string())); + agent_state + .approval_handlers + .write() + .await + .insert(session_id.to_string(), Arc::clone(&tauri_handler)); + Some(Arc::new(PermissionAwareApprovalHandler::new(tauri_handler, perm_config))) +} + +/// Build the `SubagentInheritance` bundle. Children share the parent's persister +/// (persisting under their own child session_id; the crate stamps the parent +/// link in metadata) and the snapshot checkpoint dir. +async fn build_subagent_inheritance( + config: &AgentConfig, + agent_state: &AgentState, + approval_handler: Option<&Arc>, + parent_session_id: String, + parent_persister: Arc, +) -> Option> { + config.subagents.as_ref()?; + let approval_factory: Option> = + approval_handler.map(|h| { + Arc::new(TauriApprovalHandlerFactory::new(Arc::clone(h))) + as Arc + }); + Some(Arc::new(agent::subagents::SubagentInheritance { + llm_client_config: config.llm.clone(), + retry_config: config.retry_config.clone(), + compaction_config: config.compaction_config.clone(), + compaction_llm: config.compaction_llm.clone(), + max_iterations: config.max_iterations, + context_engine: config.context_engine.clone(), + context_engine_repo_path: config.context_engine_repo_path.clone(), + persister: Some(parent_persister as Arc), + approval_handler_factory: approval_factory, + parent_session_id: Some(parent_session_id), + write_lock_registry: Arc::clone(&agent_state.write_lock_registry), + checkpoint_dir: config.checkpoint_dir.clone(), + })) +} + +// ── Context helpers ──────────────────────────────────────────────────────── + +/// Deserialize stored messages into LLM ChatMessages and sanitize tool pairing. +pub(crate) fn deserialize_context(stored: Vec) -> Vec { + let agent_messages = reconstruct_context(stored); + let context: Vec = agent_messages + .iter() + .filter_map(|m| serde_json::from_value(m.llm_message.clone()).ok()) + .collect(); + sanitize_tool_pairs(context) +} + +/// Ensure every assistant message with `tool_calls` is followed by matching +/// `tool` results; drop orphaned tool results. Required by the OpenAI wire format. +pub(crate) fn sanitize_tool_pairs(messages: Vec) -> Vec { + let mut result: Vec = Vec::with_capacity(messages.len()); + let mut i = 0; + while i < messages.len() { + let msg = &messages[i]; + if msg.role == "assistant" { + if let Some(ref tool_calls) = msg.tool_calls { + let expected_ids: Vec<&str> = tool_calls.iter().map(|tc| tc.id.as_str()).collect(); + let mut found_ids = Vec::new(); + let mut j = i + 1; + while j < messages.len() && messages[j].role == "tool" { + if let Some(ref tcid) = messages[j].tool_call_id { + if expected_ids.contains(&tcid.as_str()) { + found_ids.push(tcid.as_str()); + } + } + j += 1; + } + result.push(messages[i].clone()); + for k in (i + 1)..j { + result.push(messages[k].clone()); + } + for expected_id in &expected_ids { + if !found_ids.contains(expected_id) { + result.push(ChatMessage::tool_result( + *expected_id, + "[Tool result unavailable — the call was denied or the session ended before completion.]", + )); + } + } + i = j; + } else { + result.push(messages[i].clone()); + i += 1; + } + } else if msg.role == "tool" { + i += 1; // orphaned tool result — drop + } else { + result.push(messages[i].clone()); + i += 1; + } + } + result +} + +// ── Response types ───────────────────────────────────────────────────────── + +#[derive(Debug, Serialize)] +pub struct SendMessageResponse { + pub session_id: String, +} + +#[derive(Debug, Serialize)] +pub struct ToolChip { + pub name: String, + pub summary: String, +} + +#[derive(Debug, Serialize)] +pub struct AgentDisplayMessage { + pub id: String, + pub role: String, + pub text: String, + pub created_at: String, + pub session_id: String, + /// Tool calls that ran in the lead-up to this assistant message (for the + /// "Thought for…" collapsible). Reconstructed from persisted tool_call rows. + pub tools: Vec, + /// Wall-clock seconds spent on tool calls before this assistant message. + pub duration_seconds: u32, + /// Image data-URLs attached to this message (rebuilt from on-disk refs). + pub images: Vec, +} + +/// Pull a short, human-friendly summary out of a tool call's JSON arguments. +/// Tool names + arg keys mirror the crate's tools (camelCase keys). +fn tool_arg_summary(name: &str, args: &serde_json::Value) -> String { + let get = |k: &str| args.get(k).and_then(|v| v.as_str()).unwrap_or(""); + let base = |p: &str| p.rsplit('/').next().unwrap_or(p).to_string(); + match name { + "read" | "write" | "edit" | "apply_patch" => base(get("filePath")), + "ls" => base(get("path")), + "bash" | "git" => get("command").to_string(), + "grep" | "glob" => get("pattern").to_string(), + "codebase_search" | "codebase_graph" => get("query").to_string(), + "save_plan" | "edit_plan" => base(get("path")), + "todo_write" => "Updated todos".to_string(), + "spawn_subagent" => get("name").to_string(), + _ => String::new(), + } +} + +/// Seconds between two RFC3339 timestamps (0 if unparseable / negative). +fn elapsed_secs(from: &str, to: &str) -> u32 { + match ( + chrono::DateTime::parse_from_rfc3339(from), + chrono::DateTime::parse_from_rfc3339(to), + ) { + (Ok(a), Ok(b)) => (b - a).num_seconds().max(0) as u32, + _ => 0, + } +} + +/// Parse the tool_call name(s) + arg summaries from a persisted tool_call row. +fn parse_tool_chips(llm_message: &str) -> Vec { + let Ok(v) = serde_json::from_str::(llm_message) else { + return Vec::new(); + }; + let Some(calls) = v.get("tool_calls").and_then(|c| c.as_array()) else { + return Vec::new(); + }; + calls + .iter() + .filter_map(|c| { + let func = c.get("function")?; + let name = func.get("name").and_then(|n| n.as_str())?.to_string(); + let args = func + .get("arguments") + .and_then(|a| a.as_str()) + .and_then(|s| serde_json::from_str::(s).ok()) + .unwrap_or(serde_json::Value::Null); + let summary = tool_arg_summary(&name, &args); + Some(ToolChip { name, summary }) + }) + .collect() +} + +#[derive(Debug, Serialize)] +pub struct AgentDiffResult { + pub files_changed: u32, + pub insertions: u32, + pub deletions: u32, + pub stat: String, + pub diff: String, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CheckpointInfo { + pub turn: u32, + pub file_count: usize, + pub paths: Vec, +} + +#[derive(Debug, Serialize)] +pub struct ContextUsageResponse { + pub total_tokens: u32, + /// `None` for unknown models → UI shows raw token count with no max/percentage. + pub context_limit: Option, + pub message_count: u32, +} + +fn parse_mode(mode: &str) -> ToolMode { + match mode { + "plan" => ToolMode::Plan, + "coding" => ToolMode::Coding, + _ => ToolMode::Ask, + } +} + +// ── Session lifecycle ────────────────────────────────────────────────────── + +/// Create a new session (folder + mode). The session is the unit shown in the +/// sidebar; messages are sent into it via `agent_send_message`. +#[tauri::command] +pub async fn agent_create_session( + folder: String, + title: Option, + mode: Option, + provider_id: Option, + model: Option, + app_state: State<'_, AppState>, + agent_state: State<'_, AgentState>, +) -> Result { + let id = uuid::Uuid::new_v4().to_string(); + let db = Arc::clone(&agent_state.db); + let folder_c = folder.clone(); + // Mode is just the session's initial mode; it can be switched per message. + let mode_c = mode.unwrap_or_else(|| "coding".to_string()); + let title_c = title.clone(); + // Record the (provider, model) the session is created with: explicit pick, + // else the active selection. Resume then uses the same one. + let default = default_session_model(&app_state); + let provider_c = provider_id.or_else(|| default.as_ref().map(|r| r.provider_id.clone())); + let model_c = model.or_else(|| default.as_ref().map(|r| r.model.clone())); + tokio::task::spawn_blocking(move || { + db.create_session( + &id, + &folder_c, + &mode_c, + title_c.as_deref(), + None, + provider_c.as_deref(), + model_c.as_deref(), + )?; + db.get_session(&id) + }) + .await + .map_err(|e| format!("join error: {e}"))? + .map_err(|e| format!("Failed to create session: {e}"))? + .ok_or_else(|| "Session not found after create".to_string()) +} + +/// Rename a session (sidebar title). +#[tauri::command] +pub async fn agent_rename_session( + session_id: String, + title: String, + agent_state: State<'_, AgentState>, +) -> Result<(), String> { + let db = Arc::clone(&agent_state.db); + tokio::task::spawn_blocking(move || db.set_session_title(&session_id, &title)) + .await + .map_err(|e| format!("join error: {e}"))? + .map_err(|e| format!("Failed to rename session: {e}")) +} + +/// Re-pin an open session's provider + model. Used when the user switches the +/// model picker while a session is open — subsequent turns and the context bar +/// then use the newly selected model. +#[tauri::command] +pub async fn agent_set_session_model( + session_id: String, + provider_id: String, + model: String, + agent_state: State<'_, AgentState>, +) -> Result<(), String> { + let db = Arc::clone(&agent_state.db); + tokio::task::spawn_blocking(move || db.set_session_model(&session_id, &provider_id, &model)) + .await + .map_err(|e| format!("join error: {e}"))? + .map_err(|e| format!("Failed to set session model: {e}")) +} + +/// Soft-delete a session (hidden from the sidebar; data preserved on disk). +/// Rejected while its folder has a running loop — stop that first. +#[tauri::command] +pub async fn agent_delete_session( + session_id: String, + agent_state: State<'_, AgentState>, +) -> Result<(), String> { + let db = Arc::clone(&agent_state.db); + let sid = session_id.clone(); + let session = tokio::task::spawn_blocking(move || db.get_session(&sid)) + .await + .map_err(|e| format!("join error: {e}"))? + .map_err(|e| format!("Failed to load session: {e}"))?; + if let Some(s) = &session { + if agent_state.running_folders.read().await.contains(&s.folder) { + return Err("Stop the running session in this folder before deleting.".to_string()); + } + } + let db = Arc::clone(&agent_state.db); + let sid = session_id.clone(); + tokio::task::spawn_blocking(move || db.delete_session(&sid)) + .await + .map_err(|e| format!("join error: {e}"))? + .map_err(|e| format!("Failed to delete session: {e}"))?; + // Best-effort GC of the session's on-disk image attachments. + let _ = std::fs::remove_dir_all(crate::agent_bridge::db::images_dir(&session_id)); + Ok(()) +} + +/// List all sessions for the sidebar, most recent first. +#[tauri::command] +pub async fn agent_list_sessions( + agent_state: State<'_, AgentState>, +) -> Result, String> { + let db = Arc::clone(&agent_state.db); + tokio::task::spawn_blocking(move || db.list_sessions()) + .await + .map_err(|e| format!("join error: {e}"))? + .map_err(|e| format!("Failed to list sessions: {e}")) +} + +/// Send a message into a session. `mode` ("ask" | "plan" | "coding") can be +/// switched on any message; it is persisted as the session's current mode. +#[tauri::command] +pub async fn agent_send_message( + session_id: String, + message: String, + mode: Option, + attachments: Option>, + app_handle: AppHandle, + agent_state: State<'_, AgentState>, + app_state: State<'_, AppState>, +) -> Result { + let mut session = { + let db = Arc::clone(&agent_state.db); + let sid = session_id.clone(); + tokio::task::spawn_blocking(move || db.get_session(&sid)) + .await + .map_err(|e| format!("join error: {e}"))? + .map_err(|e| format!("Failed to load session: {e}"))? + .ok_or_else(|| format!("Session not found: {session_id}"))? + }; + + // Apply a per-message mode switch and remember it on the session. + if let Some(m) = mode { + if m != session.mode { + let db = Arc::clone(&agent_state.db); + let sid = session_id.clone(); + let m_c = m.clone(); + let _ = tokio::task::spawn_blocking(move || db.set_session_mode(&sid, &m_c)).await; + } + session.mode = m; + } + + run_agent_turn( + app_handle, + &app_state, + &agent_state, + session, + message, + attachments, + None, + ) + .await?; + + Ok(SendMessageResponse { session_id }) +} + +/// Start a NEW coding session seeded with a plan, and emit `agent-session-complete` +/// so the frontend opens the coding thread. +#[tauri::command] +pub async fn agent_start_coding_from_plan( + app_handle: AppHandle, + app_state: State<'_, AppState>, + agent_state: State<'_, AgentState>, + project_path: String, + plan_text: String, + plan_path: Option, + title: Option, +) -> Result { + if !std::path::Path::new(&project_path).exists() { + return Err(format!("Project path does not exist: {project_path}")); + } + + let plan_ref = plan_path.unwrap_or_else(|| format!("{project_path}/.agent/plan.md")); + let task_summary = format!( + "Implement the following plan step by step. The full plan also lives at: {plan_ref}\n\n\ + First, use todo_write to create todos from the plan's implementation steps, then work \ + through each one, marking them completed as you go. Edit files in place in the project.\n\n\ + ---\n\n{plan_text}" + ); + + // Create the coding session row. + let session_id = uuid::Uuid::new_v4().to_string(); + { + let db = Arc::clone(&agent_state.db); + let id = session_id.clone(); + let folder = project_path.clone(); + let title = title.unwrap_or_else(|| "Implement plan".to_string()); + let default = default_session_model(&app_state); + let provider_c = default.as_ref().map(|r| r.provider_id.clone()); + let model_c = default.as_ref().map(|r| r.model.clone()); + tokio::task::spawn_blocking(move || { + db.create_session(&id, &folder, "coding", Some(&title), None, provider_c.as_deref(), model_c.as_deref()) + }) + .await + .map_err(|e| format!("join error: {e}"))? + .map_err(|e| format!("Failed to create session: {e}"))?; + } + + let session = { + let db = Arc::clone(&agent_state.db); + let sid = session_id.clone(); + tokio::task::spawn_blocking(move || db.get_session(&sid)) + .await + .map_err(|e| format!("join error: {e}"))? + .map_err(|e| format!("Failed to load session: {e}"))? + .ok_or("Session missing after create")? + }; + + run_agent_turn( + app_handle.clone(), + &app_state, + &agent_state, + session, + task_summary.clone(), + None, + None, + ) + .await?; + + let _ = app_handle.emit( + "agent-session-complete", + serde_json::json!({ + "session_id": session_id, + "project_path": project_path, + "mode": "coding", + "task_summary": task_summary, + }), + ); + + Ok(SendMessageResponse { session_id }) +} + +/// Core: build config + persister + approval handler, start the loop in the +/// session's mode, and spawn the relay. `turn_offset_override` lets rewind pin a turn. +async fn run_agent_turn( + app_handle: AppHandle, + app_state: &AppState, + agent_state: &AgentState, + session: SessionRow, + message: String, + attachments: Option>, + _turn_offset_override: Option, +) -> Result<(), String> { + let session_id = session.id.clone(); + let folder = session.folder.clone(); + let mode = parse_mode(&session.mode); + + // One active session per folder. + { + let mut running = agent_state.running_folders.write().await; + if running.contains(&folder) { + return Err(format!("A session is already running for {folder}")); + } + running.insert(folder.clone()); + } + // Release the folder lock on any early return. + let release = |folder: String| { + let running = Arc::clone(&agent_state.running_folders); + tokio::spawn(async move { + running.write().await.remove(&folder); + }); + }; + + let work_dir = PathBuf::from(&folder); + if !work_dir.exists() { + release(folder.clone()); + return Err(format!("Folder does not exist: {folder}")); + } + + let (provider, model) = match session_model(app_state, &session) { + Ok(pm) => pm, + Err(e) => { + release(folder.clone()); + return Err(e); + } + }; + // Global compaction model (any provider), resolved once for this turn. + let compaction = read_selection(app_state).compaction.and_then(|r| resolve_ref(app_state, &r)); + // Tri-state context limit: known/discovered → Some(n) (auto-compact); unknown → None. + let context_limit = resolve_capability(agent_state, &provider, &model).await.context_limit; + + // Plan-mode note: surface existing plan files so the agent reuses edit_plan. + let project_note = if mode == ToolMode::Plan { + let agent_dir = work_dir.join(".agent"); + let existing: Vec = std::fs::read_dir(&agent_dir) + .into_iter() + .flatten() + .filter_map(|e| e.ok()) + .filter(|e| { + let n = e.file_name().to_string_lossy().to_string(); + n.starts_with("plan") && n.ends_with(".md") + }) + .map(|e| format!(".agent/{}", e.file_name().to_string_lossy())) + .collect(); + if existing.is_empty() { + None + } else { + Some(format!( + "EXISTING PLAN FILES: {}. If this request refines an existing plan, use edit_plan; \ + otherwise ask the user whether to replace it or create a new plan file.", + existing.join(", ") + )) + } + } else { + None + }; + + let skill_registry = crate::agent_bridge::skills::build_registry_for_agent(&agent_state.db, &work_dir); + let subagent_registry = crate::agent_bridge::subagents::build_registry_for_agent(&agent_state.db, &work_dir); + + // Context engine (opt-in). When enabled, the agent gets the search/graph + // tools and the repo is streamed up for indexing in the background. + let ce_settings = read_context_engine(app_state); + let ce_config = if ce_settings.enabled { + Some(agent::context_engine::ContextEngineConfig { + base_url: normalize_base_url(&ce_settings.base_url), + user_id: "local".to_string(), + workspace_id: 0, + machine_id: machine_id(app_state), + repo_path: folder.clone(), + auth_token: String::new(), + }) + } else { + None + }; + + let mut config = build_agent_config( + &provider, + &model, + compaction.as_ref().map(|(p, m)| (p, m.as_str())), + work_dir.clone(), + mode, + context_limit, + project_note.as_deref(), + agent_state.checkpoint_dir(), + skill_registry, + subagent_registry, + ce_config, + ); + + let emitter: Arc = Arc::new(TauriEventEmitter::new(app_handle.clone())); + + // Keep the context-engine index live for this repo. `start_watching` is + // idempotent — it bumps `last_used_at`, runs a fresh full_sync to catch + // offline edits, and starts the fs-watcher for incremental syncs. + if ce_settings.enabled { + let wm = app_handle + .state::>() + .inner() + .clone(); + let folder_owned = folder.clone(); + tokio::spawn(async move { + if let Err(e) = wm.start_watching(&folder_owned).await { + log::warn!("[ContextWatcher] start_watching failed for {folder_owned}: {e}"); + } + }); + } + let perm_config = load_permission_config(app_state, Some(&folder)); + let approval_handler = create_approval_handler(agent_state, emitter.clone(), &session_id, perm_config).await; + + let persister = Arc::new(SqliteMessagePersister::new(Arc::clone(&agent_state.db), folder.clone())); + + config.subagent_inheritance = build_subagent_inheritance( + &config, + agent_state, + approval_handler.as_ref(), + session_id.clone(), + Arc::clone(&persister), + ) + .await; + + // Prior context for resume. + let prior_context = { + let db = Arc::clone(&agent_state.db); + let sid = session_id.clone(); + let stored = tokio::task::spawn_blocking(move || db.load_session_for_context(&sid, 500)) + .await + .map_err(|e| format!("join error: {e}"))? + .map_err(|e| format!("Failed to load context: {e}"))?; + deserialize_context(stored) + }; + + // Persisted token count for compaction accuracy. + let initial_tokens = { + let db = Arc::clone(&agent_state.db); + let sid = session_id.clone(); + tokio::task::spawn_blocking(move || db.get_context_usage(&sid)) + .await + .ok() + .and_then(|r| r.ok()) + .flatten() + .map(|(t, _, _)| t as usize) + }; + + // Globally-unique turn numbering across messages (snapshot dir is the source of truth). + let turn_offset = git_ops::checkpoint::list(&agent_state.checkpoint_root, &session_id) + .await + .ok() + .and_then(|turns| turns.last().map(|t| t.turn)) + .unwrap_or(0); + + let manager = agent_state.get_or_create_manager().await; + let user_msg = build_user_message(&message, attachments).await; + + let event_rx = match mode { + ToolMode::Coding => manager + .start_coding_session( + session_id.clone(), + config, + user_msg, + None, + if prior_context.is_empty() { None } else { Some(prior_context) }, + None, + initial_tokens, + approval_handler, + if turn_offset > 0 { Some(turn_offset) } else { None }, + true, + Some(persister as Arc), + ) + .await, + _ => manager + .start_ask_session( + session_id.clone(), + config, + user_msg, + if prior_context.is_empty() { None } else { Some(prior_context) }, + initial_tokens, + approval_handler, + Some(persister as Arc), + ) + .await, + } + .map_err(|e| { + release(folder.clone()); + format!("Failed to start session: {e}") + })?; + + // Mark active + set a title from the first message if unset. + { + let db = Arc::clone(&agent_state.db); + let sid = session_id.clone(); + let needs_title = session.title.as_deref().unwrap_or("").is_empty(); + // Substring fallback so the sidebar has a label immediately. + let fallback: String = message.chars().take(60).collect(); + let _ = tokio::task::spawn_blocking(move || { + let _ = db.set_session_status(&sid, "active"); + if needs_title && !fallback.trim().is_empty() { + let _ = db.set_session_title(&sid, fallback.trim()); + } + }) + .await; + + // Then refine it asynchronously: use the configured title model, else fall + // back to the session's own model. (If that LLM call fails, the substring + // title set above stays — i.e. "user message only".) + if needs_title && !message.trim().is_empty() { + let (title_provider, title_model) = read_selection(app_state) + .title + .and_then(|r| resolve_ref(app_state, &r)) + .unwrap_or_else(|| (provider.clone(), model.clone())); + spawn_title_generation( + app_handle.clone(), + Arc::clone(&agent_state.db), + session_id.clone(), + message.clone(), + title_provider, + title_model, + ); + } + } + + let relay_handle = spawn_event_relay( + emitter, + event_rx, + session_id.clone(), + Some(Arc::clone(&agent_state.db)), + folder.clone(), + Some(agent_state.checkpoint_dir()), + ); + + // Monitor: clear the folder lock + flip status to idle when the relay ends. + { + let running = Arc::clone(&agent_state.running_folders); + let db = Arc::clone(&agent_state.db); + let sid = session_id.clone(); + let folder = folder.clone(); + tokio::spawn(async move { + let _ = relay_handle.await; + running.write().await.remove(&folder); + let _ = tokio::task::spawn_blocking(move || db.set_session_status(&sid, "idle")).await; + }); + } + + Ok(()) +} + +/// Cancel a running session. +#[tauri::command] +pub async fn agent_cancel_session( + session_id: String, + agent_state: State<'_, AgentState>, +) -> Result<(), String> { + { + let guard = agent_state.session_manager.read().await; + if let Some(ref manager) = *guard { + manager.cancel_session(&session_id).await; + } + } + agent_state.approval_handlers.write().await.remove(&session_id); + Ok(()) +} + +/// Resolve a pending tool approval. +#[tauri::command] +pub async fn agent_approve_tool( + agent_state: State<'_, AgentState>, + session_id: String, + tool_call_id: String, + approved: bool, +) -> Result<(), String> { + let handlers = agent_state.approval_handlers.read().await; + if let Some(handler) = handlers.get(&session_id) { + handler.resolve(&tool_call_id, approved); + Ok(()) + } else { + Err(format!("No approval handler for session '{session_id}'")) + } +} + +/// Fetch displayable (user/assistant text) messages for a session. +#[tauri::command] +pub async fn agent_get_messages( + session_id: String, + agent_state: State<'_, AgentState>, +) -> Result, String> { + let db = Arc::clone(&agent_state.db); + let sid = session_id.clone(); + let stored = tokio::task::spawn_blocking(move || db.load_session_messages(&sid)) + .await + .map_err(|e| format!("join error: {e}"))? + .map_err(|e| format!("Failed to load messages: {e}"))?; + + // Walk rows chronologically: accumulate tool_call chips and attach them to + // the next assistant text message (the "Thought for…" collapsible). This + // replaces the old `` marker scheme (chat-DB compat). + let mut out = Vec::new(); + let mut pending_tools: Vec = Vec::new(); + let mut pending_started: Option = None; + for msg in &stored { + if msg.type_ == "tool_call" { + if pending_started.is_none() { + pending_started = Some(msg.created_at.clone()); + } + pending_tools.extend(parse_tool_chips(&msg.llm_message)); + continue; + } + if (msg.role != "user" && msg.role != "assistant") || msg.type_ != "text" { + continue; + } + // Extract text + image data-URLs (handles multimodal block arrays, which + // a bare `content.as_str()` would miss — silently dropping image messages). + let (text, images) = + crate::agent_bridge::db::extract_display_content(&msg.llm_message, &msg.session_id); + if text.is_empty() && images.is_empty() { + if msg.role == "user" { + pending_tools.clear(); + pending_started = None; + } + continue; + } + let (tools, duration_seconds) = if msg.role == "assistant" { + let secs = pending_started + .as_deref() + .map(|start| elapsed_secs(start, &msg.created_at)) + .unwrap_or(0); + pending_started = None; + (std::mem::take(&mut pending_tools), secs) + } else { + pending_tools.clear(); + pending_started = None; + (Vec::new(), 0) + }; + out.push(AgentDisplayMessage { + id: msg.id.to_string(), + role: msg.role.clone(), + text, + created_at: msg.created_at.clone(), + session_id: msg.session_id.clone(), + tools, + duration_seconds, + images, + }); + } + Ok(out) +} + +// ── Context usage / clear / compact ──────────────────────────────────────── + +#[tauri::command] +pub async fn agent_get_context_usage( + session_id: String, + agent_state: State<'_, AgentState>, + app_state: State<'_, AppState>, +) -> Result, String> { + let db = Arc::clone(&agent_state.db); + let resolved = db + .get_session(&session_id) + .ok() + .flatten() + .and_then(|s| session_model(&app_state, &s).ok()); + // Tri-state: known/discovered → Some(n); unknown → None (no max/percentage). + let current_limit = match resolved { + Some((ref provider, ref model)) => { + resolve_capability(&agent_state, provider, model).await.context_limit.map(|n| n as u32) + } + None => None, + }; + let sid = session_id.clone(); + let usage = tokio::task::spawn_blocking(move || db.get_context_usage(&sid)) + .await + .map_err(|e| format!("join error: {e}"))? + .map_err(|e| format!("Failed to get context usage: {e}"))?; + Ok(usage.map(|(t, _persisted, m)| ContextUsageResponse { + total_tokens: t, + context_limit: current_limit, + message_count: m, + })) +} + +#[tauri::command] +pub async fn agent_clear_context( + session_id: String, + agent_state: State<'_, AgentState>, +) -> Result<(), String> { + let db = Arc::clone(&agent_state.db); + let folder = db.get_session(&session_id).ok().flatten().map(|s| s.folder).unwrap_or_default(); + let sid = session_id.clone(); + tokio::task::spawn_blocking(move || db.insert_context_reset(&sid, &folder)) + .await + .map_err(|e| format!("join error: {e}"))? + .map_err(|e| format!("Failed to clear context: {e}")) +} + +#[tauri::command] +pub async fn agent_compact_context( + session_id: String, + agent_state: State<'_, AgentState>, + app_state: State<'_, AppState>, +) -> Result { + let db = Arc::clone(&agent_state.db); + let folder = db.get_session(&session_id).ok().flatten().map(|s| s.folder).unwrap_or_default(); + + let db_load = Arc::clone(&db); + let sid_load = session_id.clone(); + let stored = tokio::task::spawn_blocking(move || db_load.load_session_messages(&sid_load)) + .await + .map_err(|e| format!("join error: {e}"))? + .map_err(|e| format!("Failed to load messages: {e}"))?; + let messages = deserialize_context(stored); + + let keep_recent = agent::agent::config::CompactionConfig::default().keep_recent_messages; + let Some((start, end)) = agent::agent::compaction::compaction_boundaries(&messages, keep_recent) else { + return Ok("nothing_to_compact".to_string()); + }; + + let prompt = agent::agent::compaction::build_compaction_prompt(&messages[start..end]); + let compact_msgs = vec![ + ChatMessage::system( + "Summarize this conversation. Structure: ## Goal, ## Key Decisions, ## Work Completed, \ + ## Current State, ## Relevant Files. Preserve exact file paths, function names, error \ + messages, and technical details. Be concise but complete." + .to_string(), + ), + ChatMessage::user(prompt), + ]; + + // Prefer the global compaction selection; else fall back to the session's own model. + let (provider, comp_model) = read_selection(&app_state) + .compaction + .and_then(|r| resolve_ref(&app_state, &r)) + .map(Ok) + .unwrap_or_else(|| { + db.get_session(&session_id) + .ok() + .flatten() + .map(|s| session_model(&app_state, &s)) + .unwrap_or_else(|| Err("No model selected. Pick one in Settings.".to_string())) + })?; + let mut compaction_cfg = provider_to_llm_config(&provider, &comp_model); + compaction_cfg.disable_cache_control = true; + let client = LlmClient::new(compaction_cfg); + let (silent_tx, _rx) = tokio::sync::mpsc::channel(1); + let compact_sid = format!("compact-{}", uuid::Uuid::new_v4()); + + let mut summary_text: Option = None; + for _ in 0..2u8 { + match client.chat_completion(&compact_msgs, &[], &silent_tx, &compact_sid, None).await { + Ok(response) => { + let text = response.content.unwrap_or_default(); + if !text.trim().is_empty() { + summary_text = Some(text); + break; + } + } + Err(e) => log::error!("[compact] LLM error: {e}"), + } + } + + let (result_type, summary) = match summary_text { + Some(text) => ("compacted", format!("[Context summary from earlier in this conversation]\n{text}")), + None => ("truncated", "[Earlier context was truncated due to length.]".to_string()), + }; + + let db_w = Arc::clone(&db); + let sid_w = session_id.clone(); + let folder_w = folder.clone(); + let start_u = start as u32; + tokio::task::spawn_blocking(move || db_w.insert_compaction(&sid_w, &folder_w, &summary, start_u)) + .await + .map_err(|e| format!("join error: {e}"))? + .map_err(|e| format!("Failed to save compaction: {e}"))?; + + let db_d = Arc::clone(&db); + let sid_d = session_id.clone(); + let _ = tokio::task::spawn_blocking(move || db_d.delete_context_usage(&sid_d)).await; + + Ok(result_type.to_string()) +} + +// ── Working-tree diffs ───────────────────────────────────────────────────── + +async fn intent_to_add_new_files(repo: &std::path::Path) { + let _ = git_ops::exec::run_git(repo, &["add", "-N", "."]).await; +} + +#[tauri::command] +pub async fn agent_get_diff(project_path: String) -> Result { + let repo = std::path::Path::new(&project_path); + intent_to_add_new_files(repo).await; + let result = git_ops::diff(repo, None, false, None).await.map_err(|e| e.to_string())?; + Ok(AgentDiffResult { + files_changed: result.files_changed, + insertions: result.insertions, + deletions: result.deletions, + stat: result.stat, + diff: result.diff, + }) +} + +#[tauri::command] +pub async fn agent_get_working_diff( + project_path: String, + files: Option>, +) -> Result { + let repo = std::path::Path::new(&project_path); + intent_to_add_new_files(repo).await; + let file_refs: Option> = files.as_ref().map(|f| f.iter().map(|s| s.as_str()).collect()); + let result = git_ops::diff(repo, file_refs.as_deref(), false, None).await.map_err(|e| e.to_string())?; + Ok(AgentDiffResult { + files_changed: result.files_changed, + insertions: result.insertions, + deletions: result.deletions, + stat: result.stat, + diff: result.diff, + }) +} + +// ── Providers (endpoints) + model selections ──────────────────────────────── + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ProvidersResponse { + pub providers: Vec, + pub selection: ModelSelection, +} + +#[tauri::command] +pub async fn agent_list_providers(app_state: State<'_, AppState>) -> Result { + Ok(ProvidersResponse { + providers: read_providers(&app_state), + selection: read_selection(&app_state), + }) +} + +/// Add an OpenAI-compatible provider (the built-in OpenAI/Anthropic rows already exist). +#[tauri::command] +pub async fn agent_add_provider( + provider: ProviderConfig, + app_state: State<'_, AppState>, +) -> Result { + let mut providers = read_providers(&app_state); + let mut provider = provider; + if provider.id.trim().is_empty() { + provider.id = uuid::Uuid::new_v4().to_string(); + } + if provider.is_builtin() { + return Err("Built-in providers already exist; add an OpenAI-compatible one".to_string()); + } + providers.push(provider.clone()); + write_providers(&app_state, &providers)?; + Ok(provider) +} + +#[tauri::command] +pub async fn agent_update_provider( + provider: ProviderConfig, + app_state: State<'_, AppState>, +) -> Result<(), String> { + let mut providers = read_providers(&app_state); + match providers.iter_mut().find(|p| p.id == provider.id) { + Some(slot) => *slot = provider, + None => return Err(format!("Provider not found: {}", provider.id)), + } + write_providers(&app_state, &providers) +} + +#[tauri::command] +pub async fn agent_delete_provider(id: String, app_state: State<'_, AppState>) -> Result<(), String> { + let mut providers = read_providers(&app_state); + if providers.iter().any(|p| p.id == id && p.is_builtin()) { + return Err("Built-in providers can't be deleted".to_string()); + } + providers.retain(|p| p.id != id); + write_providers(&app_state, &providers)?; + // Clear any selections that pointed at the removed provider. + let mut sel = read_selection(&app_state); + let clear = |r: &mut Option| { + if r.as_ref().map(|x| x.provider_id == id).unwrap_or(false) { + *r = None; + } + }; + clear(&mut sel.active); + clear(&mut sel.compaction); + clear(&mut sel.title); + write_selection(&app_state, &sel) +} + +/// Set one of the global model selections. `role` ∈ "active" | "compaction" | "title". +#[tauri::command] +pub async fn agent_set_model_selection( + role: String, + provider_id: String, + model: String, + app_state: State<'_, AppState>, +) -> Result<(), String> { + if provider_by_id(&app_state, &provider_id).is_none() { + return Err(format!("Provider not found: {provider_id}")); + } + let mut sel = read_selection(&app_state); + let r = Some(ModelRef { provider_id, model }); + match role.as_str() { + "active" => sel.active = r, + "compaction" => sel.compaction = r, + "title" => sel.title = r, + other => return Err(format!("Unknown selection role: {other}")), + } + write_selection(&app_state, &sel) +} + +/// Both OpenAI (`/models`) and Anthropic (`/v1/models`) return `{ "data": [ { "id": .. } ] }`. +#[derive(Deserialize)] +struct ModelsListResponse { + #[serde(default)] + data: Vec, +} + +#[derive(Deserialize)] +struct ModelEntry { + id: String, + /// OpenRouter / vLLM style. Anthropic + OpenAI don't return this → stays None. + #[serde(default)] + context_length: Option, + #[serde(default)] + max_context_length: Option, +} + +/// A model advertised by a provider's `/models`, with any discovered context length. +#[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct FetchedModel { + pub id: String, + pub context_length: Option, +} + +/// Build the GET request to a provider's models endpoint (URL + auth headers). +fn models_request(provider: &ProviderConfig) -> Result { + use reqwest::header::{HeaderMap, AUTHORIZATION, HeaderValue}; + + let base = provider.base_url.trim_end_matches('/'); + let mut headers = HeaderMap::new(); + let url = match provider.provider() { + Provider::Anthropic => { + if !provider.api_key.is_empty() { + if let Ok(v) = HeaderValue::from_str(&provider.api_key) { + headers.insert("x-api-key", v); + } + } + headers.insert( + "anthropic-version", + HeaderValue::from_static(agent::llm::anthropic::ANTHROPIC_VERSION), + ); + format!("{base}/v1/models") + } + Provider::OpenAI => { + if !provider.api_key.is_empty() { + if let Ok(v) = HeaderValue::from_str(&format!("Bearer {}", provider.api_key)) { + headers.insert(AUTHORIZATION, v); + } + } + format!("{base}/models") + } + }; + + let client = reqwest::Client::builder() + .timeout(std::time::Duration::from_secs(8)) + .build() + .map_err(|e| format!("HTTP client error: {e}"))?; + Ok(client.get(&url).headers(headers)) +} + +/// Query the provider's models endpoint and return the advertised models plus +/// any discovered context length. Uses the draft's base_url + api_key + kind +/// (so it works before saving). +#[tauri::command] +pub async fn agent_fetch_provider_models(provider: ProviderConfig) -> Result, String> { + let resp = models_request(&provider)? + .send() + .await + .map_err(|e| format!("Request failed: {e}"))?; + if !resp.status().is_success() { + return Err(format!("Models endpoint returned {}", resp.status())); + } + let body = resp.text().await.map_err(|e| format!("Failed to read response: {e}"))?; + let parsed: ModelsListResponse = + serde_json::from_str(&body).map_err(|_| "Unexpected models response shape".to_string())?; + Ok(parsed + .data + .into_iter() + .map(|m| FetchedModel { id: m.id, context_length: m.context_length.or(m.max_context_length) }) + .collect()) +} + +/// Key check used before saving. Sends a minimal "hi" (max 16 tokens) through the +/// SAME native path the agent uses (`/chat/completions` or `/v1/messages`), so it +/// validates the key + that the model actually responds. Rejects ONLY on a clear +/// auth failure (401/403); other errors (bad model, network, no models picked yet) +/// are inconclusive → allowed, so saving is never blocked spuriously. +#[tauri::command] +pub async fn agent_verify_provider(provider: ProviderConfig) -> Result<(), String> { + if provider.api_key.is_empty() { + return Ok(()); // nothing to verify + } + // Probe the first configured model; if none picked yet, fall back to a + // models-endpoint key check so we can still catch a bad key. + let Some(probe_model) = provider.models.first().cloned() else { + return match models_request(&provider)?.send().await { + Ok(resp) if resp.status() == reqwest::StatusCode::UNAUTHORIZED + || resp.status() == reqwest::StatusCode::FORBIDDEN => + { + Err(format!("API key rejected ({})", resp.status().as_u16())) + } + _ => Ok(()), + }; + }; + + let mut cfg = provider_to_llm_config(&provider, &probe_model); + cfg.max_completion_tokens = Some(16); + let client = LlmClient::new(cfg); + let (tx, _rx) = tokio::sync::mpsc::channel(8); + let sid = format!("verify-{}", uuid::Uuid::new_v4()); + match client + .chat_completion(&[ChatMessage::user("hi")], &[], &tx, &sid, None) + .await + { + Ok(_) => Ok(()), + Err(agent::error::AgentError::LlmApiError { status, .. }) if status == 401 || status == 403 => { + Err(format!("API key rejected ({status})")) + } + Err(_) => Ok(()), // bad model / network — inconclusive, don't block saving + } +} + +// ── Permissions ──────────────────────────────────────────────────────────── + +#[tauri::command] +pub fn agent_get_permissions( + state: State, + project_path: Option, +) -> Result { + let conn = state.db.conn.lock(); + Ok(crate::agent_bridge::permissions::get_permission(&conn, project_path.as_deref())) +} + +#[tauri::command] +pub fn agent_set_permission(state: State, config: PermissionConfig) -> Result<(), String> { + let conn = state.db.conn.lock(); + crate::agent_bridge::permissions::set_permission(&conn, &config) +} + +// ── Skills / subagents ───────────────────────────────────────────────────── + +fn is_valid_name(name: &str) -> bool { + let len = name.len(); + (1..=64).contains(&len) + && name.bytes().all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-') +} + +#[tauri::command] +pub fn agent_list_skills( + agent_state: State, + working_dir: Option, +) -> Result, String> { + let wd = working_dir.map(PathBuf::from); + Ok(crate::agent_bridge::skills::list_all_for_dialog(&agent_state.db, wd.as_deref())) +} + +#[tauri::command] +pub fn agent_get_skills_paths( + working_dir: Option, +) -> Result { + let wd = working_dir.map(PathBuf::from); + Ok(crate::agent_bridge::skills::paths_for_display(wd.as_deref())) +} + +#[tauri::command] +pub fn agent_set_skill_enabled( + agent_state: State, + name: String, + enabled: bool, +) -> Result<(), String> { + if !is_valid_name(&name) { + return Err(format!("invalid skill name: {name:?}")); + } + agent_state.db.set_skill_enabled(&name, enabled).map_err(|e| e.to_string()) +} + +#[tauri::command] +pub fn agent_list_subagents( + agent_state: State, + working_dir: Option, +) -> Result, String> { + let wd = working_dir.map(PathBuf::from); + Ok(crate::agent_bridge::subagents::list_all_for_dialog(&agent_state.db, wd.as_deref())) +} + +#[tauri::command] +pub fn agent_get_subagents_paths( + working_dir: Option, +) -> Result { + let wd = working_dir.map(PathBuf::from); + Ok(crate::agent_bridge::subagents::paths_for_display(wd.as_deref())) +} + +#[tauri::command] +pub fn agent_set_subagent_enabled( + agent_state: State, + name: String, + enabled: bool, +) -> Result<(), String> { + if !is_valid_name(&name) { + return Err(format!("invalid subagent name: {name:?}")); + } + agent_state.db.set_subagent_enabled(&name, enabled).map_err(|e| e.to_string()) +} + +// ── Checkpoints (snapshot-backed) ────────────────────────────────────────── + +#[tauri::command] +pub async fn agent_list_checkpoints( + session_id: String, + agent_state: State<'_, AgentState>, +) -> Result, String> { + let turns = git_ops::checkpoint::list(&agent_state.checkpoint_root, &session_id) + .await + .unwrap_or_default(); + Ok(turns + .into_iter() + .map(|t| CheckpointInfo { turn: t.turn, file_count: t.file_count, paths: t.paths }) + .collect()) +} + +#[tauri::command] +pub async fn agent_get_turn_diff( + session_id: String, + turn: u32, + agent_state: State<'_, AgentState>, +) -> Result { + let d = git_ops::checkpoint::diff_turn(&agent_state.checkpoint_root, &session_id, turn) + .await + .map_err(|e| format!("Failed to diff turn: {e}"))?; + Ok(AgentDiffResult { + files_changed: d.files_changed, + insertions: d.insertions, + deletions: d.deletions, + stat: d.stat, + diff: d.diff, + }) +} + +/// Cumulative working-tree diff of the session's project folder (uncommitted changes). +#[tauri::command] +pub async fn agent_get_full_diff( + session_id: String, + agent_state: State<'_, AgentState>, +) -> Result { + let folder = { + let db = Arc::clone(&agent_state.db); + let sid = session_id.clone(); + tokio::task::spawn_blocking(move || db.get_session(&sid)) + .await + .map_err(|e| format!("join error: {e}"))? + .map_err(|e| format!("db error: {e}"))? + .map(|s| s.folder) + .ok_or("Session not found")? + }; + let repo = std::path::Path::new(&folder); + intent_to_add_new_files(repo).await; + let result = git_ops::diff(repo, None, false, None).await.map_err(|e| e.to_string())?; + Ok(AgentDiffResult { + files_changed: result.files_changed, + insertions: result.insertions, + deletions: result.deletions, + stat: result.stat, + diff: result.diff, + }) +} + +/// Restore the project files to a checkpoint turn and prune forward turns + messages. +#[tauri::command] +pub async fn agent_restore_checkpoint( + session_id: String, + turn: u32, + agent_state: State<'_, AgentState>, + app: AppHandle, +) -> Result<(), String> { + // Cancel any running session first. + { + let guard = agent_state.session_manager.read().await; + if let Some(ref manager) = *guard { + manager.cancel_session(&session_id).await; + } + } + agent_state.approval_handlers.write().await.remove(&session_id); + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + + let folder = { + let db = Arc::clone(&agent_state.db); + let sid = session_id.clone(); + tokio::task::spawn_blocking(move || db.get_session(&sid)) + .await + .ok() + .and_then(Result::ok) + .flatten() + .map(|s| s.folder) + .unwrap_or_default() + }; + git_ops::checkpoint::restore_to( + &agent_state.checkpoint_root, + &session_id, + turn, + std::path::Path::new(&folder), + ) + .await + .map_err(|e| format!("Failed to restore checkpoint: {e}"))?; + let _ = git_ops::checkpoint::delete_from(&agent_state.checkpoint_root, &session_id, turn + 1).await; + + // Drop messages from undone turns so the conversation matches the files. + let db = Arc::clone(&agent_state.db); + let sid = session_id.clone(); + let _ = tokio::task::spawn_blocking(move || db.rewind_from_turn(&sid, turn + 1)).await; + + let _ = app.emit( + "agent:checkpoint_restored", + serde_json::json!({ "thread_id": session_id, "turn_count": turn }), + ); + Ok(()) +} + +/// Rewind to a user message (optionally restoring files) and re-run with new text. +#[tauri::command] +pub async fn agent_rewind_to_message( + app: AppHandle, + session_id: String, + message_sqlite_id: i64, + restore_code: bool, + new_text: String, + attachments: Option>, + agent_state: State<'_, AgentState>, + app_state: State<'_, AppState>, +) -> Result { + // Block if the session is still running. + if agent_state.running_folders.read().await.iter().next().is_some() { + // Cheap guard: refuse if anything is running; precise per-folder check below. + } + + let target = { + let db = Arc::clone(&agent_state.db); + tokio::task::spawn_blocking(move || db.get_message_by_id(message_sqlite_id)) + .await + .map_err(|e| format!("join error: {e}"))? + .map_err(|e| format!("db error: {e}"))? + .ok_or("Message not found")? + }; + if target.role != "user" { + return Err("Can only rewind to user messages".into()); + } + if target.session_id != session_id { + return Err("Message does not belong to this session".into()); + } + + // Soft-delete this message and everything after it. + { + let db = Arc::clone(&agent_state.db); + let sid = session_id.clone(); + let _ = tokio::task::spawn_blocking(move || db.rewind_messages(&sid, target.id)).await; + } + + let session = { + let db = Arc::clone(&agent_state.db); + let sid = session_id.clone(); + tokio::task::spawn_blocking(move || db.get_session(&sid)) + .await + .map_err(|e| format!("join error: {e}"))? + .map_err(|e| format!("db error: {e}"))? + .ok_or("Session not found")? + }; + + if restore_code { + let target_turn = target.turn_count.unwrap_or(1); + let restore_turn = target_turn.saturating_sub(1); + let _ = git_ops::checkpoint::restore_to( + &agent_state.checkpoint_root, + &session_id, + restore_turn, + std::path::Path::new(&session.folder), + ) + .await; + let _ = git_ops::checkpoint::delete_from(&agent_state.checkpoint_root, &session_id, target_turn).await; + } + + run_agent_turn(app, &app_state, &agent_state, session, new_text, attachments, None).await?; + Ok(SendMessageResponse { session_id }) +} diff --git a/apps/desktop/src-tauri/src/agent_bridge/db.rs b/apps/desktop/src-tauri/src/agent_bridge/db.rs new file mode 100644 index 00000000..3eb423b9 --- /dev/null +++ b/apps/desktop/src-tauri/src/agent_bridge/db.rs @@ -0,0 +1,1129 @@ +use std::path::Path; +use std::sync::Arc; + +use async_trait::async_trait; +use parking_lot::Mutex; +use rusqlite::Connection; + +use agent::persistence::{ + self, AgentMessage, MessagePersister, MessageRole, MessageType, PersistError, PersistResult, + Sender, +}; + +// ── AgentDb ──────────────────────────────────────────────────────────────── +// +// Greenfield single-user local store. Schema v1 — no migration ladder carried +// over from the chat product. Sessions are the unit of work (folder + mode); +// messages are keyed purely by `session_id`. Checkpoints live outside SQLite, +// in the file-snapshot dir managed by `git-ops`. + +/// Local SQLite database for agent sessions + LLM message history. +pub struct AgentDb { + conn: Mutex, +} + +/// A row from the `sessions` table — the unit shown in the session-list sidebar. +#[derive(Debug, Clone, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionRow { + pub id: String, + pub folder: String, + /// "ask" | "plan" | "coding" + pub mode: String, + pub title: Option, + pub parent_session_id: Option, + pub created_at: String, + pub updated_at: String, + /// "active" | "idle" | "error" + pub status: String, + /// Provider + model this session was created with (resume uses the same one). + /// `None` for legacy sessions → falls back to the active selection. + pub provider_id: Option, + pub model: Option, +} + +/// Raw row from the `agent_messages` table. +#[derive(Debug, Clone)] +#[allow(dead_code)] +pub struct StoredMessage { + pub id: i64, + pub session_id: String, + pub project_path: String, + pub role: String, + pub type_: String, + pub llm_message: String, + pub metadata: String, + pub created_at: String, + pub rewound_at: Option, + pub turn_count: Option, +} + +/// Error type for AgentDb operations. +#[derive(Debug, thiserror::Error)] +pub enum AgentDbError { + #[error("SQLite error: {0}")] + Sqlite(#[from] rusqlite::Error), + #[error("IO error: {0}")] + Io(#[from] std::io::Error), + #[error("JSON error: {0}")] + Json(#[from] serde_json::Error), +} + +impl AgentDb { + /// Open or create `agent_data.db` in the given directory with the v1 schema. + pub fn new(data_dir: &Path) -> Result { + std::fs::create_dir_all(data_dir)?; + let db_path = data_dir.join("agent_data.db"); + let conn = Connection::open(&db_path)?; + + conn.execute_batch( + "PRAGMA journal_mode = WAL; + PRAGMA busy_timeout = 5000; + PRAGMA synchronous = NORMAL; + PRAGMA wal_autocheckpoint = 400; + PRAGMA cache_size = -32768;", + )?; + + // Fresh-DB tuning: only set page_size before the first table is written. + let initialized: bool = conn + .query_row( + "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='sessions'", + [], + |row| row.get::<_, i64>(0), + ) + .map(|c| c > 0)?; + if !initialized { + conn.execute_batch("PRAGMA page_size = 8192;")?; + } + + conn.execute_batch( + "CREATE TABLE IF NOT EXISTS sessions ( + id TEXT PRIMARY KEY, + folder TEXT NOT NULL, + mode TEXT NOT NULL, + title TEXT, + parent_session_id TEXT, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'active', + provider_id TEXT, + model TEXT, + deleted_at TEXT + ); + CREATE INDEX IF NOT EXISTS idx_sessions_updated ON sessions(updated_at DESC); + CREATE INDEX IF NOT EXISTS idx_sessions_folder ON sessions(folder); + + CREATE TABLE IF NOT EXISTS agent_messages ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + session_id TEXT NOT NULL DEFAULT '', + project_path TEXT NOT NULL DEFAULT '', + role TEXT NOT NULL, + type TEXT NOT NULL, + llm_message TEXT NOT NULL, + metadata TEXT NOT NULL DEFAULT '{}', + created_at TEXT NOT NULL, + rewound_at TEXT DEFAULT NULL, + turn_count INTEGER DEFAULT NULL + ); + CREATE INDEX IF NOT EXISTS idx_msgs_session + ON agent_messages(session_id, created_at); + CREATE INDEX IF NOT EXISTS idx_msgs_session_active + ON agent_messages(session_id, rewound_at); + CREATE INDEX IF NOT EXISTS idx_msgs_type + ON agent_messages(session_id, type); + + CREATE TABLE IF NOT EXISTS skill_prefs ( + skill_name TEXT PRIMARY KEY, + enabled INTEGER NOT NULL DEFAULT 1 + ); + + CREATE TABLE IF NOT EXISTS subagent_prefs ( + subagent_name TEXT PRIMARY KEY, + enabled INTEGER NOT NULL DEFAULT 1 + ); + + PRAGMA user_version = 1;", + )?; + + // Idempotent migrations: add `provider_id` + `model` to sessions tables + // created before multi-provider support. Ignore "duplicate column". + for col in ["provider_id", "model", "deleted_at"] { + if let Err(e) = conn.execute(&format!("ALTER TABLE sessions ADD COLUMN {col} TEXT"), []) { + if !e.to_string().contains("duplicate column") { + return Err(e.into()); + } + } + } + + Ok(Self { + conn: Mutex::new(conn), + }) + } + + #[allow(dead_code)] + pub fn schema_version(&self) -> Result { + let conn = self.conn.lock(); + Ok(conn.query_row("PRAGMA user_version", [], |row| row.get(0))?) + } + + // ── Sessions ───────────────────────────────────────────────────────── + + /// Insert a new session row. `created_at`/`updated_at` are set to now. + #[allow(clippy::too_many_arguments)] + pub fn create_session( + &self, + id: &str, + folder: &str, + mode: &str, + title: Option<&str>, + parent_session_id: Option<&str>, + provider_id: Option<&str>, + model: Option<&str>, + ) -> Result<(), AgentDbError> { + let conn = self.conn.lock(); + let now = now_iso(); + conn.execute( + "INSERT INTO sessions (id, folder, mode, title, parent_session_id, created_at, updated_at, status, provider_id, model) + VALUES (?, ?, ?, ?, ?, ?, ?, 'idle', ?, ?)", + rusqlite::params![id, folder, mode, title, parent_session_id, now, now, provider_id, model], + )?; + Ok(()) + } + + /// Get a single session by id. + pub fn get_session(&self, id: &str) -> Result, AgentDbError> { + let conn = self.conn.lock(); + let result = conn.query_row( + "SELECT id, folder, mode, title, parent_session_id, created_at, updated_at, status, provider_id, model + FROM sessions WHERE id = ?", + [id], + map_session_row, + ); + match result { + Ok(row) => Ok(Some(row)), + Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), + Err(e) => Err(e.into()), + } + } + + /// List all sessions, most-recently-updated first. + pub fn list_sessions(&self) -> Result, AgentDbError> { + let conn = self.conn.lock(); + let mut stmt = conn.prepare( + "SELECT id, folder, mode, title, parent_session_id, created_at, updated_at, status, provider_id, model + FROM sessions WHERE deleted_at IS NULL ORDER BY updated_at DESC", + )?; + let rows = stmt.query_map([], map_session_row)?; + let mut out = Vec::new(); + for r in rows { + out.push(r?); + } + Ok(out) + } + + /// Set a session's status and bump `updated_at`. + pub fn set_session_status(&self, id: &str, status: &str) -> Result<(), AgentDbError> { + let conn = self.conn.lock(); + conn.execute( + "UPDATE sessions SET status = ?, updated_at = ? WHERE id = ?", + rusqlite::params![status, now_iso(), id], + )?; + Ok(()) + } + + /// Soft-delete a session: stamp `deleted_at` so it drops out of the sidebar + /// list while its messages + checkpoints stay on disk (recoverable). + pub fn delete_session(&self, id: &str) -> Result<(), AgentDbError> { + let conn = self.conn.lock(); + conn.execute( + "UPDATE sessions SET deleted_at = ?, updated_at = ? WHERE id = ?", + rusqlite::params![now_iso(), now_iso(), id], + )?; + Ok(()) + } + + /// Set a session's title (first message preview, etc.) and bump `updated_at`. + pub fn set_session_title(&self, id: &str, title: &str) -> Result<(), AgentDbError> { + let conn = self.conn.lock(); + conn.execute( + "UPDATE sessions SET title = ?, updated_at = ? WHERE id = ?", + rusqlite::params![title, now_iso(), id], + )?; + Ok(()) + } + + /// Re-pin a session's provider + model (when the user switches the picker for + /// an open session). Subsequent turns + context sizing use the new model. + pub fn set_session_model(&self, id: &str, provider_id: &str, model: &str) -> Result<(), AgentDbError> { + let conn = self.conn.lock(); + conn.execute( + "UPDATE sessions SET provider_id = ?, model = ?, updated_at = ? WHERE id = ?", + rusqlite::params![provider_id, model, now_iso(), id], + )?; + Ok(()) + } + + /// Set a session's current mode ("ask" | "plan" | "coding") and bump `updated_at`. + pub fn set_session_mode(&self, id: &str, mode: &str) -> Result<(), AgentDbError> { + let conn = self.conn.lock(); + conn.execute( + "UPDATE sessions SET mode = ?, updated_at = ? WHERE id = ?", + rusqlite::params![mode, now_iso(), id], + )?; + Ok(()) + } + + /// Bump a session's `updated_at` to now (recency for the sidebar). + pub fn touch_session(&self, id: &str) -> Result<(), AgentDbError> { + let conn = self.conn.lock(); + conn.execute( + "UPDATE sessions SET updated_at = ? WHERE id = ?", + rusqlite::params![now_iso(), id], + )?; + Ok(()) + } + + /// Return the id of an active session for the given folder, if any. + /// Used to enforce one active session per folder. + pub fn active_session_for_folder(&self, folder: &str) -> Result, AgentDbError> { + let conn = self.conn.lock(); + let result = conn.query_row( + "SELECT id FROM sessions WHERE folder = ? AND status = 'active' AND deleted_at IS NULL ORDER BY updated_at DESC LIMIT 1", + [folder], + |row| row.get::<_, String>(0), + ); + match result { + Ok(id) => Ok(Some(id)), + Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), + Err(e) => Err(e.into()), + } + } + + // ── Skill / subagent prefs ─────────────────────────────────────────── + + pub fn load_disabled_skills(&self) -> Result, AgentDbError> { + let conn = self.conn.lock(); + let mut stmt = conn.prepare("SELECT skill_name FROM skill_prefs WHERE enabled = 0")?; + let rows = stmt.query_map([], |r| r.get::<_, String>(0))?; + Ok(rows.filter_map(Result::ok).collect()) + } + + pub fn set_skill_enabled(&self, name: &str, enabled: bool) -> Result<(), AgentDbError> { + let conn = self.conn.lock(); + conn.execute( + "INSERT INTO skill_prefs (skill_name, enabled) VALUES (?1, ?2) + ON CONFLICT(skill_name) DO UPDATE SET enabled = excluded.enabled", + rusqlite::params![name, enabled as i32], + )?; + Ok(()) + } + + pub fn load_disabled_subagents( + &self, + ) -> Result, AgentDbError> { + let conn = self.conn.lock(); + let mut stmt = conn.prepare("SELECT subagent_name FROM subagent_prefs WHERE enabled = 0")?; + let rows = stmt.query_map([], |r| r.get::<_, String>(0))?; + Ok(rows.filter_map(Result::ok).collect()) + } + + pub fn set_subagent_enabled(&self, name: &str, enabled: bool) -> Result<(), AgentDbError> { + let conn = self.conn.lock(); + conn.execute( + "INSERT INTO subagent_prefs (subagent_name, enabled) VALUES (?1, ?2) + ON CONFLICT(subagent_name) DO UPDATE SET enabled = excluded.enabled", + rusqlite::params![name, enabled as i32], + )?; + Ok(()) + } + + // ── Messages ───────────────────────────────────────────────────────── + + /// Insert a message for a session. Returns the row id as a string. + #[allow(clippy::too_many_arguments)] + pub fn insert_message( + &self, + session_id: &str, + project_path: &str, + role: &str, + type_: &str, + llm_message: &str, + metadata: &str, + turn_count: Option, + ) -> Result { + let conn = self.conn.lock(); + conn.execute( + "INSERT INTO agent_messages (session_id, project_path, role, type, llm_message, metadata, created_at, turn_count) + VALUES (?, ?, ?, ?, ?, ?, ?, ?)", + rusqlite::params![session_id, project_path, role, type_, llm_message, metadata, now_iso(), turn_count], + )?; + Ok(conn.last_insert_rowid().to_string()) + } + + /// Load all active (non-rewound) rows for a session, ordered by id ASC. + pub fn load_session_messages( + &self, + session_id: &str, + ) -> Result, AgentDbError> { + let conn = self.conn.lock(); + let mut stmt = conn.prepare( + "SELECT id, session_id, project_path, role, type, llm_message, metadata, created_at, rewound_at, turn_count + FROM agent_messages + WHERE session_id = ? AND rewound_at IS NULL + ORDER BY id ASC", + )?; + let rows = stmt.query_map([session_id], map_stored_message)?; + let mut out = Vec::new(); + for r in rows { + out.push(r?); + } + Ok(out) + } + + /// Load a session's messages optimized for LLM context (compaction-aware). + /// Mirrors the historical behaviour: load from the last compaction marker + /// (honoring `kept_before_count`), else fall back to the most recent + /// `fallback_limit` rows; then trim leading orphaned tool rows. + pub fn load_session_for_context( + &self, + session_id: &str, + fallback_limit: u32, + ) -> Result, AgentDbError> { + let conn = self.conn.lock(); + + let reset_floor: i64 = conn + .query_row( + "SELECT COALESCE(MAX(id), 0) FROM agent_messages + WHERE session_id = ? AND type = 'context_reset' AND rewound_at IS NULL", + [session_id], + |row| row.get(0), + ) + .unwrap_or(0); + + let last_compaction_id: Option = conn + .query_row( + "SELECT MAX(id) FROM agent_messages + WHERE session_id = ? AND type = 'compaction' AND rewound_at IS NULL AND id > ?", + rusqlite::params![session_id, reset_floor], + |row| row.get(0), + ) + .unwrap_or(None); + + let mut messages = if let Some(compaction_id) = last_compaction_id { + let kept_before: i64 = conn + .query_row( + "SELECT COALESCE(json_extract(metadata, '$.kept_before_count'), 0) FROM agent_messages WHERE id = ?", + [compaction_id], + |row| row.get(0), + ) + .unwrap_or(0); + + let start_id: i64 = if kept_before > 0 { + conn.query_row( + "SELECT COALESCE(MIN(id), ?) FROM ( + SELECT id FROM agent_messages + WHERE session_id = ? AND id < ? AND id > ? + AND type NOT IN ('compaction', 'context_usage', 'context_reset') + AND rewound_at IS NULL + ORDER BY id DESC LIMIT ? + )", + rusqlite::params![compaction_id, session_id, compaction_id, reset_floor, kept_before], + |row| row.get(0), + ) + .unwrap_or(compaction_id) + } else { + compaction_id + }; + + let mut stmt = conn.prepare( + "SELECT id, session_id, project_path, role, type, llm_message, metadata, created_at, rewound_at, turn_count + FROM agent_messages + WHERE session_id = ? AND id >= ? + AND type NOT IN ('context_usage', 'context_reset') + AND rewound_at IS NULL + ORDER BY id ASC", + )?; + let rows = stmt.query_map(rusqlite::params![session_id, start_id], map_stored_message)?; + let mut msgs = Vec::new(); + for r in rows { + msgs.push(r?); + } + msgs + } else { + let mut stmt = conn.prepare( + "SELECT id, session_id, project_path, role, type, llm_message, metadata, created_at, rewound_at, turn_count + FROM agent_messages + WHERE session_id = ? AND id > ? + AND type NOT IN ('context_usage', 'context_reset') + AND rewound_at IS NULL + ORDER BY id DESC LIMIT ?", + )?; + let rows = stmt.query_map( + rusqlite::params![session_id, reset_floor, fallback_limit], + map_stored_message, + )?; + let mut msgs = Vec::new(); + for r in rows { + msgs.push(r?); + } + msgs.reverse(); + msgs + }; + + // Trim leading orphaned tool rows so the context starts clean. + while let Some(first) = messages.first() { + if first.role == "tool" || (first.role == "assistant" && first.type_ == "tool_call") { + messages.remove(0); + } else { + break; + } + } + + Ok(messages) + } + + /// Get the latest `session_init` record for a session (if any). + pub fn get_session_init( + &self, + session_id: &str, + ) -> Result, AgentDbError> { + let conn = self.conn.lock(); + let result = conn.query_row( + "SELECT id, session_id, project_path, role, type, llm_message, metadata, created_at, rewound_at, turn_count + FROM agent_messages + WHERE session_id = ? AND type = 'session_init' + ORDER BY id DESC LIMIT 1", + [session_id], + map_stored_message, + ); + match result { + Ok(msg) => Ok(Some(msg)), + Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), + Err(e) => Err(e.into()), + } + } + + /// Get a single message by SQLite row id. + pub fn get_message_by_id(&self, id: i64) -> Result, AgentDbError> { + let conn = self.conn.lock(); + let result = conn.query_row( + "SELECT id, session_id, project_path, role, type, llm_message, metadata, created_at, rewound_at, turn_count + FROM agent_messages WHERE id = ?", + [id], + map_stored_message, + ); + match result { + Ok(msg) => Ok(Some(msg)), + Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), + Err(e) => Err(e.into()), + } + } + + /// Soft-delete all messages with id >= from_id for a session (rewind). + pub fn rewind_messages( + &self, + session_id: &str, + from_id: i64, + ) -> Result { + let conn = self.conn.lock(); + conn.execute( + "UPDATE agent_messages SET rewound_at = ? + WHERE session_id = ? AND id >= ? AND rewound_at IS NULL", + rusqlite::params![now_iso(), session_id, from_id], + )?; + Ok(conn.changes() as usize) + } + + /// Soft-delete all messages with turn_count >= from_turn for a session. + /// Used after a checkpoint restore so the conversation matches the files. + pub fn rewind_from_turn( + &self, + session_id: &str, + from_turn: u32, + ) -> Result { + let conn = self.conn.lock(); + conn.execute( + "UPDATE agent_messages SET rewound_at = ? + WHERE session_id = ? AND turn_count >= ? AND rewound_at IS NULL", + rusqlite::params![now_iso(), session_id, from_turn], + )?; + Ok(conn.changes() as usize) + } + + // ── Context usage / reset ──────────────────────────────────────────── + + /// Upsert token-usage stats for a session (one row per session). + pub fn upsert_context_usage( + &self, + session_id: &str, + project_path: &str, + total_tokens: u32, + context_limit: u32, + message_count: u32, + ) -> Result<(), AgentDbError> { + let conn = self.conn.lock(); + let metadata = serde_json::json!({ + "total_tokens": total_tokens, + "context_limit": context_limit, + "message_count": message_count, + }) + .to_string(); + + let tx = conn.unchecked_transaction()?; + tx.execute( + "DELETE FROM agent_messages WHERE session_id = ? AND type = 'context_usage'", + [session_id], + )?; + tx.execute( + "INSERT INTO agent_messages (session_id, project_path, role, type, llm_message, metadata, created_at) + VALUES (?, ?, 'system', 'context_usage', '{}', ?, ?)", + rusqlite::params![session_id, project_path, metadata, now_iso()], + )?; + tx.commit()?; + Ok(()) + } + + /// Get persisted (total_tokens, context_limit, message_count) for a session. + pub fn get_context_usage( + &self, + session_id: &str, + ) -> Result, AgentDbError> { + let conn = self.conn.lock(); + let result = conn.query_row( + "SELECT metadata FROM agent_messages + WHERE session_id = ? AND type = 'context_usage' LIMIT 1", + [session_id], + |row| row.get::<_, String>(0), + ); + match result { + Ok(meta_str) => { + let meta: serde_json::Value = serde_json::from_str(&meta_str).unwrap_or_default(); + Ok(Some(( + meta["total_tokens"].as_u64().unwrap_or(0) as u32, + meta["context_limit"].as_u64().unwrap_or(0) as u32, + meta["message_count"].as_u64().unwrap_or(0) as u32, + ))) + } + Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), + Err(e) => Err(e.into()), + } + } + + /// Delete the context_usage row for a session (after manual compaction). + pub fn delete_context_usage(&self, session_id: &str) -> Result<(), AgentDbError> { + let conn = self.conn.lock(); + conn.execute( + "DELETE FROM agent_messages WHERE session_id = ? AND type = 'context_usage'", + [session_id], + )?; + Ok(()) + } + + /// Insert a context_reset marker (hard floor for context loading) and clear usage. + pub fn insert_context_reset( + &self, + session_id: &str, + project_path: &str, + ) -> Result<(), AgentDbError> { + let conn = self.conn.lock(); + conn.execute( + "INSERT INTO agent_messages (session_id, project_path, role, type, llm_message, metadata, created_at) + VALUES (?, ?, 'system', 'context_reset', '{}', '{}', ?)", + rusqlite::params![session_id, project_path, now_iso()], + )?; + conn.execute( + "DELETE FROM agent_messages WHERE session_id = ? AND type = 'context_usage'", + [session_id], + )?; + Ok(()) + } + + /// Insert a compaction record. Mirrors the agent loop's own persisted format. + pub fn insert_compaction( + &self, + session_id: &str, + project_path: &str, + summary: &str, + kept_before_count: u32, + ) -> Result { + let metadata = serde_json::json!({ + "version": 1, + "kept_before_count": kept_before_count, + }) + .to_string(); + let llm = serde_json::json!({"role": "system", "content": summary}).to_string(); + self.insert_message(session_id, project_path, "system", "compaction", &llm, &metadata, None) + } +} + +// ── Row mappers ────────────────────────────────────────────────────────────── + +fn now_iso() -> String { + chrono::Utc::now() + .format("%Y-%m-%dT%H:%M:%S%.3fZ") + .to_string() +} + +fn map_session_row(row: &rusqlite::Row) -> rusqlite::Result { + Ok(SessionRow { + id: row.get(0)?, + folder: row.get(1)?, + mode: row.get(2)?, + title: row.get(3)?, + parent_session_id: row.get(4)?, + created_at: row.get(5)?, + updated_at: row.get(6)?, + status: row.get(7)?, + provider_id: row.get(8)?, + model: row.get(9)?, + }) +} + +/// Columns: id, session_id, project_path, role, type, llm_message, metadata, +/// created_at, rewound_at, turn_count (10 columns). +fn map_stored_message(row: &rusqlite::Row) -> rusqlite::Result { + Ok(StoredMessage { + id: row.get(0)?, + session_id: row.get(1)?, + project_path: row.get(2)?, + role: row.get(3)?, + type_: row.get(4)?, + llm_message: row.get(5)?, + metadata: row.get(6)?, + created_at: row.get(7)?, + rewound_at: row.get(8)?, + turn_count: row.get(9)?, + }) +} + +// ── Role/Type string conversions ─────────────────────────────────────────── + +fn role_to_str(role: MessageRole) -> &'static str { + match role { + MessageRole::User => "user", + MessageRole::Assistant => "assistant", + MessageRole::Tool => "tool", + MessageRole::System => "system", + } +} + +fn type_to_str(mt: MessageType) -> &'static str { + match mt { + MessageType::Text => "text", + MessageType::SessionInit => "session_init", + MessageType::Compaction => "compaction", + MessageType::ToolCall => "tool_call", + MessageType::ToolResult => "tool_result", + MessageType::CompletionSummary => "completion_summary", + } +} + +fn str_to_role(s: &str) -> MessageRole { + persistence::str_to_role(s) +} + +fn str_to_type(s: &str) -> MessageType { + persistence::str_to_type(s) +} + +// ── Context reconstruction ───────────────────────────────────────────────── + +/// Reconstruct LLM context from stored messages, applying compaction. +/// Finds the last compaction record, reads `kept_before_count`, and keeps that +/// many non-compaction messages before it plus everything after. +// ── Image persistence (disk-backed attachments) ──────────────────────────── +// +// Image bytes are stored on disk under /images//.; +// the persisted message keeps only a `supercoder-image:` reference so +// SQLite rows don't bloat with base64. The reference is rebuilt into a data-URL +// before the message reaches the LLM client (the wire format is unchanged). + +const IMAGE_REF_PREFIX: &str = "supercoder-image:"; + +/// On-disk image directory for a session. +pub fn images_dir(session_id: &str) -> std::path::PathBuf { + crate::app_data_dir().join("images").join(session_id) +} + +fn ext_for_media(media: &str) -> &'static str { + match media { + "image/png" => "png", + "image/jpeg" | "image/jpg" => "jpg", + "image/gif" => "gif", + "image/webp" => "webp", + "image/svg+xml" => "svg", + "image/bmp" => "bmp", + _ => "img", + } +} + +fn media_for_ext(ext: &str) -> String { + match ext { + "png" => "image/png", + "jpg" | "jpeg" => "image/jpeg", + "gif" => "image/gif", + "webp" => "image/webp", + "svg" => "image/svg+xml", + "bmp" => "image/bmp", + _ => "application/octet-stream", + } + .to_string() +} + +fn parse_data_uri(url: &str) -> Option<(String, String)> { + let rest = url.strip_prefix("data:")?; + let (meta, data) = rest.split_once(',')?; + let media = meta.strip_suffix(";base64").unwrap_or(meta); + if media.is_empty() { + return None; + } + Some((media.to_string(), data.to_string())) +} + +/// Replace inline `data:` image URLs with on-disk references before persisting. +/// Best-effort: on any IO/parse failure the original data-URL is left inline. +fn externalize_images(llm_message: &mut serde_json::Value, session_id: &str) { + let Some(blocks) = llm_message.get_mut("content").and_then(|c| c.as_array_mut()) else { + return; + }; + for block in blocks { + let url = match block.get("image_url").and_then(|i| i.get("url")).and_then(|u| u.as_str()) { + Some(u) if u.starts_with("data:") => u.to_string(), + _ => continue, + }; + let Some((media, data)) = parse_data_uri(&url) else { continue }; + use base64::Engine; + let Ok(bytes) = base64::engine::general_purpose::STANDARD.decode(data.as_bytes()) else { + continue; + }; + let dir = images_dir(session_id); + if std::fs::create_dir_all(&dir).is_err() { + continue; + } + let file = format!("{}.{}", uuid::Uuid::new_v4(), ext_for_media(&media)); + if std::fs::write(dir.join(&file), &bytes).is_err() { + continue; + } + block["image_url"]["url"] = serde_json::Value::String(format!("{IMAGE_REF_PREFIX}{file}")); + } +} + +/// Rebuild `data:` image URLs from on-disk references before the message is used. +/// Legacy inline data-URLs pass through untouched; missing files are left as-is. +fn inline_images(llm_message: &mut serde_json::Value, session_id: &str) { + let Some(blocks) = llm_message.get_mut("content").and_then(|c| c.as_array_mut()) else { + return; + }; + for block in blocks { + let file = match block.get("image_url").and_then(|i| i.get("url")).and_then(|u| u.as_str()) { + Some(u) => match u.strip_prefix(IMAGE_REF_PREFIX) { + Some(f) => f.to_string(), + None => continue, + }, + None => continue, + }; + let path = images_dir(session_id).join(&file); + let Ok(bytes) = std::fs::read(&path) else { continue }; + use base64::Engine; + let encoded = base64::engine::general_purpose::STANDARD.encode(&bytes); + let ext = std::path::Path::new(&file).extension().and_then(|e| e.to_str()).unwrap_or("img"); + let data_url = format!("data:{};base64,{}", media_for_ext(ext), encoded); + block["image_url"]["url"] = serde_json::Value::String(data_url); + } +} + +/// Extract the display text and image data-URLs from a stored `llm_message` JSON. +/// Handles both plain-string content and the multimodal block array (text + +/// image_url). On-disk image refs are rebuilt into data-URLs so the UI can show +/// them. Used by the message-list command to render images in the chat bubble. +pub fn extract_display_content(llm_message_json: &str, session_id: &str) -> (String, Vec) { + let mut val: serde_json::Value = + serde_json::from_str(llm_message_json).unwrap_or(serde_json::json!({})); + inline_images(&mut val, session_id); + + let content = &val["content"]; + if let Some(s) = content.as_str() { + return (s.to_string(), Vec::new()); + } + let Some(blocks) = content.as_array() else { + return (String::new(), Vec::new()); + }; + + let mut text = String::new(); + let mut images = Vec::new(); + for block in blocks { + match block.get("type").and_then(|t| t.as_str()) { + Some("text") => { + if let Some(t) = block.get("text").and_then(|t| t.as_str()) { + text.push_str(t); + } + } + Some("image_url") => { + if let Some(u) = block + .get("image_url") + .and_then(|i| i.get("url")) + .and_then(|u| u.as_str()) + { + images.push(u.to_string()); + } + } + _ => {} + } + } + (text, images) +} + +pub fn reconstruct_context(stored: Vec) -> Vec { + if stored.is_empty() { + return Vec::new(); + } + + let last_compaction_pos = stored.iter().rposition(|m| m.type_ == "compaction"); + + match last_compaction_pos { + Some(pos) => { + let kept_before_count = serde_json::from_str::(&stored[pos].metadata) + .ok() + .and_then(|meta| meta["kept_before_count"].as_u64()) + .unwrap_or(0) as usize; + + let mut count = 0; + let mut start = pos; + while start > 0 && count < kept_before_count { + start -= 1; + if stored[start].type_ != "compaction" { + count += 1; + } + } + + stored + .into_iter() + .skip(start) + .map(stored_to_agent_message) + .collect() + } + None => stored.into_iter().map(stored_to_agent_message).collect(), + } +} + +fn stored_to_agent_message(stored: StoredMessage) -> AgentMessage { + let mut llm_message: serde_json::Value = + serde_json::from_str(&stored.llm_message).unwrap_or(serde_json::json!({})); + // Rebuild any disk-backed image references into data-URLs before the LLM sees them. + inline_images(&mut llm_message, &stored.session_id); + let metadata: serde_json::Value = + serde_json::from_str(&stored.metadata).unwrap_or(serde_json::json!({})); + let content = llm_message["content"].as_str().unwrap_or("").to_string(); + let sender = if stored.role == "user" { + Sender::HumanUser + } else { + Sender::Agent + }; + + AgentMessage { + content, + llm_message, + metadata, + role: str_to_role(&stored.role), + message_type: str_to_type(&stored.type_), + sender, + turn_count: stored.turn_count, + } +} + +// ── SqliteMessagePersister ───────────────────────────────────────────────── + +/// Implements [`MessagePersister`] backed by [`AgentDb`]. Messages are keyed by +/// the `session_id` passed to each call; `project_path` is stamped on every row +/// for reference. One instance is shared across a session and its subagents — +/// children persist under their own `session_id` (the crate stamps the parent +/// link into message metadata). +pub struct SqliteMessagePersister { + db: Arc, + project_path: String, +} + +impl SqliteMessagePersister { + pub fn new(db: Arc, project_path: String) -> Self { + Self { db, project_path } + } + + pub fn project_path(&self) -> &str { + &self.project_path + } +} + +#[async_trait] +impl MessagePersister for SqliteMessagePersister { + async fn persist_message( + &self, + message: &AgentMessage, + session_id: &str, + ) -> Result { + let db = Arc::clone(&self.db); + let sid = session_id.to_string(); + let project_path = self.project_path.clone(); + let role = role_to_str(message.role).to_string(); + let type_ = type_to_str(message.message_type).to_string(); + let turn_count = message.turn_count; + // Externalize inline image bytes to disk, keeping only a reference in SQLite. + let mut llm_value = message.llm_message.clone(); + externalize_images(&mut llm_value, session_id); + let llm_message = serde_json::to_string(&llm_value) + .map_err(|e| PersistError::Storage(e.to_string()))?; + let metadata = serde_json::to_string(&message.metadata) + .map_err(|e| PersistError::Storage(e.to_string()))?; + + let id = tokio::task::spawn_blocking(move || { + db.insert_message(&sid, &project_path, &role, &type_, &llm_message, &metadata, turn_count) + }) + .await + .map_err(|e| PersistError::Storage(e.to_string()))? + .map_err(|e| PersistError::Storage(e.to_string()))?; + + Ok(PersistResult { id }) + } + + async fn load_context(&self, session_id: &str) -> Result, PersistError> { + let db = Arc::clone(&self.db); + let sid = session_id.to_string(); + let stored = tokio::task::spawn_blocking(move || db.load_session_for_context(&sid, 500)) + .await + .map_err(|e| PersistError::Storage(e.to_string()))? + .map_err(|e| PersistError::Storage(e.to_string()))?; + Ok(reconstruct_context(stored)) + } +} + +// ── Tests ────────────────────────────────────────────────────────────────── + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + use tempfile::tempdir; + + fn make_db() -> (tempfile::TempDir, AgentDb) { + let dir = tempdir().unwrap(); + let db = AgentDb::new(dir.path()).unwrap(); + (dir, db) + } + + fn make_persister() -> (tempfile::TempDir, Arc, SqliteMessagePersister) { + let dir = tempdir().unwrap(); + let db = Arc::new(AgentDb::new(dir.path()).unwrap()); + let persister = SqliteMessagePersister::new(Arc::clone(&db), "/proj".into()); + (dir, db, persister) + } + + fn llm_json(role: &str, content: &str) -> String { + json!({"role": role, "content": content}).to_string() + } + + fn insert_text(db: &AgentDb, session_id: &str, content: &str) -> String { + db.insert_message(session_id, "/proj", "user", "text", &llm_json("user", content), "{}", None) + .unwrap() + } + + fn agent_msg(content: &str, role: MessageRole, mt: MessageType) -> AgentMessage { + AgentMessage { + content: content.into(), + llm_message: json!({"role": role_to_str(role), "content": content}), + metadata: json!({}), + role, + message_type: mt, + sender: if matches!(role, MessageRole::User) { Sender::HumanUser } else { Sender::Agent }, + turn_count: None, + } + } + + #[test] + fn test_schema_v1() { + let (_d, db) = make_db(); + assert_eq!(db.schema_version().unwrap(), 1); + } + + #[test] + fn test_session_crud() { + let (_d, db) = make_db(); + db.create_session("s1", "/proj", "coding", Some("Fix bug"), None, None, None).unwrap(); + let s = db.get_session("s1").unwrap().unwrap(); + assert_eq!(s.folder, "/proj"); + assert_eq!(s.mode, "coding"); + assert_eq!(s.status, "active"); + + // one-active-per-folder lookup + assert_eq!(db.active_session_for_folder("/proj").unwrap().as_deref(), Some("s1")); + db.set_session_status("s1", "idle").unwrap(); + assert!(db.active_session_for_folder("/proj").unwrap().is_none()); + + let all = db.list_sessions().unwrap(); + assert_eq!(all.len(), 1); + } + + #[test] + fn test_session_isolation() { + let (_d, db) = make_db(); + insert_text(&db, "a", "msg-a"); + insert_text(&db, "b", "msg-b"); + let a = db.load_session_messages("a").unwrap(); + assert_eq!(a.len(), 1); + assert!(a[0].llm_message.contains("msg-a")); + } + + #[tokio::test] + async fn test_persister_roundtrip() { + let (_d, _db, p) = make_persister(); + let msg = agent_msg("hello", MessageRole::Assistant, MessageType::Text); + p.persist_message(&msg, "s1").await.unwrap(); + let loaded = p.load_context("s1").await.unwrap(); + assert_eq!(loaded.len(), 1); + assert_eq!(loaded[0].content, "hello"); + assert_eq!(loaded[0].role, MessageRole::Assistant); + } + + #[test] + fn test_compaction_reconstruction() { + let (_d, db) = make_db(); + for i in 1..=5 { + insert_text(&db, "t1", &format!("msg-{i}")); + } + db.insert_compaction("t1", "/proj", "Summary of 1-3", 2).unwrap(); + for i in 6..=7 { + insert_text(&db, "t1", &format!("msg-{i}")); + } + let stored = db.load_session_messages("t1").unwrap(); + let rec = reconstruct_context(stored); + let contents: Vec = rec.iter().map(|m| m.content.clone()).collect(); + for i in 1..=3 { + assert!(!contents.contains(&format!("msg-{i}")), "msg-{i} should be compacted"); + } + for i in 4..=7 { + assert!(contents.contains(&format!("msg-{i}")), "msg-{i} should be present"); + } + assert!(contents.iter().any(|c| c.contains("Summary of 1-3"))); + } + + #[test] + fn test_context_usage_roundtrip() { + let (_d, db) = make_db(); + db.upsert_context_usage("s1", "/proj", 1200, 128000, 8).unwrap(); + let usage = db.get_context_usage("s1").unwrap().unwrap(); + assert_eq!(usage, (1200, 128000, 8)); + db.delete_context_usage("s1").unwrap(); + assert!(db.get_context_usage("s1").unwrap().is_none()); + } + + #[test] + fn test_rewind() { + let (_d, db) = make_db(); + let _id1 = insert_text(&db, "t1", "keep"); + let id2: i64 = insert_text(&db, "t1", "drop").parse().unwrap(); + insert_text(&db, "t1", "also drop"); + let n = db.rewind_messages("t1", id2).unwrap(); + assert_eq!(n, 2); + let remaining = db.load_session_messages("t1").unwrap(); + assert_eq!(remaining.len(), 1); + assert!(remaining[0].llm_message.contains("keep")); + } +} diff --git a/apps/desktop/src-tauri/src/agent_bridge/events.rs b/apps/desktop/src-tauri/src/agent_bridge/events.rs new file mode 100644 index 00000000..1d1bbc9e --- /dev/null +++ b/apps/desktop/src-tauri/src/agent_bridge/events.rs @@ -0,0 +1,557 @@ +use std::collections::{HashMap, HashSet}; +use std::path::PathBuf; +use std::sync::{Arc, Mutex}; +use std::time::Instant; + +use async_trait::async_trait; +use serde_json::Value; +use tokio::sync::{mpsc, oneshot}; + +use agent::approval::{ApprovalDecision, ApprovalHandler}; +use agent::types::AgentEvent; + +use crate::agent_bridge::db::AgentDb; +use crate::agent_bridge::permissions::PermissionConfig; +use crate::agent_bridge::traits::{emit_or_log, EventEmitter}; + +// ── Message accumulator ─────────────────────────────────────────────────────── +// +// Builds the finalized assistant message content (thinking markers + text) that +// the frontend renders as a collapsible "Thought for N seconds" block followed +// by the reply text. Tool calls between text become thinking steps. + +pub struct MessageAccumulator { + thinking_steps: Vec, + thinking_start: Option, + text_buffer: String, +} + +impl MessageAccumulator { + pub fn new() -> Self { + Self { + thinking_steps: Vec::new(), + thinking_start: None, + text_buffer: String::new(), + } + } + + /// On tool start: flush any pending text as a finalized message, record the step. + pub fn on_tool_start(&mut self, _tool_name: &str, args_summary: &str) -> Option { + let flushed = (!self.text_buffer.is_empty()).then(|| self.build_content()); + if self.thinking_start.is_none() { + self.thinking_start = Some(Instant::now()); + } + self.thinking_steps.push(args_summary.to_string()); + flushed + } + + pub fn on_text_delta(&mut self, delta: &str) { + self.text_buffer.push_str(delta); + } + + /// Build the final content string with thinking markers + text, then reset. + pub fn build_content(&mut self) -> String { + let mut content = String::new(); + if !self.thinking_steps.is_empty() { + let duration = self.thinking_start.map(|s| s.elapsed().as_secs()).unwrap_or(0); + content.push_str(&format!("\n")); + for step in &self.thinking_steps { + content.push_str(step); + content.push('\n'); + } + content.push_str("\n\n"); + } + content.push_str(&self.text_buffer); + self.thinking_steps.clear(); + self.thinking_start = None; + self.text_buffer.clear(); + content + } + + pub fn has_content(&self) -> bool { + !self.text_buffer.is_empty() || !self.thinking_steps.is_empty() + } +} + +impl Default for MessageAccumulator { + fn default() -> Self { + Self::new() + } +} + +// ── Relay ───────────────────────────────────────────────────────────────────── + +/// Bundles relay configuration. `session_id` doubles as the `thread_id` carried +/// in every frontend event (the UI keys streaming state by it). +struct RelayContext { + emitter: Arc, + db: Option>, + session_id: String, + project_path: String, + /// Snapshot checkpoint root (`/.supercoder/checkpoints`). `diff_turn` + /// is computed against `//turn-N`. None disables diffs. + checkpoint_dir: Option, + checkpoint_handles: Arc>>>, +} + +impl RelayContext { + fn flush_content(&self, content: &str) { + emit_message_complete(self.emitter.as_ref(), &self.session_id, content); + } +} + +/// Emit a finalized assistant message to the frontend so the streaming bubble +/// becomes a permanent thread message. The agent loop has already persisted the +/// real message to SQLite; this carries the display content (incl. thinking). +fn emit_message_complete(emitter: &dyn EventEmitter, session_id: &str, content: &str) { + let message = serde_json::json!({ + "id": uuid::Uuid::new_v4().to_string(), + "role": "assistant", + "content": content, + "created_at": chrono::Utc::now().format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string(), + }); + emit_or_log( + emitter, + "agent:message_complete", + serde_json::json!({ "thread_id": session_id, "message": message }), + ); +} + +/// Spawn the event relay: drains `event_rx`, maps each `AgentEvent` to a +/// namespaced Tauri event, captures turn diffs from the snapshot dir, and +/// persists token usage. Returns a JoinHandle the caller tracks for cancel. +#[allow(clippy::too_many_arguments)] +pub fn spawn_event_relay( + emitter: Arc, + mut event_rx: mpsc::Receiver, + session_id: String, + db: Option>, + project_path: String, + checkpoint_dir: Option, +) -> tokio::task::JoinHandle<()> { + let ctx = RelayContext { + emitter, + db, + session_id, + project_path, + checkpoint_dir, + checkpoint_handles: Arc::new(tokio::sync::Mutex::new(Vec::new())), + }; + + tokio::spawn(async move { + let mut accumulator = MessageAccumulator::new(); + let mut subagent_tool_call_ids: HashSet = HashSet::new(); + let mut saw_done = false; + + while let Some(event) = event_rx.recv().await { + if relay_event(&ctx, &mut accumulator, &mut subagent_tool_call_ids, event).await { + saw_done = true; + } + } + + // Flush any trailing content if the channel closed without a Done. + if !saw_done && accumulator.has_content() { + let content = accumulator.build_content(); + ctx.flush_content(&content); + emit_or_log( + ctx.emitter.as_ref(), + "agent:done", + serde_json::json!({ "thread_id": ctx.session_id, "summary": Value::Null }), + ); + } + + // Await in-flight turn-diff tasks so the final turn's diff lands. + let handles: Vec<_> = std::mem::take(&mut *ctx.checkpoint_handles.lock().await); + for h in handles { + let _ = h.await; + } + }) +} + +async fn relay_event( + ctx: &RelayContext, + accumulator: &mut MessageAccumulator, + subagent_tool_call_ids: &mut HashSet, + event: AgentEvent, +) -> bool { + let tid = ctx.session_id.clone(); + match event { + AgentEvent::ThinkingDelta { .. } => false, + + AgentEvent::TextDelta { delta, .. } => { + accumulator.on_text_delta(&delta); + emit_or_log( + ctx.emitter.as_ref(), + "agent:text_delta", + serde_json::json!({ "thread_id": tid, "delta": delta }), + ); + false + } + + AgentEvent::ToolStart { tool_call_id, tool_name, args_summary, .. } => { + // spawn_subagent renders only as a SubagentStart chip. + if tool_name == "spawn_subagent" { + subagent_tool_call_ids.insert(tool_call_id); + return false; + } + if let Some(content) = accumulator.on_tool_start(&tool_name, &args_summary) { + ctx.flush_content(&content); + } + emit_or_log( + ctx.emitter.as_ref(), + "agent:tool_start", + serde_json::json!({ + "thread_id": tid, + "tool_call_id": tool_call_id, + "tool_name": tool_name, + "args_summary": args_summary, + }), + ); + false + } + + AgentEvent::ToolEnd { tool_call_id, success, summary, modified_files, .. } => { + if subagent_tool_call_ids.remove(&tool_call_id) { + return false; + } + emit_or_log( + ctx.emitter.as_ref(), + "agent:tool_end", + serde_json::json!({ + "thread_id": tid, + "tool_call_id": tool_call_id, + "success": success, + "summary": summary, + "modified_files": modified_files, + }), + ); + false + } + + AgentEvent::ToolStatus { .. } => false, + + AgentEvent::Error { message, retrying, .. } => { + emit_or_log( + ctx.emitter.as_ref(), + "agent:error", + serde_json::json!({ "thread_id": tid, "message": message, "retrying": retrying }), + ); + false + } + + AgentEvent::Compaction { .. } => { + emit_or_log( + ctx.emitter.as_ref(), + "agent:compaction", + serde_json::json!({ "thread_id": tid }), + ); + false + } + + AgentEvent::TokenUsage { + total_tokens, context_limit, cache_read_tokens, cache_creation_tokens, .. + } => { + // `None` (unknown model) → emit null / persist 0; the UI shows the raw + // token count with no max or percentage. + emit_or_log( + ctx.emitter.as_ref(), + "agent:token_usage", + serde_json::json!({ + "thread_id": tid, + "total_tokens": total_tokens, + "context_limit": context_limit, + "cache_read_tokens": cache_read_tokens, + "cache_creation_tokens": cache_creation_tokens, + }), + ); + // Persist usage so the indicator survives app refresh (fire-and-forget). + if let Some(ref db_ref) = ctx.db { + let db = Arc::clone(db_ref); + let sid = ctx.session_id.clone(); + let pp = ctx.project_path.clone(); + let limit = context_limit.unwrap_or(0); + let _ = tokio::task::spawn_blocking(move || { + if let Err(e) = db.upsert_context_usage(&sid, &pp, total_tokens, limit, 0) { + log::warn!("[Relay] Failed to persist context usage: {e}"); + } + }); + } + false + } + + AgentEvent::Done { summary, .. } => { + if accumulator.has_content() { + let content = accumulator.build_content(); + ctx.flush_content(&content); + } + emit_or_log( + ctx.emitter.as_ref(), + "agent:done", + serde_json::json!({ "thread_id": tid, "summary": summary }), + ); + true + } + + AgentEvent::TurnCompleted { turn_count, modified_files, .. } => { + if let Some(ref dir) = ctx.checkpoint_dir { + let dir = dir.clone(); + let sid = ctx.session_id.clone(); + let emitter = ctx.emitter.clone(); + let files = modified_files; + let handle = tokio::spawn(async move { + match git_ops::checkpoint::diff_turn(&dir, &sid, turn_count).await { + Ok(d) => { + emit_or_log( + emitter.as_ref(), + "agent:turn_diff_completed", + serde_json::json!({ + "thread_id": sid, + "turn_count": turn_count, + "files": files, + "additions": d.insertions, + "deletions": d.deletions, + "diff": d.diff, + "stat": d.stat, + "status": "ready", + }), + ); + } + Err(e) => { + log::warn!("[Checkpoint] diff_turn failed for turn {turn_count}: {e}"); + emit_or_log( + emitter.as_ref(), + "agent:turn_diff_completed", + serde_json::json!({ + "thread_id": sid, + "turn_count": turn_count, + "files": files, + "additions": 0, + "deletions": 0, + "diff": "", + "stat": "", + "status": "partial", + }), + ); + } + } + }); + ctx.checkpoint_handles.lock().await.push(handle); + } + false + } + + AgentEvent::UserQuestionAsked { session_id, question, options } => { + emit_or_log( + ctx.emitter.as_ref(), + "agent:question_asked", + serde_json::json!({ + "thread_id": tid, + "session_id": session_id, + "question": question, + "options": options, + }), + ); + false + } + + AgentEvent::PlanReady { session_id, plan, plan_path, project_path } => { + emit_or_log( + ctx.emitter.as_ref(), + "agent:plan_ready", + serde_json::json!({ + "thread_id": tid, + "session_id": session_id, + "plan": plan, + "plan_path": plan_path, + "project_path": project_path, + }), + ); + false + } + + AgentEvent::TodoUpdated { todos, .. } => { + emit_or_log( + ctx.emitter.as_ref(), + "agent:todo_updated", + serde_json::json!({ "thread_id": tid, "todos": todos }), + ); + false + } + + AgentEvent::SkillLoaded { skill_name, .. } => { + emit_or_log( + ctx.emitter.as_ref(), + "agent:skill_loaded", + serde_json::json!({ "thread_id": tid, "skill_name": skill_name }), + ); + false + } + + AgentEvent::SubagentStart { + parent_tool_call_id, child_session_id, subagent_name, prompt_preview, .. + } => { + let step_label = format!("Subagent: {subagent_name}"); + if let Some(content) = accumulator.on_tool_start("subagent", &step_label) { + ctx.flush_content(&content); + } + emit_or_log( + ctx.emitter.as_ref(), + "agent:subagent_start", + serde_json::json!({ + "thread_id": tid, + "parent_tool_call_id": parent_tool_call_id, + "child_session_id": child_session_id, + "subagent_name": subagent_name, + "prompt_preview": prompt_preview, + }), + ); + false + } + + AgentEvent::SubagentEnd { + parent_tool_call_id, child_session_id, success, summary, .. + } => { + emit_or_log( + ctx.emitter.as_ref(), + "agent:subagent_end", + serde_json::json!({ + "thread_id": tid, + "parent_tool_call_id": parent_tool_call_id, + "child_session_id": child_session_id, + "success": success, + "summary": summary, + }), + ); + false + } + } +} + +// ── Approval handlers ───────────────────────────────────────────────────────── + +/// Emits `agent:approval_needed` and blocks until the frontend resolves it via +/// `agent_approve_tool`. +pub struct TauriApprovalHandler { + emitter: Arc, + pending: Arc>>>, + thread_id: String, +} + +impl TauriApprovalHandler { + pub fn new(emitter: Arc, thread_id: String) -> Self { + Self { + emitter, + pending: Arc::new(Mutex::new(HashMap::new())), + thread_id, + } + } + + pub fn resolve(&self, tool_call_id: &str, approved: bool) { + if let Some(tx) = self.pending.lock().unwrap().remove(tool_call_id) { + let _ = tx.send(approved); + } + } +} + +#[async_trait] +impl ApprovalHandler for TauriApprovalHandler { + async fn request_approval( + &self, + tool_name: &str, + tool_call_id: &str, + args: &Value, + args_summary: &str, + ) -> ApprovalDecision { + let (tx, rx) = oneshot::channel(); + self.pending.lock().unwrap().insert(tool_call_id.to_string(), tx); + + emit_or_log( + self.emitter.as_ref(), + "agent:approval_needed", + serde_json::json!({ + "thread_id": self.thread_id, + "tool_call_id": tool_call_id, + "tool_name": tool_name, + "description": args_summary, + "args": args, + }), + ); + + match rx.await { + Ok(true) => ApprovalDecision::Approved, + Ok(false) => ApprovalDecision::Denied { reason: Some("User denied".to_string()) }, + Err(_) => ApprovalDecision::Denied { reason: Some("Session cancelled".to_string()) }, + } + } +} + +/// Checks the permission config before asking the user; auto-approves tools the +/// config marks safe. `[SECURITY]`-tagged requests always go to the user. +pub struct PermissionAwareApprovalHandler { + inner: Arc, + config: PermissionConfig, +} + +impl PermissionAwareApprovalHandler { + pub fn new(inner: Arc, config: PermissionConfig) -> Self { + Self { inner, config } + } +} + +#[async_trait] +impl ApprovalHandler for PermissionAwareApprovalHandler { + async fn request_approval( + &self, + tool_name: &str, + tool_call_id: &str, + args: &Value, + args_summary: &str, + ) -> ApprovalDecision { + let force_ask = args_summary.starts_with("[SECURITY]"); + if !force_ask && !crate::agent_bridge::permissions::needs_approval(&self.config, tool_name) { + return ApprovalDecision::Approved; + } + self.inner.request_approval(tool_name, tool_call_id, args, args_summary).await + } +} + +/// Tags a subagent's approval requests with `(subagent:NAME)` while delegating +/// to the parent's permission-aware handler. +pub struct SubagentApprovalHandler { + inner: Arc, + subagent_name: String, +} + +#[async_trait] +impl ApprovalHandler for SubagentApprovalHandler { + async fn request_approval( + &self, + tool_name: &str, + tool_call_id: &str, + args: &Value, + args_summary: &str, + ) -> ApprovalDecision { + let tagged = format!("(subagent:{}) {}", self.subagent_name, args_summary); + self.inner.request_approval(tool_name, tool_call_id, args, &tagged).await + } +} + +/// Produces tagged per-subagent approval handlers from the parent handler. +pub struct TauriApprovalHandlerFactory { + inner: Arc, +} + +impl TauriApprovalHandlerFactory { + pub fn new(inner: Arc) -> Self { + Self { inner } + } +} + +impl agent::subagents::ApprovalHandlerFactory for TauriApprovalHandlerFactory { + fn for_subagent(&self, subagent_name: &str) -> Arc { + Arc::new(SubagentApprovalHandler { + inner: Arc::clone(&self.inner), + subagent_name: subagent_name.to_string(), + }) + } +} diff --git a/apps/desktop/src-tauri/src/agent_bridge/mod.rs b/apps/desktop/src-tauri/src/agent_bridge/mod.rs new file mode 100644 index 00000000..64adc3cd --- /dev/null +++ b/apps/desktop/src-tauri/src/agent_bridge/mod.rs @@ -0,0 +1,7 @@ +pub mod commands; +pub mod db; +pub mod events; +pub mod permissions; +pub mod skills; +pub mod subagents; +pub mod traits; diff --git a/apps/desktop/src-tauri/src/agent_bridge/permissions.rs b/apps/desktop/src-tauri/src/agent_bridge/permissions.rs new file mode 100644 index 00000000..09aa7b2f --- /dev/null +++ b/apps/desktop/src-tauri/src/agent_bridge/permissions.rs @@ -0,0 +1,350 @@ +use rusqlite::Connection; +use serde::{Deserialize, Serialize}; + +// ── Types ────────────────────────────────────────────────────────────────── + +/// Permission level for agent tool execution. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum PermissionLevel { + /// All tools run without approval. + AutoApproveAll, + /// Destructive tools require approval (default). + ApproveDestructive, + /// All tools require approval. + ApproveEverything, +} + +/// Per-tool overrides that take priority over the permission level. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ToolOverrides { + /// Tools that always run without approval regardless of level. + pub auto_approve: Vec, + /// Tools that always require approval regardless of level. + pub always_ask: Vec, +} + +/// Complete permission configuration for a project (or global default). +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PermissionConfig { + /// None = global default, Some = project-specific. + pub project_path: Option, + /// Permission level. + pub level: PermissionLevel, + /// Per-tool overrides. + pub tool_overrides: Option, +} + +/// Tools considered destructive (can modify filesystem or external state). +const DESTRUCTIVE_TOOLS: &[&str] = &["write", "edit", "bash", "git", "create_pr", "apply_patch"]; + +/// System tools that never require approval, even in ApproveEverything mode. +/// These are internal/UI tools that don't affect the codebase. +const SYSTEM_TOOLS: &[&str] = &["todo_write", "ask_user", "save_plan", "edit_plan"]; + +// ── Table management ─────────────────────────────────────────────────────── + +/// Create the agent_permissions table in the existing settings DB if it doesn't exist. +pub fn ensure_permissions_table(conn: &Connection) -> Result<(), String> { + conn.execute_batch( + "CREATE TABLE IF NOT EXISTS agent_permissions ( + project_path TEXT PRIMARY KEY, + level TEXT NOT NULL DEFAULT 'approve_destructive', + tool_overrides TEXT + );", + ) + .map_err(|e| e.to_string()) +} + +// ── CRUD ─────────────────────────────────────────────────────────────────── + +/// Get permission config for a project path. Falls back to global default if no +/// project-specific config exists. Returns the default (ApproveDestructive) if +/// no config exists at all. +pub fn get_permission(conn: &Connection, project_path: Option<&str>) -> PermissionConfig { + let key = project_path.unwrap_or("__global__"); + + let result: Option<(String, Option)> = conn + .query_row( + "SELECT level, tool_overrides FROM agent_permissions WHERE project_path = ?", + [key], + |row| Ok((row.get(0)?, row.get(1)?)), + ) + .ok(); + + match result { + Some((level_str, overrides_str)) => { + PermissionConfig { + project_path: project_path.map(String::from), + level: str_to_level(&level_str), + tool_overrides: overrides_str + .and_then(|s| serde_json::from_str(&s).ok()), + } + }, + None => { + if let Some(path_str) = project_path { + // Walk up parent directories to find a matching config. + // This handles worktree paths like /project/.agent-worktrees/session-id + // matching a config saved for /project. + let mut search_path = std::path::Path::new(path_str); + while let Some(parent) = search_path.parent() { + if parent.as_os_str().is_empty() { + break; + } + let parent_str = parent.to_string_lossy(); + let parent_result: Option<(String, Option)> = conn + .query_row( + "SELECT level, tool_overrides FROM agent_permissions WHERE project_path = ?", + [parent_str.as_ref()], + |row| Ok((row.get(0)?, row.get(1)?)), + ) + .ok(); + + if let Some((level_str, overrides_str)) = parent_result { + return PermissionConfig { + project_path: project_path.map(String::from), + level: str_to_level(&level_str), + tool_overrides: overrides_str + .and_then(|s| serde_json::from_str(&s).ok()), + }; + } + search_path = parent; + } + + // No ancestor match — fall back to global + let global = get_permission(conn, None); + PermissionConfig { + project_path: project_path.map(String::from), + level: global.level, + tool_overrides: global.tool_overrides, + } + } else { + // No global config — return default + PermissionConfig { + project_path: None, + level: PermissionLevel::ApproveDestructive, + tool_overrides: None, + } + } + } + } +} + +/// Save a permission config. Uses project_path or "__global__" as key. +pub fn set_permission(conn: &Connection, config: &PermissionConfig) -> Result<(), String> { + let key = config + .project_path + .as_deref() + .unwrap_or("__global__"); + let level_str = level_to_str(config.level); + let overrides_str = config + .tool_overrides + .as_ref() + .map(|o| serde_json::to_string(o).map_err(|e| e.to_string())) + .transpose()?; + + conn.execute( + "INSERT OR REPLACE INTO agent_permissions (project_path, level, tool_overrides) VALUES (?, ?, ?)", + rusqlite::params![key, level_str, overrides_str], + ) + .map_err(|e| e.to_string())?; + Ok(()) +} + +// ── Approval logic ───────────────────────────────────────────────────────── + +/// Check if a tool requires user approval given the current permission config. +pub fn needs_approval(config: &PermissionConfig, tool_name: &str) -> bool { + // Check overrides first — they take priority + if let Some(ref overrides) = config.tool_overrides { + if overrides.auto_approve.iter().any(|t| t == tool_name) { + return false; + } + if overrides.always_ask.iter().any(|t| t == tool_name) { + return true; + } + } + + // System tools never need approval + if SYSTEM_TOOLS.contains(&tool_name) { + return false; + } + + // Fall back to level + match config.level { + PermissionLevel::AutoApproveAll => false, + PermissionLevel::ApproveEverything => true, + PermissionLevel::ApproveDestructive => is_destructive(tool_name), + } +} + +fn is_destructive(tool_name: &str) -> bool { + DESTRUCTIVE_TOOLS.contains(&tool_name) +} + +fn level_to_str(level: PermissionLevel) -> &'static str { + match level { + PermissionLevel::AutoApproveAll => "auto_approve_all", + PermissionLevel::ApproveDestructive => "approve_destructive", + PermissionLevel::ApproveEverything => "approve_everything", + } +} + +fn str_to_level(s: &str) -> PermissionLevel { + match s { + "auto_approve_all" => PermissionLevel::AutoApproveAll, + "approve_everything" => PermissionLevel::ApproveEverything, + _ => PermissionLevel::ApproveDestructive, + } +} + +// ── Tests ────────────────────────────────────────────────────────────────── + +#[cfg(test)] +mod tests { + use super::*; + + fn setup_db() -> Connection { + let conn = Connection::open_in_memory().unwrap(); + ensure_permissions_table(&conn).unwrap(); + conn + } + + #[test] + fn test_ensure_permissions_table() { + let conn = setup_db(); + // Should be able to query the table + let count: i64 = conn + .query_row("SELECT COUNT(*) FROM agent_permissions", [], |row| { + row.get(0) + }) + .unwrap(); + assert_eq!(count, 0); + } + + #[test] + fn test_get_default_permission() { + let conn = setup_db(); + let config = get_permission(&conn, None); + assert_eq!(config.level, PermissionLevel::ApproveDestructive); + assert!(config.tool_overrides.is_none()); + assert!(config.project_path.is_none()); + } + + #[test] + fn test_set_and_get_permission() { + let conn = setup_db(); + let config = PermissionConfig { + project_path: None, + level: PermissionLevel::AutoApproveAll, + tool_overrides: None, + }; + set_permission(&conn, &config).unwrap(); + + let loaded = get_permission(&conn, None); + assert_eq!(loaded.level, PermissionLevel::AutoApproveAll); + } + + #[test] + fn test_project_specific_permission() { + let conn = setup_db(); + + // Set global to AutoApproveAll + set_permission( + &conn, + &PermissionConfig { + project_path: None, + level: PermissionLevel::AutoApproveAll, + tool_overrides: None, + }, + ) + .unwrap(); + + // Set project-specific to ApproveEverything + set_permission( + &conn, + &PermissionConfig { + project_path: Some("/home/user/project".into()), + level: PermissionLevel::ApproveEverything, + tool_overrides: None, + }, + ) + .unwrap(); + + let project_config = get_permission(&conn, Some("/home/user/project")); + assert_eq!(project_config.level, PermissionLevel::ApproveEverything); + + // Other project falls back to global + let other_config = get_permission(&conn, Some("/home/user/other")); + assert_eq!(other_config.level, PermissionLevel::AutoApproveAll); + } + + #[test] + fn test_needs_approval_auto_approve_all() { + let config = PermissionConfig { + project_path: None, + level: PermissionLevel::AutoApproveAll, + tool_overrides: None, + }; + assert!(!needs_approval(&config, "read")); + assert!(!needs_approval(&config, "write")); + assert!(!needs_approval(&config, "bash")); + assert!(!needs_approval(&config, "edit")); + } + + #[test] + fn test_needs_approval_approve_everything() { + let config = PermissionConfig { + project_path: None, + level: PermissionLevel::ApproveEverything, + tool_overrides: None, + }; + assert!(needs_approval(&config, "read")); + assert!(needs_approval(&config, "glob")); + assert!(needs_approval(&config, "write")); + assert!(needs_approval(&config, "bash")); + // System tools bypass even ApproveEverything + assert!(!needs_approval(&config, "todo_write")); + assert!(!needs_approval(&config, "ask_user")); + assert!(!needs_approval(&config, "save_plan")); + assert!(!needs_approval(&config, "edit_plan")); + } + + #[test] + fn test_needs_approval_destructive_only() { + let config = PermissionConfig { + project_path: None, + level: PermissionLevel::ApproveDestructive, + tool_overrides: None, + }; + // Read-only tools pass + assert!(!needs_approval(&config, "read")); + assert!(!needs_approval(&config, "glob")); + assert!(!needs_approval(&config, "grep")); + // Destructive tools need approval + assert!(needs_approval(&config, "write")); + assert!(needs_approval(&config, "edit")); + assert!(needs_approval(&config, "bash")); + assert!(needs_approval(&config, "git")); + assert!(needs_approval(&config, "create_pr")); + } + + #[test] + fn test_tool_overrides() { + let config = PermissionConfig { + project_path: None, + level: PermissionLevel::ApproveDestructive, + tool_overrides: Some(ToolOverrides { + auto_approve: vec!["bash".into()], + always_ask: vec!["read".into()], + }), + }; + // bash is destructive but overridden to auto_approve + assert!(!needs_approval(&config, "bash")); + // read is non-destructive but overridden to always_ask + assert!(needs_approval(&config, "read")); + // write is destructive with no override — follows level + assert!(needs_approval(&config, "write")); + // glob is non-destructive with no override — follows level + assert!(!needs_approval(&config, "glob")); + } +} diff --git a/apps/desktop/src-tauri/src/agent_bridge/skills.rs b/apps/desktop/src-tauri/src/agent_bridge/skills.rs new file mode 100644 index 00000000..f2f1a25a --- /dev/null +++ b/apps/desktop/src-tauri/src/agent_bridge/skills.rs @@ -0,0 +1,221 @@ +use std::collections::HashSet; +use std::path::{Path, PathBuf}; +use std::sync::Arc; + +use agent::skills::registry::{read_tier_from_fs, SkillInput}; +use agent::skills::{SkillRegistry, DEFAULT_SKILLS}; + +use super::db::AgentDb; + +/// Resolve the user-global skills directory. Matches the `dirs::data_local_dir()` +/// pattern used for `agent_data.db` so user-global skills live next to the DB at +/// e.g. macOS: `~/Library/Application Support/.supercoder/skills/`. +pub fn global_skills_dir() -> PathBuf { + crate::app_data_dir().join("skills") +} + +/// Project-scoped skills directory: `/.agent/skills/`. +pub fn project_skills_dir(working_dir: &Path) -> PathBuf { + working_dir.join(".agent").join("skills") +} + +/// Replace the user's home-dir prefix with `~` for display. Falls back to the +/// full path if HOME isn't set or the path doesn't live under home. +/// +/// Uses `Path::strip_prefix` rather than string-level `str::strip_prefix` so a +/// home of `/Users/alice` doesn't match a path like `/Users/alice2/foo` +/// (which the byte-level match would collapse to `~2/foo`). +fn home_shorten(path: &Path) -> String { + if let Some(home) = dirs::home_dir() { + if let Ok(rest) = path.strip_prefix(&home) { + let rest_str = rest.display().to_string(); + if rest_str.is_empty() { + return "~".to_string(); + } + return format!("~/{rest_str}"); + } + } + path.display().to_string() +} + +/// Paths returned to the /skills dialog so the empty-state can show the user +/// exactly where to drop a SKILL.md folder. Both paths are home-shortened for +/// display. +#[derive(Debug, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SkillsPaths { + pub global: String, + pub project: Option, +} + +/// Compute the display paths. Project is populated only when a working dir is +/// supplied. +pub fn paths_for_display(working_dir: Option<&Path>) -> SkillsPaths { + SkillsPaths { + global: home_shorten(&global_skills_dir()), + project: working_dir.map(|wd| home_shorten(&project_skills_dir(wd))), + } +} + +/// Pull embedded default skills (compile-time `include_dir!`). +fn read_default_tier() -> Vec { + DEFAULT_SKILLS + .dirs() + .filter_map(|d| { + let skill_md = d.path().join("SKILL.md"); + DEFAULT_SKILLS + .get_file(&skill_md) + .map(|f| SkillInput { + raw: String::from_utf8_lossy(f.contents()).into_owned(), + path: PathBuf::from(format!("defaults://{}", d.path().display())), + }) + }) + .collect() +} + +/// Build a `SkillRegistry` from the three tiers (default → global → project), +/// filtering out skills the user has toggled off. Returns `None` if the +/// resulting registry is empty — callers treat that as "no skills present, +/// don't touch the prompt or register the tool." +pub fn build_registry_for_agent( + agent_db: &AgentDb, + working_dir: &Path, +) -> Option> { + let disabled: HashSet = agent_db.load_disabled_skills().unwrap_or_else(|e| { + log::warn!("[skills] failed to load disabled skills, treating all as enabled: {e}"); + HashSet::new() + }); + + let default = read_default_tier(); + let global_root = global_skills_dir(); + let project_root = project_skills_dir(working_dir); + let global = read_tier_from_fs(&global_root); + let project = read_tier_from_fs(&project_root); + + log::info!( + "[skills] build_registry_for_agent: default={} global={} (from {:?}) project={} (from {:?}) disabled={:?}", + default.len(), + global.len(), + global_root, + project.len(), + project_root, + disabled, + ); + + let registry = SkillRegistry::new(default, global, project, &disabled); + if registry.is_empty() { + log::info!("[skills] build_registry_for_agent: empty registry — skills feature inactive this spawn"); + None + } else { + let names: Vec = registry.names(); + log::info!( + "[skills] build_registry_for_agent: registry active, {} skill(s) loaded: {:?}", + registry.len(), + names, + ); + Some(Arc::new(registry)) + } +} + +/// Enumerate all discovered skills across all tiers for the `/skills` dialog, +/// including those toggled off. The `enabled` flag reflects current user +/// preference. +pub fn list_all_for_dialog( + agent_db: &AgentDb, + working_dir: Option<&Path>, +) -> Vec { + let disabled = agent_db.load_disabled_skills().unwrap_or_default(); + let default = read_default_tier(); + let global_root = global_skills_dir(); + let global = read_tier_from_fs(&global_root); + let project = working_dir + .map(|wd| read_tier_from_fs(&project_skills_dir(wd))) + .unwrap_or_default(); + + log::info!( + "[skills] list_all_for_dialog: default={} global={} (from {:?}) project={} working_dir={:?} disabled={:?}", + default.len(), + global.len(), + global_root, + project.len(), + working_dir, + disabled, + ); + + // Build an empty-disabled registry purely for its precedence/parse logic — + // the dialog shows whatever precedence would win at agent spawn time. + let resolved = SkillRegistry::new(default, global, project, &HashSet::new()); + let entries: Vec = resolved + .all() + .into_iter() + .map(|s| DialogEntry { + name: s.name.clone(), + description: s.description.clone(), + origin: match s.origin { + agent::skills::Origin::Default => "default", + agent::skills::Origin::Global => "global", + agent::skills::Origin::Project => "project", + } + .to_string(), + enabled: !disabled.contains(&s.name), + path: s.path.display().to_string(), + }) + .collect(); + log::info!( + "[skills] list_all_for_dialog: returning {} entries to frontend", + entries.len() + ); + entries +} + +/// Flat struct returned to the frontend for the `/skills` dialog. +#[derive(Debug, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DialogEntry { + pub name: String, + pub description: String, + pub origin: String, + pub enabled: bool, + pub path: String, +} + +#[cfg(test)] +mod tests { + use super::*; + + // `home_shorten` uses `dirs::home_dir()` at runtime which we can't override + // per-test; the test below exercises the path-component boundary logic + // directly by inlining a clone of the function with an explicit home arg. + fn home_shorten_with(path: &Path, home: &Path) -> String { + if let Ok(rest) = path.strip_prefix(home) { + let rest_str = rest.display().to_string(); + if rest_str.is_empty() { + return "~".to_string(); + } + return format!("~/{rest_str}"); + } + path.display().to_string() + } + + #[test] + fn home_shorten_boundary_respects_path_components() { + let home = Path::new("/Users/alice"); + // Prefix-collision case that the old `str::strip_prefix` would garble. + assert_eq!( + home_shorten_with(Path::new("/Users/alice2/foo"), home), + "/Users/alice2/foo" + ); + // Real home match. + assert_eq!( + home_shorten_with(Path::new("/Users/alice/Library/foo"), home), + "~/Library/foo" + ); + // Path IS home itself. + assert_eq!(home_shorten_with(home, home), "~"); + // Path outside home, no collision. + assert_eq!( + home_shorten_with(Path::new("/tmp/skills"), home), + "/tmp/skills" + ); + } +} diff --git a/apps/desktop/src-tauri/src/agent_bridge/subagents.rs b/apps/desktop/src-tauri/src/agent_bridge/subagents.rs new file mode 100644 index 00000000..1f46be3b --- /dev/null +++ b/apps/desktop/src-tauri/src/agent_bridge/subagents.rs @@ -0,0 +1,144 @@ +use std::collections::HashSet; +use std::path::{Path, PathBuf}; +use std::sync::Arc; + +use agent::subagents::registry::{read_tier_from_fs, SubagentInput}; +use agent::subagents::{SubagentRegistry, DEFAULT_SUBAGENTS}; + +use super::db::AgentDb; + +pub fn global_subagents_dir() -> PathBuf { + crate::app_data_dir().join("subagents") +} + +pub fn project_subagents_dir(working_dir: &Path) -> PathBuf { + working_dir.join(".agent").join("subagents") +} + +fn home_shorten(path: &Path) -> String { + if let Some(home) = dirs::home_dir() { + if let Ok(rest) = path.strip_prefix(&home) { + let rest_str = rest.display().to_string(); + if rest_str.is_empty() { + return "~".to_string(); + } + return format!("~/{rest_str}"); + } + } + path.display().to_string() +} + +#[derive(Debug, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SubagentsPaths { + pub global: String, + pub project: Option, +} + +pub fn paths_for_display(working_dir: Option<&Path>) -> SubagentsPaths { + SubagentsPaths { + global: home_shorten(&global_subagents_dir()), + project: working_dir.map(|wd| home_shorten(&project_subagents_dir(wd))), + } +} + +fn read_default_tier() -> Vec { + DEFAULT_SUBAGENTS + .dirs() + .filter_map(|d| { + let dir_name = d.path().file_name()?.to_str()?; + let def_md = d.path().join(format!("{dir_name}.md")); + DEFAULT_SUBAGENTS.get_file(&def_md).map(|f| SubagentInput { + raw: String::from_utf8_lossy(f.contents()).into_owned(), + path: PathBuf::from(format!("defaults://{}", d.path().display())), + }) + }) + .collect() +} + +/// Build a `SubagentRegistry` from default → global → project tiers, filtering +/// disabled entries. Returns None when empty so callers know to skip the +/// system-prompt block and the `spawn_subagent` tool registration. +pub fn build_registry_for_agent( + agent_db: &AgentDb, + working_dir: &Path, +) -> Option> { + let disabled: HashSet = agent_db.load_disabled_subagents().unwrap_or_else(|e| { + log::warn!("[subagents] load_disabled failed, treating all enabled: {e}"); + HashSet::new() + }); + + let default = read_default_tier(); + let global_root = global_subagents_dir(); + let project_root = project_subagents_dir(working_dir); + let global = read_tier_from_fs(&global_root); + let project = read_tier_from_fs(&project_root); + + log::info!( + "[subagents] build_registry_for_agent: default={} global={} (from {:?}) project={} (from {:?}) disabled={:?}", + default.len(), + global.len(), + global_root, + project.len(), + project_root, + disabled, + ); + + let registry = SubagentRegistry::new(default, global, project, &disabled); + if registry.is_empty() { + log::info!("[subagents] build_registry_for_agent: empty — subagents inactive this spawn"); + None + } else { + log::info!( + "[subagents] build_registry_for_agent: {} subagent(s) loaded: {:?}", + registry.len(), + registry.names() + ); + Some(Arc::new(registry)) + } +} + +pub fn list_all_for_dialog( + agent_db: &AgentDb, + working_dir: Option<&Path>, +) -> Vec { + let disabled = agent_db.load_disabled_subagents().unwrap_or_default(); + let default = read_default_tier(); + let global_root = global_subagents_dir(); + let global = read_tier_from_fs(&global_root); + let project = working_dir + .map(|wd| read_tier_from_fs(&project_subagents_dir(wd))) + .unwrap_or_default(); + + let resolved = SubagentRegistry::new(default, global, project, &HashSet::new()); + resolved + .all() + .into_iter() + .map(|s| DialogEntry { + name: s.name.clone(), + description: s.description.clone(), + origin: match s.origin { + agent::subagents::Origin::Default => "default", + agent::subagents::Origin::Global => "global", + agent::subagents::Origin::Project => "project", + } + .to_string(), + enabled: !disabled.contains(&s.name), + allowed_tools: s.allowed_tools.clone(), + model: s.model.clone(), + path: s.path.display().to_string(), + }) + .collect() +} + +#[derive(Debug, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DialogEntry { + pub name: String, + pub description: String, + pub origin: String, + pub enabled: bool, + pub allowed_tools: Option>, + pub model: Option, + pub path: String, +} diff --git a/apps/desktop/src-tauri/src/agent_bridge/traits.rs b/apps/desktop/src-tauri/src/agent_bridge/traits.rs new file mode 100644 index 00000000..ac29d0fc --- /dev/null +++ b/apps/desktop/src-tauri/src/agent_bridge/traits.rs @@ -0,0 +1,107 @@ +use serde_json::Value; +use tauri::{AppHandle, Emitter}; + +// ── EventEmitter ────────────────────────────────────────────────────────────── + +/// Abstracts Tauri event emission (`app.emit()`). +/// Returns `Result` so callers can log on failure instead of silent `let _ =`. +pub trait EventEmitter: Send + Sync { + fn emit(&self, event: &str, payload: Value) -> Result<(), String>; +} + +/// Emit an event, logging on failure. Use this everywhere instead of +/// `let _ = app.emit(...)`. +pub fn emit_or_log(emitter: &(impl EventEmitter + ?Sized), event: &str, payload: Value) { + if let Err(e) = emitter.emit(event, payload) { + log::warn!("[EventEmitter] Failed to emit '{}': {}", event, e); + } +} + +/// Production implementation wrapping `app.emit()`. +pub struct TauriEventEmitter { + pub app: AppHandle, +} + +impl TauriEventEmitter { + pub fn new(app: AppHandle) -> Self { + Self { app } + } +} + +impl EventEmitter for TauriEventEmitter { + fn emit(&self, event: &str, payload: Value) -> Result<(), String> { + self.app + .emit(event, payload) + .map_err(|e| format!("Failed to emit {event}: {e}")) + } +} + +// ── Mock ────────────────────────────────────────────────────────────────────── + +#[cfg(test)] +pub mod mocks { + use super::*; + use std::sync::{Arc, Mutex}; + + #[derive(Clone, Default)] + pub struct MockEventEmitter { + pub events: Arc>>, + should_fail: Arc>, + } + + impl MockEventEmitter { + pub fn new() -> Self { + Self::default() + } + + pub fn set_should_fail(&self, fail: bool) { + *self.should_fail.lock().unwrap() = fail; + } + + pub fn event_count(&self) -> usize { + self.events.lock().unwrap().len() + } + + pub fn events_named(&self, name: &str) -> Vec { + self.events + .lock() + .unwrap() + .iter() + .filter(|(n, _)| n == name) + .map(|(_, v)| v.clone()) + .collect() + } + } + + impl EventEmitter for MockEventEmitter { + fn emit(&self, event: &str, payload: Value) -> Result<(), String> { + if *self.should_fail.lock().unwrap() { + return Err(format!("Mock emit failure for {event}")); + } + self.events.lock().unwrap().push((event.to_string(), payload)); + Ok(()) + } + } +} + +#[cfg(test)] +mod tests { + use super::mocks::*; + use super::*; + + #[test] + fn test_mock_event_emitter() { + let emitter = MockEventEmitter::new(); + emitter.emit("agent:test", serde_json::json!({"foo": 1})).unwrap(); + assert_eq!(emitter.event_count(), 1); + assert_eq!(emitter.events_named("agent:test").len(), 1); + } + + #[test] + fn test_emit_or_log_does_not_panic_on_failure() { + let emitter = MockEventEmitter::new(); + emitter.set_should_fail(true); + emit_or_log(&emitter, "test", serde_json::json!({})); + assert_eq!(emitter.event_count(), 0); + } +} diff --git a/apps/desktop/src-tauri/src/context_watcher/commands.rs b/apps/desktop/src-tauri/src/context_watcher/commands.rs new file mode 100644 index 00000000..eef0657b --- /dev/null +++ b/apps/desktop/src-tauri/src/context_watcher/commands.rs @@ -0,0 +1,30 @@ +use std::sync::Arc; +use tauri::State; + +use super::watcher_manager::{IndexWatcherStatus, WatcherManager}; + +#[tauri::command] +pub async fn context_watcher_start( + repo_path: String, + state: State<'_, Arc>, +) -> Result<(), String> { + let wm = state.inner().clone(); + wm.start_watching(&repo_path).await +} + +#[tauri::command] +pub async fn context_watcher_stop( + repo_path: String, + state: State<'_, Arc>, +) -> Result<(), String> { + state.stop_watching(&repo_path).await; + Ok(()) +} + +#[tauri::command] +pub async fn context_watcher_status( + repo_path: String, + state: State<'_, Arc>, +) -> Result, String> { + Ok(state.get_status(&repo_path).await) +} diff --git a/apps/desktop/src-tauri/src/context_watcher/db.rs b/apps/desktop/src-tauri/src/context_watcher/db.rs new file mode 100644 index 00000000..8103673d --- /dev/null +++ b/apps/desktop/src-tauri/src/context_watcher/db.rs @@ -0,0 +1,127 @@ +use rusqlite::Connection; + +/// Create the `watched_repos` table if it doesn't exist. +pub fn ensure_watched_repos_table(conn: &Connection) -> rusqlite::Result<()> { + conn.execute_batch( + "CREATE TABLE IF NOT EXISTS watched_repos ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + repo_path TEXT NOT NULL UNIQUE, + last_used_at TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) + );", + ) +} + +// NOTE: chat-desktop's `get_or_create_machine_id` was intentionally dropped. +// SuperCoder has a single machine_id source — `commands::machine_id(app_state)` +// (settings key "machine_id"). The watcher reuses that exact value so its +// context-engine collection key matches the search client's. + +/// Upsert a repo as actively watched (updates `last_used_at`). +pub fn upsert_watched_repo(conn: &Connection, repo_path: &str) -> rusqlite::Result<()> { + conn.execute( + "INSERT INTO watched_repos (repo_path, last_used_at) + VALUES (?1, strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) + ON CONFLICT(repo_path) DO UPDATE SET last_used_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')", + [repo_path], + )?; + Ok(()) +} + +/// Get repos watched within the last 7 days. +pub fn get_active_watched_repos(conn: &Connection) -> rusqlite::Result> { + let mut stmt = conn.prepare( + "SELECT repo_path FROM watched_repos + WHERE last_used_at > strftime('%Y-%m-%dT%H:%M:%fZ', 'now', '-7 days')", + )?; + let rows = stmt.query_map([], |row| row.get(0))?; + let mut repos = Vec::new(); + for row in rows { + repos.push(row?); + } + Ok(repos) +} + +/// Delete repos older than 7 days. Returns number of rows deleted. +pub fn cleanup_stale_repos(conn: &Connection) -> rusqlite::Result { + let count = conn.execute( + "DELETE FROM watched_repos + WHERE last_used_at <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now', '-7 days')", + [], + )?; + Ok(count) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn setup_db() -> Connection { + let conn = Connection::open_in_memory().unwrap(); + conn.execute_batch( + "CREATE TABLE IF NOT EXISTS settings ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL, + updated_at TEXT DEFAULT CURRENT_TIMESTAMP + );", + ) + .unwrap(); + ensure_watched_repos_table(&conn).unwrap(); + conn + } + + #[test] + fn test_upsert_and_get_repos() { + let conn = setup_db(); + upsert_watched_repo(&conn, "/home/user/project-a").unwrap(); + upsert_watched_repo(&conn, "/home/user/project-b").unwrap(); + + let repos = get_active_watched_repos(&conn).unwrap(); + assert_eq!(repos.len(), 2); + assert!(repos.contains(&"/home/user/project-a".to_string())); + assert!(repos.contains(&"/home/user/project-b".to_string())); + } + + #[test] + fn test_stale_repo_excluded() { + let conn = setup_db(); + + // Insert a repo, then manually set its last_used_at to 8 days ago + upsert_watched_repo(&conn, "/home/user/old-project").unwrap(); + conn.execute( + "UPDATE watched_repos SET last_used_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now', '-8 days') + WHERE repo_path = '/home/user/old-project'", + [], + ) + .unwrap(); + + // Insert a fresh repo + upsert_watched_repo(&conn, "/home/user/fresh-project").unwrap(); + + let repos = get_active_watched_repos(&conn).unwrap(); + assert_eq!(repos.len(), 1); + assert_eq!(repos[0], "/home/user/fresh-project"); + } + + #[test] + fn test_cleanup_removes_stale() { + let conn = setup_db(); + + upsert_watched_repo(&conn, "/home/user/old").unwrap(); + conn.execute( + "UPDATE watched_repos SET last_used_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now', '-8 days') + WHERE repo_path = '/home/user/old'", + [], + ) + .unwrap(); + + upsert_watched_repo(&conn, "/home/user/recent").unwrap(); + + let deleted = cleanup_stale_repos(&conn).unwrap(); + assert_eq!(deleted, 1); + + let repos = get_active_watched_repos(&conn).unwrap(); + assert_eq!(repos.len(), 1); + assert_eq!(repos[0], "/home/user/recent"); + } +} diff --git a/apps/desktop/src-tauri/src/context_watcher/file_watcher.rs b/apps/desktop/src-tauri/src/context_watcher/file_watcher.rs new file mode 100644 index 00000000..6c7ff5e6 --- /dev/null +++ b/apps/desktop/src-tauri/src/context_watcher/file_watcher.rs @@ -0,0 +1,363 @@ +use notify::{Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher}; +use std::collections::HashSet; +use std::path::PathBuf; +use std::sync::Arc; +use std::time::{Duration, Instant}; +use tokio::sync::mpsc; +use tokio_util::sync::CancellationToken; + +use super::ignore_filter::IgnoreFilter; + +const DEBOUNCE_MS: u64 = 500; +// 60s cooldown → max 1 trigger per minute. +// Rationale: the watcher only watches the canonical main checkout. Agent coding +// sessions edit files in .agent-worktrees (blocked by IgnoreFilter), so the +// watcher rarely fires in practice. 1/min is plenty for catching user edits in +// their editor while keeping supercoder re-index load minimal. +const COOLDOWN_SECS: u64 = 60; + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +/// A batch of file changes ready to sync. +#[derive(Debug)] +pub struct ChangeBatch { + pub created_or_modified: Vec, + pub deleted: Vec, +} + +#[derive(Debug, thiserror::Error)] +pub enum WatcherError { + #[error("notify error: {0}")] + Notify(#[from] notify::Error), + + #[error("watcher channel closed")] + ChannelClosed, +} + +// --------------------------------------------------------------------------- +// FileWatcher +// --------------------------------------------------------------------------- + +pub struct FileWatcher { + _watcher: RecommendedWatcher, +} + +impl FileWatcher { + /// Start watching a repo directory. Returns a receiver for change batches. + /// + /// The watcher and debounce/throttle loop run as background tokio tasks. + /// Cancel via the provided token. + pub fn start( + repo_path: PathBuf, + filter: Arc, + cancel_token: CancellationToken, + ) -> Result<(Self, mpsc::Receiver), WatcherError> { + // Channel for raw notify events → debounce task + let (raw_tx, raw_rx) = std::sync::mpsc::channel::>(); + + let mut watcher = RecommendedWatcher::new( + move |res| { + let _ = raw_tx.send(res); + }, + notify::Config::default(), + )?; + + watcher.watch(&repo_path, RecursiveMode::Recursive)?; + + // Channel for debounced batches → consumer + let (batch_tx, batch_rx) = mpsc::channel::(16); + + // Spawn the debounce+throttle loop + tokio::spawn(debounce_loop(raw_rx, batch_tx, filter, cancel_token)); + + Ok((Self { _watcher: watcher }, batch_rx)) + } +} + +// --------------------------------------------------------------------------- +// Debounce + throttle loop +// --------------------------------------------------------------------------- + +async fn debounce_loop( + raw_rx: std::sync::mpsc::Receiver>, + batch_tx: mpsc::Sender, + filter: Arc, + cancel_token: CancellationToken, +) { + let mut pending_changes: HashSet = HashSet::new(); + let mut pending_deletes: HashSet = HashSet::new(); + let mut last_trigger: Option = None; + let mut debounce_deadline: Option = None; + let mut deferred_deadline: Option = None; + + loop { + tokio::select! { + _ = cancel_token.cancelled() => { + break; + } + + // Poll for events every 10ms, then check deadlines inline + _ = tokio::time::sleep(Duration::from_millis(10)) => { + let was_empty = !has_pending(&pending_changes, &pending_deletes); + + // Drain all available events from the notify channel + while let Ok(event_result) = raw_rx.try_recv() { + if let Ok(event) = event_result { + process_event(&event, &filter, &mut pending_changes, &mut pending_deletes); + } + } + + // Set debounce deadline on transition from empty → non-empty + if was_empty && has_pending(&pending_changes, &pending_deletes) { + debounce_deadline = Some(Instant::now() + Duration::from_millis(DEBOUNCE_MS)); + } + + // Check deadlines + let now = Instant::now(); + + // Check debounce deadline + if let Some(dd) = debounce_deadline { + if now >= dd { + debounce_deadline = None; + + let cooldown_ok = match last_trigger { + Some(lt) => now.duration_since(lt) >= Duration::from_secs(COOLDOWN_SECS), + None => true, + }; + + if cooldown_ok && has_pending(&pending_changes, &pending_deletes) { + emit_batch(&mut pending_changes, &mut pending_deletes, &batch_tx, &mut last_trigger).await; + } else if !cooldown_ok && has_pending(&pending_changes, &pending_deletes) { + if let Some(lt) = last_trigger { + deferred_deadline = Some(lt + Duration::from_secs(COOLDOWN_SECS)); + } + } + } + } + + // Check deferred deadline (cooldown expired) + if let Some(dd) = deferred_deadline { + if now >= dd { + deferred_deadline = None; + if has_pending(&pending_changes, &pending_deletes) { + emit_batch(&mut pending_changes, &mut pending_deletes, &batch_tx, &mut last_trigger).await; + } + } + } + } + } + } +} + +fn process_event( + event: &Event, + filter: &IgnoreFilter, + pending_changes: &mut HashSet, + pending_deletes: &mut HashSet, +) { + for path in &event.paths { + if !filter.should_include(path) { + continue; + } + + match event.kind { + EventKind::Create(_) | EventKind::Modify(_) => { + // Skip directories: `mkdir foo` events also fire here, and + // reading a directory with fs::read would fail EISDIR downstream. + // is_file() is cheap (one stat). Remove events don't need this + // guard — the path is already gone by the time we see them. + if !path.is_file() { + continue; + } + pending_deletes.remove(path); + pending_changes.insert(path.clone()); + } + EventKind::Remove(_) => { + pending_changes.remove(path); + pending_deletes.insert(path.clone()); + } + _ => {} + } + } +} + +fn has_pending(changes: &HashSet, deletes: &HashSet) -> bool { + !changes.is_empty() || !deletes.is_empty() +} + +async fn emit_batch( + pending_changes: &mut HashSet, + pending_deletes: &mut HashSet, + batch_tx: &mpsc::Sender, + last_trigger: &mut Option, +) { + let batch = ChangeBatch { + created_or_modified: pending_changes.drain().collect(), + deleted: pending_deletes.drain().collect(), + }; + *last_trigger = Some(Instant::now()); + let _ = batch_tx.send(batch).await; +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs; + use tempfile::TempDir; + + #[tokio::test] + async fn test_debounce_batches_rapid_events() { + let tmp = TempDir::new().unwrap(); + let filter = Arc::new(IgnoreFilter::new(tmp.path())); + let cancel = CancellationToken::new(); + + let (watcher, mut rx) = + FileWatcher::start(tmp.path().to_path_buf(), filter, cancel.clone()).unwrap(); + + // Write multiple files rapidly + for i in 0..5 { + fs::write(tmp.path().join(format!("file{i}.txt")), format!("content{i}")).unwrap(); + tokio::time::sleep(Duration::from_millis(50)).await; + } + + // Wait for debounce to fire (500ms + margin) + let batch = tokio::time::timeout(Duration::from_secs(3), rx.recv()) + .await + .expect("should receive batch") + .expect("channel not closed"); + + // All 5 files should be in a single batch (or fewer batches) + assert!( + !batch.created_or_modified.is_empty(), + "should have created/modified files" + ); + + cancel.cancel(); + drop(watcher); + } + + #[tokio::test] + async fn test_ignored_files_excluded() { + let tmp = TempDir::new().unwrap(); + let filter = Arc::new(IgnoreFilter::new(tmp.path())); + let cancel = CancellationToken::new(); + + let (watcher, mut rx) = + FileWatcher::start(tmp.path().to_path_buf(), filter, cancel.clone()).unwrap(); + + // Write an ignored file (png) + fs::write(tmp.path().join("image.png"), "fake png").unwrap(); + + // Write a normal file to trigger a batch + tokio::time::sleep(Duration::from_millis(100)).await; + fs::write(tmp.path().join("code.rs"), "fn main(){}").unwrap(); + + let batch = tokio::time::timeout(Duration::from_secs(3), rx.recv()) + .await + .expect("should receive batch") + .expect("channel not closed"); + + // The batch should contain code.rs but not image.png + let names: Vec<_> = batch + .created_or_modified + .iter() + .filter_map(|p| p.file_name().and_then(|n| n.to_str()).map(String::from)) + .collect(); + assert!(names.contains(&"code.rs".to_string())); + assert!(!names.contains(&"image.png".to_string())); + + cancel.cancel(); + drop(watcher); + } + + #[tokio::test] + async fn test_delete_events_classified() { + // Test the process_event function directly to avoid platform-specific + // fsevents behavior (macOS may not report Remove for pre-existing files). + let mut changes = HashSet::new(); + let mut deletes = HashSet::new(); + let tmp = TempDir::new().unwrap(); + let filter = IgnoreFilter::new(tmp.path()); + + let file = tmp.path().join("target_file.rs"); + // Create the file on disk — process_event now skips non-file paths + // (directory events, stale paths) via `path.is_file()` guard. + std::fs::write(&file, b"// test").unwrap(); + + // Simulate a create event + let create_event = Event { + kind: EventKind::Create(notify::event::CreateKind::File), + paths: vec![file.clone()], + attrs: Default::default(), + }; + process_event(&create_event, &filter, &mut changes, &mut deletes); + assert!(changes.contains(&file)); + assert!(!deletes.contains(&file)); + + // Simulate a remove event — should move from changes to deletes + let remove_event = Event { + kind: EventKind::Remove(notify::event::RemoveKind::File), + paths: vec![file.clone()], + attrs: Default::default(), + }; + process_event(&remove_event, &filter, &mut changes, &mut deletes); + assert!(!changes.contains(&file), "file should be removed from changes"); + assert!(deletes.contains(&file), "file should be in deletes"); + } + + #[tokio::test] + async fn test_cancel_token_stops_loop() { + let tmp = TempDir::new().unwrap(); + let filter = Arc::new(IgnoreFilter::new(tmp.path())); + let cancel = CancellationToken::new(); + + let (watcher, mut rx) = + FileWatcher::start(tmp.path().to_path_buf(), filter, cancel.clone()).unwrap(); + + // Cancel immediately + cancel.cancel(); + + // Channel should close (recv returns None) within a short time + let result = tokio::time::timeout(Duration::from_secs(2), rx.recv()).await; + // Either timeout (loop stopped, no batch) or None (channel closed) is acceptable + match result { + Ok(None) => {} // Channel closed — expected + Err(_) => {} // Timeout — loop stopped but channel not yet dropped + Ok(Some(_)) => {} // Got a batch before stopping — also fine + } + + drop(watcher); + } + + #[tokio::test] + async fn test_cooldown_defers_trigger() { + let tmp = TempDir::new().unwrap(); + let filter = Arc::new(IgnoreFilter::new(tmp.path())); + let cancel = CancellationToken::new(); + + let (watcher, mut rx) = + FileWatcher::start(tmp.path().to_path_buf(), filter, cancel.clone()).unwrap(); + + // Write first file — triggers first batch + fs::write(tmp.path().join("first.txt"), "a").unwrap(); + let _batch1 = tokio::time::timeout(Duration::from_secs(3), rx.recv()) + .await + .expect("should receive first batch") + .expect("channel not closed"); + + // Write second file immediately — should be deferred (cooldown active) + fs::write(tmp.path().join("second.txt"), "b").unwrap(); + + // The second batch should NOT arrive within 1 second (cooldown is 30s) + // but in tests we just verify the mechanism works by checking it eventually arrives + // We won't wait 30s in a test — just verify no immediate batch + let result = tokio::time::timeout(Duration::from_secs(1), rx.recv()).await; + // Should timeout because cooldown is active + assert!(result.is_err(), "should not receive batch during cooldown"); + + cancel.cancel(); + drop(watcher); + } +} diff --git a/apps/desktop/src-tauri/src/context_watcher/ignore_filter.rs b/apps/desktop/src-tauri/src/context_watcher/ignore_filter.rs new file mode 100644 index 00000000..ac15d8ac --- /dev/null +++ b/apps/desktop/src-tauri/src/context_watcher/ignore_filter.rs @@ -0,0 +1,375 @@ +use ignore::gitignore::{Gitignore, GitignoreBuilder}; +use std::path::{Path, PathBuf}; + +const BLOCKED_DIRS: &[&str] = &[ + ".git", + "node_modules", + "target", + "dist", + "build", + "__pycache__", + ".next", + "vendor", + ".agent-worktrees", + // Credential / config dirs — never sync to the context engine + ".ssh", + ".aws", + ".gnupg", +]; + +const BLOCKED_EXTENSIONS: &[&str] = &[ + "lock", "png", "jpg", "jpeg", "gif", "svg", "ico", "webp", "woff", "woff2", "ttf", "eot", + "mp3", "mp4", "avi", "mov", "zip", "tar", "gz", "bz2", "7z", "rar", "jar", "war", "exe", + "dll", "so", "dylib", "o", "a", "pyc", "class", "wasm", "map", + // Credential / secret file extensions — never sync to the context engine + "pem", "key", "cer", "crt", "p12", "pfx", "jks", "keystore", "asc", "gpg", +]; + +/// Compound extensions that need special matching (contain dots). +const BLOCKED_COMPOUND_EXTENSIONS: &[&str] = &["min.js", "min.css"]; + +/// Exact filenames (case-insensitive) that are always excluded — common secret files +/// that don't have a recognizable extension. This is a safety net on top of .gitignore, +/// which many users forget to update for local secret files. +const BLOCKED_FILENAMES: &[&str] = &[ + ".env", + ".env.local", + ".env.development", + ".env.production", + ".env.staging", + ".env.test", + "credentials", + "credentials.json", + "secrets.yaml", + "secrets.yml", + "secrets.json", + "id_rsa", + "id_dsa", + "id_ecdsa", + "id_ed25519", + ".netrc", + ".pgpass", + ".npmrc", + ".pypirc", +]; + +/// Filename prefixes that indicate a secret file (e.g., `.env.anything`). +const BLOCKED_FILENAME_PREFIXES: &[&str] = &[".env."]; + +/// Max file size to sync (1 MB). +const MAX_FILE_SIZE: u64 = 1_048_576; + +pub struct IgnoreFilter { + gitignore: Option, + repo_root: PathBuf, +} + +impl IgnoreFilter { + /// Build filter for a repo. Loads `.gitignore` if present. + pub fn new(repo_root: &Path) -> Self { + let gitignore_path = repo_root.join(".gitignore"); + let gitignore = if gitignore_path.exists() { + let mut builder = GitignoreBuilder::new(repo_root); + builder.add(gitignore_path); + builder.build().ok() + } else { + None + }; + + Self { + gitignore, + repo_root: repo_root.to_path_buf(), + } + } + + /// Returns `true` if a directory should be walked into (not blocked). + pub fn should_walk_dir(&self, dir: &Path) -> bool { + !self.is_blocked_dir(dir) + } + + /// Returns `true` if the file should be synced (not ignored). + pub fn should_include(&self, path: &Path) -> bool { + // Check blocked directory components + if self.is_blocked_dir(path) { + return false; + } + + // Check blocked filenames (secrets like .env, credentials, id_rsa) + if self.is_blocked_filename(path) { + return false; + } + + // Check blocked extensions + if self.has_blocked_extension(path) { + return false; + } + + // Check .gitignore + if let Some(ref gi) = self.gitignore { + let relative = path.strip_prefix(&self.repo_root).unwrap_or(path); + let is_dir = path.is_dir(); + if gi.matched(relative, is_dir).is_ignore() { + return false; + } + } + + // Check file size (only for existing files) + if let Ok(meta) = std::fs::metadata(path) { + if meta.is_file() && meta.len() > MAX_FILE_SIZE { + return false; + } + } + + true + } + + /// Check if any path component is a blocked directory. + fn is_blocked_dir(&self, path: &Path) -> bool { + for component in path.components() { + if let std::path::Component::Normal(name) = component { + if let Some(name_str) = name.to_str() { + if BLOCKED_DIRS.contains(&name_str) { + return true; + } + } + } + } + false + } + + /// Check if the file's base name matches a known secret file + /// (case-insensitive exact match or prefix match). + fn is_blocked_filename(&self, path: &Path) -> bool { + let file_name = match path.file_name().and_then(|n| n.to_str()) { + Some(n) => n, + None => return false, + }; + let lower = file_name.to_ascii_lowercase(); + + if BLOCKED_FILENAMES.iter().any(|&n| n == lower) { + return true; + } + if BLOCKED_FILENAME_PREFIXES.iter().any(|&p| lower.starts_with(p)) { + return true; + } + false + } + + /// Check if the file has a blocked extension. + fn has_blocked_extension(&self, path: &Path) -> bool { + let file_name = match path.file_name().and_then(|n| n.to_str()) { + Some(n) => n, + None => return false, + }; + + // Check compound extensions (e.g., "foo.min.js" but NOT "admin.js") + for ext in BLOCKED_COMPOUND_EXTENSIONS { + if file_name.ends_with(&format!(".{ext}")) { + return true; + } + } + + // Check simple extensions + if let Some(ext) = path.extension().and_then(|e| e.to_str()) { + if BLOCKED_EXTENSIONS.contains(&ext) { + return true; + } + } + + false + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs; + use tempfile::TempDir; + + fn make_filter(dir: &Path) -> IgnoreFilter { + IgnoreFilter::new(dir) + } + + #[test] + fn test_blocked_dirs() { + let tmp = TempDir::new().unwrap(); + let filter = make_filter(tmp.path()); + + assert!(!filter.should_include(&tmp.path().join("node_modules/foo.js"))); + assert!(!filter.should_include(&tmp.path().join(".git/config"))); + assert!(!filter.should_include(&tmp.path().join("target/debug/agent"))); + assert!(!filter.should_include(&tmp.path().join("__pycache__/mod.pyc"))); + assert!(!filter.should_include(&tmp.path().join(".agent-worktrees/ws1/file.rs"))); + // Credential dirs + assert!(!filter.should_include(&tmp.path().join(".ssh/id_rsa"))); + assert!(!filter.should_include(&tmp.path().join(".aws/credentials"))); + assert!(!filter.should_include(&tmp.path().join(".gnupg/private-keys-v1.d/x"))); + } + + #[test] + fn test_blocked_secret_filenames() { + let tmp = TempDir::new().unwrap(); + let filter = make_filter(tmp.path()); + + // Create the files so the metadata check passes + fs::write(tmp.path().join(".env"), "SECRET=1").unwrap(); + fs::write(tmp.path().join(".env.local"), "SECRET=1").unwrap(); + fs::write(tmp.path().join(".env.production"), "SECRET=1").unwrap(); + fs::write(tmp.path().join("credentials.json"), "{}").unwrap(); + fs::write(tmp.path().join("secrets.yaml"), "x: y").unwrap(); + fs::write(tmp.path().join("id_rsa"), "----- BEGIN -----").unwrap(); + fs::write(tmp.path().join(".netrc"), "machine x").unwrap(); + + assert!(!filter.should_include(&tmp.path().join(".env"))); + assert!(!filter.should_include(&tmp.path().join(".env.local"))); + assert!(!filter.should_include(&tmp.path().join(".env.production"))); + // Arbitrary .env.* suffix still blocked (prefix match) + fs::write(tmp.path().join(".env.custom"), "x=1").unwrap(); + assert!(!filter.should_include(&tmp.path().join(".env.custom"))); + assert!(!filter.should_include(&tmp.path().join("credentials.json"))); + assert!(!filter.should_include(&tmp.path().join("secrets.yaml"))); + assert!(!filter.should_include(&tmp.path().join("id_rsa"))); + assert!(!filter.should_include(&tmp.path().join(".netrc"))); + + // Case-insensitive + fs::write(tmp.path().join(".ENV"), "x=1").unwrap(); + assert!(!filter.should_include(&tmp.path().join(".ENV"))); + } + + #[test] + fn test_blocked_cert_key_extensions() { + let tmp = TempDir::new().unwrap(); + let filter = make_filter(tmp.path()); + + for ext in &["pem", "key", "p12", "pfx", "crt", "cer", "gpg"] { + let p = tmp.path().join(format!("cert.{ext}")); + fs::write(&p, "x").unwrap(); + assert!( + !filter.should_include(&p), + "cert.{ext} should be excluded", + ); + } + } + + #[test] + fn test_blocked_extensions() { + let tmp = TempDir::new().unwrap(); + let filter = make_filter(tmp.path()); + + // Create actual files so metadata check works + let png = tmp.path().join("image.png"); + fs::write(&png, "x").unwrap(); + assert!(!filter.should_include(&png)); + + let woff = tmp.path().join("font.woff"); + fs::write(&woff, "x").unwrap(); + assert!(!filter.should_include(&woff)); + + let zip = tmp.path().join("archive.zip"); + fs::write(&zip, "x").unwrap(); + assert!(!filter.should_include(&zip)); + + let exe = tmp.path().join("binary.exe"); + fs::write(&exe, "x").unwrap(); + assert!(!filter.should_include(&exe)); + } + + #[test] + fn test_lock_files() { + let tmp = TempDir::new().unwrap(); + let filter = make_filter(tmp.path()); + + // .lock extension → blocked + let cargo_lock = tmp.path().join("Cargo.lock"); + fs::write(&cargo_lock, "content").unwrap(); + assert!(!filter.should_include(&cargo_lock)); + + let yarn_lock = tmp.path().join("yarn.lock"); + fs::write(&yarn_lock, "content").unwrap(); + assert!(!filter.should_include(&yarn_lock)); + + // package-lock.json has .json extension — NOT blocked by extension + // (it would be blocked by .gitignore in real repos, not our filter) + let pkg_lock = tmp.path().join("package-lock.json"); + fs::write(&pkg_lock, "{}").unwrap(); + assert!(filter.should_include(&pkg_lock)); + } + + #[test] + fn test_source_files_included() { + let tmp = TempDir::new().unwrap(); + let filter = make_filter(tmp.path()); + + let rs = tmp.path().join("src/main.rs"); + fs::create_dir_all(rs.parent().unwrap()).unwrap(); + fs::write(&rs, "fn main() {}").unwrap(); + assert!(filter.should_include(&rs)); + + let ts = tmp.path().join("index.ts"); + fs::write(&ts, "export {}").unwrap(); + assert!(filter.should_include(&ts)); + + let go = tmp.path().join("lib/auth.go"); + fs::create_dir_all(go.parent().unwrap()).unwrap(); + fs::write(&go, "package lib").unwrap(); + assert!(filter.should_include(&go)); + } + + #[test] + fn test_gitignore_respected() { + let tmp = TempDir::new().unwrap(); + + // Create .gitignore + fs::write(tmp.path().join(".gitignore"), "*.log\n").unwrap(); + + let filter = make_filter(tmp.path()); + + let log_file = tmp.path().join("app.log"); + fs::write(&log_file, "log data").unwrap(); + assert!(!filter.should_include(&log_file)); + + // Non-ignored file should pass + let txt = tmp.path().join("readme.txt"); + fs::write(&txt, "hello").unwrap(); + assert!(filter.should_include(&txt)); + } + + #[test] + fn test_max_file_size() { + let tmp = TempDir::new().unwrap(); + let filter = make_filter(tmp.path()); + + // Create file > 1 MB + let big = tmp.path().join("big.txt"); + let data = vec![b'x'; MAX_FILE_SIZE as usize + 1]; + fs::write(&big, &data).unwrap(); + assert!(!filter.should_include(&big)); + + // File exactly at limit should be included + let ok = tmp.path().join("ok.txt"); + let data = vec![b'x'; MAX_FILE_SIZE as usize]; + fs::write(&ok, &data).unwrap(); + assert!(filter.should_include(&ok)); + } + + #[test] + fn test_should_walk_dir() { + let tmp = TempDir::new().unwrap(); + let filter = make_filter(tmp.path()); + + assert!(!filter.should_walk_dir(&tmp.path().join("node_modules"))); + assert!(!filter.should_walk_dir(&tmp.path().join(".git"))); + assert!(!filter.should_walk_dir(&tmp.path().join("target"))); + assert!(filter.should_walk_dir(&tmp.path().join("src"))); + assert!(filter.should_walk_dir(&tmp.path().join("lib"))); + } + + #[test] + fn test_nested_blocked_dir() { + let tmp = TempDir::new().unwrap(); + let filter = make_filter(tmp.path()); + + assert!(!filter.should_include(&tmp.path().join("foo/bar/node_modules/baz.js"))); + assert!(!filter.should_include(&tmp.path().join("deep/nested/.git/objects/pack"))); + } +} diff --git a/apps/desktop/src-tauri/src/context_watcher/mod.rs b/apps/desktop/src-tauri/src/context_watcher/mod.rs new file mode 100644 index 00000000..1ddfccce --- /dev/null +++ b/apps/desktop/src-tauri/src/context_watcher/mod.rs @@ -0,0 +1,8 @@ +pub mod commands; +pub mod db; +pub mod file_watcher; +pub mod ignore_filter; +pub mod streamer; +pub mod watcher_manager; + +pub use watcher_manager::{IndexWatcherStatus, WatcherManager}; diff --git a/apps/desktop/src-tauri/src/context_watcher/streamer.rs b/apps/desktop/src-tauri/src/context_watcher/streamer.rs new file mode 100644 index 00000000..f85fdd5e --- /dev/null +++ b/apps/desktop/src-tauri/src/context_watcher/streamer.rs @@ -0,0 +1,2105 @@ +//! WS7: Client streaming module that replaces the S3 upload path. +//! +//! Four-phase pipeline against the supercoder context engine: +//! Phase 0: walk repo, read & hash files (with hard caps) +//! Phase 1: POST {base}/index/diff → server returns sync_id + need[] + delete[] +//! Phase 2: POST {base}/index/stream → batched NDJSON, parallel, deterministic batch_id +//! Phase 3: POST {base}/index/sync-complete (poll) → 200/202/409/410 +//! +//! See `llm_context/streaming-refactor/07-ws7-client-streamer.md` for the full spec. + +use std::collections::HashSet; +use std::path::{Path, PathBuf}; +use std::sync::Arc; +use std::time::{Duration, Instant}; + +use base64::engine::general_purpose::STANDARD as BASE64_STANDARD; +use base64::Engine; +use flate2::write::GzEncoder; +use flate2::Compression; +use reqwest::header::{CONTENT_ENCODING, CONTENT_TYPE}; +use reqwest::StatusCode; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; +use tokio::sync::Semaphore; +use tokio::task::JoinSet; +use tokio_util::sync::CancellationToken; + +use super::ignore_filter::IgnoreFilter; + +// ────────────────── Constants ────────────────── + +const MAX_BATCH_BYTES: usize = 2 * 1024 * 1024; // 2 MB uncompressed +const MAX_BATCH_FILES: usize = 100; +const STREAM_CONCURRENCY: usize = 10; +const SYNC_DEADLINE: Duration = Duration::from_secs(300); +const POLL_INTERVAL: Duration = Duration::from_secs(2); +const MAX_RETRY_ATTEMPTS: u32 = 3; + +// Phase 0 hard caps (plan §3.7) +const MAX_FILE_SIZE_BYTES: u64 = 1_048_576; // 1 MB per file +const MAX_REPO_BYTES: u64 = 100 * 1024 * 1024; // 100 MB total +const MAX_REPO_FILES: usize = 20_000; +const MAX_DIFF_BODY_GZ_BYTES: usize = 5 * 1024 * 1024; // 5 MB gzipped + +// ────────────────── Public types ────────────────── + +#[derive(Debug, Clone)] +pub struct StreamerConfig { + pub context_engine_url: String, + pub user_id: String, + pub workspace_id: u64, + pub machine_id: String, + pub repo_path: String, + /// APISIX gateway auth token (X-Auth-Token header). Empty = no auth. + pub auth_token: String, +} + +#[derive(Debug, Clone, Default)] +pub struct SyncStats { + pub files_hashed: usize, + pub files_uploaded: usize, + pub files_deleted: usize, + pub bytes_uploaded: u64, + pub duplicate_batches: usize, + pub duration: Duration, +} + +#[derive(Debug, thiserror::Error)] +pub enum SyncError { + #[error("repo too large: {bytes} bytes (max {max})")] + RepoTooLarge { bytes: u64, max: u64 }, + + #[error("too many files: {count} (max {max})")] + TooManyFiles { count: usize, max: usize }, + + #[error("sync cancelled")] + Cancelled, + + #[error("sync deadline exceeded")] + DeadlineExceeded, + + #[error("server returned 410 Gone: {reason}")] + SyncExpired { reason: String }, + + #[error("server returned 400 Bad Request: {reason} ({details})")] + BadRequest { reason: String, details: String }, + + #[error("diff request too large: {bytes} bytes gzipped (max {max})")] + RequestTooLarge { bytes: usize, max: usize }, + + #[error("auth required: HTTP {status}")] + AuthRequired { status: u16 }, + + #[error("HTTP error: {0}")] + Http(#[from] reqwest::Error), + + #[error("I/O error: {0}")] + Io(#[from] std::io::Error), + + #[error("serialization error: {0}")] + Serialization(#[from] serde_json::Error), + + #[error("other: {0}")] + Other(String), +} + +// ────────────────── Streamer ────────────────── + +pub struct Streamer { + config: StreamerConfig, + http: reqwest::Client, + /// Deadline for `/sync-complete` polling. Defaults to `SYNC_DEADLINE`, + /// overridable in tests via `new_with_timings`. + sync_deadline: Duration, + /// Sleep between `/sync-complete` polls. Defaults to `POLL_INTERVAL`, + /// overridable in tests via `new_with_timings`. + poll_interval: Duration, +} + +impl Streamer { + pub fn new(config: StreamerConfig) -> Self { + let http = reqwest::Client::builder() + .connect_timeout(Duration::from_secs(10)) + .timeout(Duration::from_secs(60)) + .build() + .expect("reqwest client"); + Self { + config, + http, + sync_deadline: SYNC_DEADLINE, + poll_interval: POLL_INTERVAL, + } + } + + /// Test-only constructor that allows overriding the polling timings so + /// tests don't have to wait minutes. + #[cfg(test)] + pub(crate) fn new_with_timings( + config: StreamerConfig, + sync_deadline: Duration, + poll_interval: Duration, + ) -> Self { + let mut s = Self::new(config); + s.sync_deadline = sync_deadline; + s.poll_interval = poll_interval; + s + } + + /// Run a full sync: walk the tree, hash everything, call /diff, stream all needed + /// batches, poll /sync-complete. Sends `incremental=false` so the server computes + /// deletes by comparing the client's complete view against the Merkle tree. + pub async fn full_sync( + &self, + repo_root: &Path, + filter: Arc, + cancel: CancellationToken, + ) -> Result { + let started = Instant::now(); + let mut stats = SyncStats::default(); + + // Phase 0 — walk + hash. `walk_and_hash` does synchronous filesystem + // I/O (ignore::WalkBuilder + std::fs::read) which would block a Tokio + // worker thread for seconds on large repos, so we offload it to the + // blocking pool. The CancellationToken is checked per-entry inside + // the walk, so early cancellation still works. + let repo_root_owned = repo_root.to_path_buf(); + let filter_for_walk = Arc::clone(&filter); + let cancel_for_walk = cancel.clone(); + let entries = tokio::task::spawn_blocking(move || { + walk_and_hash( + &repo_root_owned, + &filter_for_walk, + &cancel_for_walk, + MAX_FILE_SIZE_BYTES, + MAX_REPO_BYTES, + MAX_REPO_FILES, + ) + }) + .await + .map_err(|e| SyncError::Other(format!("walk_and_hash join: {e}")))??; + stats.files_hashed = entries.len(); + + // Phase 1 + let diff_response = self.call_diff(&entries, false, &[], &cancel).await?; + log::info!( + "[streamer] /diff sync_id={} need={} delete={}", + diff_response.sync_id, + diff_response.need.len(), + diff_response.delete.len() + ); + + if diff_response.need.is_empty() && diff_response.delete.is_empty() { + stats.duration = started.elapsed(); + return Ok(stats); + } + + // Phase 2 + let needed = entries_filtered_by_need(&entries, &diff_response.need); + let batch_stats = self + .stream_batches( + &diff_response.sync_id, + needed, + diff_response.delete.clone(), + &cancel, + ) + .await?; + stats.files_uploaded = batch_stats.files_uploaded; + stats.files_deleted = batch_stats.files_deleted; + stats.bytes_uploaded = batch_stats.bytes_uploaded; + stats.duplicate_batches = batch_stats.duplicate_batches; + + // Phase 3 + self.poll_sync_complete(&diff_response.sync_id, &cancel) + .await?; + + stats.duration = started.elapsed(); + Ok(stats) + } + + /// Incremental sync from the file watcher. `changed` is the list of files reported + /// as added/modified; `deleted` is the explicit list of files reported as removed. + /// + /// Sends `incremental=true` + the explicit `deleted` list so the server does NOT + /// infer deletes from "every Merkle path missing from this request" (which would + /// otherwise wipe the index every keystroke). See WS3 §1 for the server contract. + pub async fn incremental_sync( + &self, + changed: &[PathBuf], + deleted: &[PathBuf], + repo_root: &Path, + cancel: CancellationToken, + ) -> Result { + let started = Instant::now(); + let mut stats = SyncStats::default(); + + let mut entries = Vec::with_capacity(changed.len()); + for path in changed { + if cancel.is_cancelled() { + return Err(SyncError::Cancelled); + } + // Skip directories — file watchers emit events for directory creation + // (e.g. `mkdir foo`) and reading a directory with fs::read returns + // `EISDIR (21)` on Unix / ACCESS_DENIED on Windows, which would fail + // the entire sync. + if !path.is_file() { + log::debug!("[streamer] skipping non-file: {}", path.display()); + continue; + } + let rel_path = path + .strip_prefix(repo_root) + .map(|p| p.to_string_lossy().to_string()) + .unwrap_or_else(|_| path.to_string_lossy().to_string()); + let content = match tokio::fs::read(path).await { + Ok(c) => c, + Err(err) + if err.kind() == std::io::ErrorKind::NotFound + || err.kind() == std::io::ErrorKind::PermissionDenied + || err.raw_os_error() == Some(21) // EISDIR + || err.raw_os_error() == Some(26) // ETXTBSY (Linux) — busy executable + || err.raw_os_error() == Some(32) => // ERROR_SHARING_VIOLATION (Windows) + { + log::debug!("[streamer] skipping {}: {}", path.display(), err); + continue; + } + Err(err) => return Err(SyncError::Io(err)), + }; + if content.len() as u64 > MAX_FILE_SIZE_BYTES { + continue; + } + // See `walk_and_hash` for the rationale: non-UTF-8 content would be + // mangled by `String::from_utf8_lossy` in `build_batch_ndjson` and + // fail server-side hash validation. + if std::str::from_utf8(&content).is_err() { + log::debug!( + "[streamer] skipping non-UTF-8 file: {}", + path.display() + ); + continue; + } + let hash = sha256_hex(&content); + entries.push(FileEntry { + path: rel_path, + hash, + content, + }); + } + stats.files_hashed = entries.len(); + + let delete_strs: Vec = deleted + .iter() + .map(|p| { + p.strip_prefix(repo_root) + .map(|pp| pp.to_string_lossy().to_string()) + .unwrap_or_else(|_| p.to_string_lossy().to_string()) + }) + .collect(); + + let diff_response = self.call_diff(&entries, true, &delete_strs, &cancel).await?; + + if diff_response.need.is_empty() && diff_response.delete.is_empty() { + stats.duration = started.elapsed(); + return Ok(stats); + } + + let needed = entries_filtered_by_need(&entries, &diff_response.need); + let batch_stats = self + .stream_batches( + &diff_response.sync_id, + needed, + diff_response.delete.clone(), + &cancel, + ) + .await?; + stats.files_uploaded = batch_stats.files_uploaded; + stats.files_deleted = batch_stats.files_deleted; + stats.bytes_uploaded = batch_stats.bytes_uploaded; + stats.duplicate_batches = batch_stats.duplicate_batches; + + self.poll_sync_complete(&diff_response.sync_id, &cancel) + .await?; + + stats.duration = started.elapsed(); + Ok(stats) + } + + // ────────────────── Phase 1: /diff ────────────────── + + async fn call_diff( + &self, + entries: &[FileEntry], + incremental: bool, + deletes: &[String], + cancel: &CancellationToken, + ) -> Result { + if cancel.is_cancelled() { + return Err(SyncError::Cancelled); + } + + let files: Vec = entries + .iter() + .map(|e| DiffFileHash { + path: e.path.clone(), + sha256: e.hash.clone(), + }) + .collect(); + + let req = DiffRequest { + user_id: self.config.user_id.clone(), + workspace_id: self.config.workspace_id, + machine_id: self.config.machine_id.clone(), + repo_path: self.config.repo_path.clone(), + github_org_id: None, + files, + incremental, + deletes: deletes.to_vec(), + }; + + let body_json = serde_json::to_vec(&req)?; + let body_gz = gzip_encode(&body_json)?; + + if body_gz.len() > MAX_DIFF_BODY_GZ_BYTES { + return Err(SyncError::RequestTooLarge { + bytes: body_gz.len(), + max: MAX_DIFF_BODY_GZ_BYTES, + }); + } + + let url = format!("{}/index/diff", self.config.context_engine_url); + + // Retry loop: 429 from the server means another client is currently + // syncing the same identity. Transient, self-resolves — back off and + // retry up to MAX_DIFF_RETRY times before surfacing as a user-visible + // error. 5xx and other transport errors still propagate immediately + // (reqwest handles those; we only retry the explicit 429 here). + const MAX_DIFF_RETRY: u32 = 3; + let mut attempt: u32 = 0; + loop { + if cancel.is_cancelled() { + return Err(SyncError::Cancelled); + } + let response = self + .http + .post(&url) + .header("X-USER-ID", &self.config.user_id) + .header("X-Workspace-ID", self.config.workspace_id.to_string()) + .header("X-Auth-Token", &self.config.auth_token) + .header(CONTENT_TYPE, "application/json") + .header(CONTENT_ENCODING, "gzip") + .body(body_gz.clone()) + .send() + .await?; + + match response.status() { + StatusCode::OK => return Ok(response.json::().await?), + StatusCode::TOO_MANY_REQUESTS if attempt < MAX_DIFF_RETRY => { + attempt += 1; + let backoff = Duration::from_secs(2u64.saturating_pow(attempt)); + log::warn!( + "[streamer] /diff 429 (attempt {}/{}), backoff {:?}", + attempt, MAX_DIFF_RETRY, backoff + ); + tokio::select! { + _ = cancel.cancelled() => return Err(SyncError::Cancelled), + _ = tokio::time::sleep(backoff) => {} + } + continue; + } + StatusCode::TOO_MANY_REQUESTS => { + return Err(SyncError::Other("concurrent sync in progress".into())); + } + StatusCode::UNAUTHORIZED | StatusCode::FORBIDDEN => { + return Err(SyncError::AuthRequired { + status: response.status().as_u16(), + }); + } + status => { + let body = response.text().await.unwrap_or_default(); + return Err(SyncError::Other(format!( + "unexpected /diff response {}: {}", + status, body + ))); + } + } + } + } + + // ────────────────── Phase 2: /stream ────────────────── + + async fn stream_batches( + &self, + sync_id: &str, + needed: Vec, + deletes: Vec, + cancel: &CancellationToken, + ) -> Result { + let batches = chunk_into_batches(needed, deletes); + log::info!("[streamer] stream_batches: {} batches", batches.len()); + + let sem = Arc::new(Semaphore::new(STREAM_CONCURRENCY)); + let mut joinset: JoinSet> = JoinSet::new(); + let mut stats = BatchStats::default(); + + // Per-call child token. Scope cancellation to this single stream_batches + // invocation so early-return (parent cancel or batch error) can cancel + // every in-flight upload without killing the repo-level parent token + // (which is reused across future syncs). + let child_cancel = cancel.child_token(); + + for batch in batches { + if cancel.is_cancelled() { + abort_and_drain(&mut joinset, &child_cancel).await; + return Err(SyncError::Cancelled); + } + let permit = sem.clone().acquire_owned().await.unwrap(); + let sync_id = sync_id.to_string(); + let streamer = self.clone_for_task(); + let task_cancel = child_cancel.clone(); + joinset.spawn(async move { + let _permit = permit; + streamer + .stream_one_batch(&sync_id, batch, &task_cancel) + .await + }); + } + + while let Some(join_res) = joinset.join_next().await { + match join_res { + Ok(Ok(outcome)) => { + stats.files_uploaded += outcome.files_uploaded; + stats.files_deleted += outcome.files_deleted; + stats.bytes_uploaded += outcome.bytes_uploaded; + if outcome.duplicate { + stats.duplicate_batches += 1; + } + } + Ok(Err(err)) => { + abort_and_drain(&mut joinset, &child_cancel).await; + return Err(err); + } + Err(join_err) => { + abort_and_drain(&mut joinset, &child_cancel).await; + return Err(SyncError::Other(format!("join: {join_err}"))); + } + } + } + + Ok(stats) + } + + async fn stream_one_batch( + &self, + sync_id: &str, + batch: Batch, + cancel: &CancellationToken, + ) -> Result { + let batch_id = deterministic_batch_id(&batch); + let ndjson = build_batch_ndjson(&batch); + let body_gz = gzip_encode(&ndjson)?; + + let url = format!("{}/index/stream", self.config.context_engine_url); + + let mut attempt = 0u32; + loop { + if cancel.is_cancelled() { + return Err(SyncError::Cancelled); + } + + let response = self + .http + .post(&url) + .header("X-USER-ID", &self.config.user_id) + .header("X-Workspace-ID", self.config.workspace_id.to_string()) + .header("X-Auth-Token", &self.config.auth_token) + .header("X-Sync-Id", sync_id) + .header("X-Batch-Id", &batch_id) + .header("X-Batch-Seq", "0") + .header(CONTENT_TYPE, "application/x-ndjson") + .header(CONTENT_ENCODING, "gzip") + .body(body_gz.clone()) + .send() + .await; + + match response { + Ok(resp) if resp.status() == StatusCode::ACCEPTED => { + let parsed: StreamBatchResponse = resp.json().await?; + let queued = parsed.queued.unwrap_or_default(); + return Ok(SingleBatchOutcome { + files_uploaded: queued.files, + files_deleted: queued.deletes, + bytes_uploaded: queued.bytes, + duplicate: false, + }); + } + Ok(resp) if resp.status() == StatusCode::OK => { + return Ok(SingleBatchOutcome { + files_uploaded: 0, + files_deleted: 0, + bytes_uploaded: 0, + duplicate: true, + }); + } + Ok(resp) if resp.status() == StatusCode::GONE => { + let body = resp.text().await.unwrap_or_default(); + return Err(SyncError::SyncExpired { reason: body }); + } + Ok(resp) if resp.status() == StatusCode::BAD_REQUEST => { + let body: serde_json::Value = resp.json().await.unwrap_or_default(); + let reason = body + .get("error") + .and_then(|v| v.as_str()) + .unwrap_or("unknown") + .to_string(); + let details = body + .get("details") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(); + return Err(SyncError::BadRequest { reason, details }); + } + Ok(resp) + if matches!( + resp.status(), + StatusCode::UNAUTHORIZED | StatusCode::FORBIDDEN | StatusCode::NOT_FOUND + ) => + { + return Err(SyncError::AuthRequired { + status: resp.status().as_u16(), + }); + } + Ok(resp) => { + let status = resp.status(); + if attempt < MAX_RETRY_ATTEMPTS { + attempt += 1; + let backoff = Duration::from_millis(100u64 * (1 << attempt)); + tokio::time::sleep(backoff).await; + continue; + } + return Err(SyncError::Other(format!("HTTP {}", status))); + } + Err(e) if e.is_timeout() || e.is_connect() => { + if attempt < MAX_RETRY_ATTEMPTS { + attempt += 1; + let backoff = Duration::from_millis(100u64 * (1 << attempt)); + tokio::time::sleep(backoff).await; + continue; + } + return Err(SyncError::Http(e)); + } + Err(e) => return Err(SyncError::Http(e)), + } + } + } + + // ────────────────── Phase 3: /sync-complete ────────────────── + + async fn poll_sync_complete( + &self, + sync_id: &str, + cancel: &CancellationToken, + ) -> Result<(), SyncError> { + let deadline = Instant::now() + self.sync_deadline; + let url = format!("{}/index/sync-complete", self.config.context_engine_url); + + loop { + if cancel.is_cancelled() { + return Err(SyncError::Cancelled); + } + if Instant::now() >= deadline { + // All batches were already accepted by the server (we only reach + // poll_sync_complete after stream_batches returned Ok). Exceeding + // the poll deadline doesn't mean the data is lost — the server is + // just slow to finalize. Treat as success with a warning. + log::warn!( + "[streamer] sync-complete poll deadline exceeded for sync_id={} \ + — batches were delivered; treating as success", + sync_id + ); + return Ok(()); + } + + let body = serde_json::json!({ "sync_id": sync_id }).to_string(); + let response = self + .http + .post(&url) + .header("X-USER-ID", &self.config.user_id) + .header("X-Workspace-ID", self.config.workspace_id.to_string()) + .header("X-Auth-Token", &self.config.auth_token) + .header(CONTENT_TYPE, "application/json") + .body(body) + .send() + .await?; + + match response.status() { + StatusCode::OK => { + log::info!("[streamer] sync complete sync_id={}", sync_id); + return Ok(()); + } + StatusCode::ACCEPTED | StatusCode::CONFLICT => { + tokio::select! { + _ = cancel.cancelled() => return Err(SyncError::Cancelled), + _ = tokio::time::sleep(self.poll_interval) => {} + } + continue; + } + StatusCode::GONE => { + // Server already finalized and cleaned up the sync_id record. + // This is a success for /sync-complete (unlike /stream, where + // 410 means the sync was aborted). The data IS indexed. + let body = response.text().await.unwrap_or_default(); + log::info!( + "[streamer] sync_id={} already finalized (410): {}", + sync_id, body + ); + return Ok(()); + } + status => { + let body = response.text().await.unwrap_or_default(); + return Err(SyncError::Other(format!( + "unexpected /sync-complete response {}: {}", + status, body + ))); + } + } + } + } + + fn clone_for_task(&self) -> Self { + Self { + config: self.config.clone(), + http: self.http.clone(), + sync_deadline: self.sync_deadline, + poll_interval: self.poll_interval, + } + } +} + +/// Cancel the scoped child token, hard-abort every remaining task in the +/// `JoinSet`, and drain it so no detached tasks survive the early return. +/// See C1 in `llm_context/streaming-refactor/followups-prioritized.md`. +async fn abort_and_drain( + joinset: &mut JoinSet>, + child: &CancellationToken, +) { + child.cancel(); + joinset.abort_all(); + while joinset.join_next().await.is_some() {} +} + +// ────────────────── Internal types ────────────────── + +#[derive(Debug, Clone)] +struct FileEntry { + path: String, + hash: String, + content: Vec, +} + +#[derive(Debug)] +struct Batch { + files: Vec, + deletes: Vec, +} + +#[derive(Default)] +struct BatchStats { + files_uploaded: usize, + files_deleted: usize, + bytes_uploaded: u64, + duplicate_batches: usize, +} + +struct SingleBatchOutcome { + files_uploaded: usize, + files_deleted: usize, + bytes_uploaded: u64, + duplicate: bool, +} + +#[derive(Serialize)] +struct DiffRequest { + // Identity MUST live in the request body (WS3 decision #0 in + // `llm_context/streaming-refactor/ws-takeways.md`). The server's + // `dto.IndexRequest` reads these from the JSON body, not from + // X-USER-ID / X-Workspace-ID headers. We still send the headers + // on the HTTP request for forward-compat, but the body is what + // the supercoder `/diff` handler actually validates against. + user_id: String, + workspace_id: u64, + machine_id: String, + repo_path: String, + #[serde(skip_serializing_if = "Option::is_none")] + github_org_id: Option, + files: Vec, + // Defaults are correct for full_sync (false / empty), so the wire format + // for an existing full sync still default-omits both fields. + #[serde(skip_serializing_if = "std::ops::Not::not")] + incremental: bool, + #[serde(skip_serializing_if = "Vec::is_empty")] + deletes: Vec, +} + +#[derive(Serialize)] +struct DiffFileHash { + path: String, + sha256: String, +} + +#[derive(Deserialize)] +struct DiffResponse { + sync_id: String, + need: Vec, + delete: Vec, +} + +#[derive(Deserialize)] +struct StreamBatchResponse { + queued: Option, + #[serde(default)] + #[allow(dead_code)] + duplicate: bool, +} + +#[derive(Deserialize, Default)] +struct QueuedCounts { + files: usize, + deletes: usize, + bytes: u64, +} + +// ────────────────── Helpers ────────────────── + +fn sha256_hex(data: &[u8]) -> String { + let mut hasher = Sha256::new(); + hasher.update(data); + format!("{:x}", hasher.finalize()) +} + +fn gzip_encode(data: &[u8]) -> Result, std::io::Error> { + use std::io::Write; + let mut encoder = GzEncoder::new(Vec::new(), Compression::default()); + encoder.write_all(data)?; + encoder.finish() +} + +/// Walk + read + hash, with overridable caps. Public path uses module constants; +/// tests pass smaller limits to keep test repos cheap. +fn walk_and_hash( + repo_root: &Path, + filter: &IgnoreFilter, + cancel: &CancellationToken, + max_file_bytes: u64, + max_repo_bytes: u64, + max_repo_files: usize, +) -> Result, SyncError> { + let mut entries = Vec::new(); + let mut total_bytes: u64 = 0; + + let walker = ignore::WalkBuilder::new(repo_root).build(); + for result in walker { + if cancel.is_cancelled() { + return Err(SyncError::Cancelled); + } + + let entry = match result { + Ok(e) => e, + Err(err) => { + log::warn!("[streamer] walk error: {err}"); + continue; + } + }; + let path = entry.path(); + if !path.is_file() || !filter.should_include(path) { + continue; + } + + let metadata = match std::fs::metadata(path) { + Ok(m) => m, + Err(_) => continue, + }; + if metadata.len() > max_file_bytes { + continue; + } + + let content = match std::fs::read(path) { + Ok(c) => c, + Err(e) + if e.kind() == std::io::ErrorKind::NotFound + || e.kind() == std::io::ErrorKind::PermissionDenied + || e.raw_os_error() == Some(21) // EISDIR + || e.raw_os_error() == Some(26) // ETXTBSY (Linux) — busy executable + || e.raw_os_error() == Some(32) => // ERROR_SHARING_VIOLATION (Windows) + { + log::debug!("[streamer] skipping {}: {}", path.display(), e); + continue; + } + Err(e) => return Err(SyncError::Io(e)), + }; + + // Skip non-UTF-8 files: the wire format encodes `content` as a JSON string + // (built via `String::from_utf8_lossy` in `build_batch_ndjson`). Any lossy + // replacement would change the bytes the server hashes, causing the WS4 + // stale-content check to reject the batch with `400 sha256_mismatch`. The + // `IgnoreFilter` already excludes the common binary extensions; this is a + // belt-and-suspenders guard for non-UTF-8 text files (Latin-1, etc.). + if std::str::from_utf8(&content).is_err() { + log::debug!( + "[streamer] skipping non-UTF-8 file: {}", + path.display() + ); + continue; + } + + total_bytes += content.len() as u64; + + if total_bytes > max_repo_bytes { + return Err(SyncError::RepoTooLarge { + bytes: total_bytes, + max: max_repo_bytes, + }); + } + if entries.len() >= max_repo_files { + return Err(SyncError::TooManyFiles { + count: entries.len() + 1, + max: max_repo_files, + }); + } + + let hash = sha256_hex(&content); + let rel_path = path + .strip_prefix(repo_root) + .unwrap_or(path) + .to_string_lossy() + .to_string(); + entries.push(FileEntry { + path: rel_path, + hash, + content, + }); + } + + log::info!( + "[streamer] walk_and_hash: {} files, {} bytes", + entries.len(), + total_bytes + ); + Ok(entries) +} + +/// Greedy chunker: pack files into batches respecting MAX_BATCH_FILES and MAX_BATCH_BYTES. +/// Deletes are tacked onto the first batch (or a delete-only batch if there are no files). +fn chunk_into_batches(needed: Vec, deletes: Vec) -> Vec { + let mut batches: Vec = Vec::new(); + let mut current = Batch { + files: Vec::new(), + deletes: Vec::new(), + }; + let mut current_bytes: usize = 0; + + for entry in needed { + let entry_size = entry.content.len(); + if !current.files.is_empty() + && (current.files.len() >= MAX_BATCH_FILES + || current_bytes + entry_size > MAX_BATCH_BYTES) + { + batches.push(current); + current = Batch { + files: Vec::new(), + deletes: Vec::new(), + }; + current_bytes = 0; + } + current_bytes += entry_size; + current.files.push(entry); + } + if !current.files.is_empty() { + batches.push(current); + } + + if let Some(first) = batches.first_mut() { + first.deletes = deletes; + } else if !deletes.is_empty() { + batches.push(Batch { + files: Vec::new(), + deletes, + }); + } + + batches +} + +/// Build the canonical batch entry list and compute the deterministic batch_id. +/// MUST match the server-side recomputation in WS4 §5.2 byte-for-byte. +fn deterministic_batch_id(batch: &Batch) -> String { + let mut entries: Vec = Vec::new(); + for f in &batch.files { + entries.push(format!("file|{}|{}", f.path, f.hash)); + } + for d in &batch.deletes { + entries.push(format!("delete|{}|", d)); + } + entries.sort(); + sha256_hex(entries.join("\n").as_bytes()) +} + +fn build_batch_ndjson(batch: &Batch) -> Vec { + let mut out = Vec::new(); + for f in &batch.files { + // Server expects base64-encoded bytes so the wire format is binary-safe + // (matches `services/supercoder/scripts/smoke_index_routes.py` and the + // WS6 #17 reference wire format). Raw UTF-8 strings get rejected with + // `illegal base64 data at input byte N` by the server's decoder. + let line = serde_json::json!({ + "op": "file", + "path": f.path, + "sha256": f.hash, + "content": BASE64_STANDARD.encode(&f.content), + }); + out.extend_from_slice(serde_json::to_vec(&line).unwrap().as_slice()); + out.push(b'\n'); + } + for d in &batch.deletes { + let line = serde_json::json!({ "op": "delete", "path": d }); + out.extend_from_slice(serde_json::to_vec(&line).unwrap().as_slice()); + out.push(b'\n'); + } + out +} + +fn entries_filtered_by_need(entries: &[FileEntry], need: &[String]) -> Vec { + let need_set: HashSet<&String> = need.iter().collect(); + entries + .iter() + .filter(|e| need_set.contains(&e.path)) + .cloned() + .collect() +} + +// ────────────────── Tests ────────────────── + +#[cfg(test)] +mod tests { + use super::*; + use flate2::read::GzDecoder; + use std::fs; + use std::io::Read; + use std::sync::Arc; + use tempfile::TempDir; + use wiremock::matchers::{header, method, path}; + use wiremock::{Mock, MockServer, Request, Respond, ResponseTemplate}; + + // ─── helpers ─── + + fn make_filter(dir: &Path) -> Arc { + Arc::new(IgnoreFilter::new(dir)) + } + + fn make_config(url: String) -> StreamerConfig { + StreamerConfig { + context_engine_url: url, + user_id: "user-1".into(), + workspace_id: 42, + machine_id: "machine-1".into(), + repo_path: "/repo".into(), + auth_token: String::new(), + } + } + + fn gunzip(data: &[u8]) -> Vec { + let mut decoder = GzDecoder::new(data); + let mut out = Vec::new(); + decoder.read_to_end(&mut out).unwrap(); + out + } + + fn make_entry(path: &str, content: &[u8]) -> FileEntry { + FileEntry { + path: path.into(), + hash: sha256_hex(content), + content: content.to_vec(), + } + } + + // ─── pure-function tests ─── + + #[test] + fn test_walk_and_hash_respects_caps() { + let tmp = TempDir::new().unwrap(); + for i in 0..15 { + fs::write(tmp.path().join(format!("f{i}.txt")), b"x").unwrap(); + } + let filter = IgnoreFilter::new(tmp.path()); + let err = walk_and_hash( + tmp.path(), + &filter, + &CancellationToken::new(), + 1024, + 1_000_000, + 10, // cap at 10 files + ) + .unwrap_err(); + match err { + SyncError::TooManyFiles { max, .. } => assert_eq!(max, 10), + other => panic!("expected TooManyFiles, got {other:?}"), + } + } + + #[test] + fn test_walk_and_hash_respects_byte_cap() { + let tmp = TempDir::new().unwrap(); + // 5 files * 1000 bytes = 5000 bytes total, cap at 2000 + for i in 0..5 { + fs::write(tmp.path().join(format!("f{i}.txt")), vec![b'x'; 1000]).unwrap(); + } + let filter = IgnoreFilter::new(tmp.path()); + let err = walk_and_hash( + tmp.path(), + &filter, + &CancellationToken::new(), + 10_000, + 2_000, + 1000, + ) + .unwrap_err(); + match err { + SyncError::RepoTooLarge { max, .. } => assert_eq!(max, 2_000), + other => panic!("expected RepoTooLarge, got {other:?}"), + } + } + + #[test] + fn test_walk_and_hash_skips_oversized_files() { + let tmp = TempDir::new().unwrap(); + fs::write(tmp.path().join("small.txt"), b"hello").unwrap(); + fs::write(tmp.path().join("big.txt"), vec![b'x'; 5000]).unwrap(); + let filter = IgnoreFilter::new(tmp.path()); + let entries = walk_and_hash( + tmp.path(), + &filter, + &CancellationToken::new(), + 1000, // per-file cap below "big.txt" + 1_000_000, + 1000, + ) + .unwrap(); + let names: Vec<&str> = entries.iter().map(|e| e.path.as_str()).collect(); + assert!(names.contains(&"small.txt")); + assert!(!names.contains(&"big.txt")); + } + + #[test] + fn test_walk_and_hash_respects_ignore_filter() { + let tmp = TempDir::new().unwrap(); + fs::create_dir_all(tmp.path().join("node_modules")).unwrap(); + fs::create_dir_all(tmp.path().join(".git")).unwrap(); + fs::write(tmp.path().join("node_modules/foo.js"), b"x").unwrap(); + fs::write(tmp.path().join(".git/config"), b"x").unwrap(); + fs::write(tmp.path().join("image.png"), b"x").unwrap(); + fs::write(tmp.path().join("good.rs"), b"fn main() {}").unwrap(); + + let filter = IgnoreFilter::new(tmp.path()); + let entries = walk_and_hash( + tmp.path(), + &filter, + &CancellationToken::new(), + 1_000_000, + 1_000_000, + 1000, + ) + .unwrap(); + let names: Vec<&str> = entries.iter().map(|e| e.path.as_str()).collect(); + assert!(names.contains(&"good.rs")); + assert!(!names.iter().any(|n| n.contains("node_modules"))); + assert!(!names.iter().any(|n| n.contains(".git/"))); + assert!(!names.contains(&"image.png")); + } + + #[test] + fn test_walk_and_hash_skips_non_utf8_files() { + let tmp = TempDir::new().unwrap(); + // Latin-1 byte 0xff is invalid UTF-8 on its own. + fs::write(tmp.path().join("bad.txt"), [b'h', b'i', 0xff]).unwrap(); + fs::write(tmp.path().join("good.txt"), b"hello").unwrap(); + let filter = IgnoreFilter::new(tmp.path()); + let entries = walk_and_hash( + tmp.path(), + &filter, + &CancellationToken::new(), + 1_000_000, + 1_000_000, + 1000, + ) + .unwrap(); + let names: Vec<&str> = entries.iter().map(|e| e.path.as_str()).collect(); + assert!(names.contains(&"good.txt")); + assert!(!names.contains(&"bad.txt")); + } + + #[test] + fn test_walk_and_hash_hash_correctness() { + let tmp = TempDir::new().unwrap(); + fs::write(tmp.path().join("a.txt"), b"hello world").unwrap(); + let filter = IgnoreFilter::new(tmp.path()); + let entries = walk_and_hash( + tmp.path(), + &filter, + &CancellationToken::new(), + 1_000_000, + 1_000_000, + 1000, + ) + .unwrap(); + let entry = entries.iter().find(|e| e.path == "a.txt").unwrap(); + // sha256("hello world") known constant + assert_eq!( + entry.hash, + "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9" + ); + } + + #[test] + fn test_deterministic_batch_id_matches_server_format() { + // Pinned digest. Server-side `computeBatchID` (WS4 §5.2) MUST produce the same + // value for the same input. The expected hash here is computed as: + // sorted(["delete|old.rs|", "file|src/a.rs|aaaa"]).join("\n") + // = "delete|old.rs|\nfile|src/a.rs|aaaa" + // sha256 of that → expected below. + let batch = Batch { + files: vec![FileEntry { + path: "src/a.rs".into(), + hash: "aaaa".into(), + content: b"ignored for batch_id".to_vec(), + }], + deletes: vec!["old.rs".into()], + }; + let id = deterministic_batch_id(&batch); + let expected = sha256_hex(b"delete|old.rs|\nfile|src/a.rs|aaaa"); + assert_eq!(id, expected); + } + + #[test] + fn test_deterministic_batch_id_different_hashes_produce_different_ids() { + let b1 = Batch { + files: vec![make_entry("a.rs", b"hello")], + deletes: vec![], + }; + let b2 = Batch { + files: vec![make_entry("a.rs", b"world")], + deletes: vec![], + }; + assert_ne!(deterministic_batch_id(&b1), deterministic_batch_id(&b2)); + } + + #[test] + fn test_deterministic_batch_id_order_independent() { + let b1 = Batch { + files: vec![make_entry("a.rs", b"x"), make_entry("b.rs", b"y")], + deletes: vec!["d1.rs".into(), "d2.rs".into()], + }; + let b2 = Batch { + files: vec![make_entry("b.rs", b"y"), make_entry("a.rs", b"x")], + deletes: vec!["d2.rs".into(), "d1.rs".into()], + }; + assert_eq!(deterministic_batch_id(&b1), deterministic_batch_id(&b2)); + } + + #[test] + fn test_chunk_into_batches_respects_file_cap() { + let entries: Vec = (0..250) + .map(|i| make_entry(&format!("f{i}.rs"), b"x")) + .collect(); + let batches = chunk_into_batches(entries, vec![]); + assert_eq!(batches.len(), 3); + assert_eq!(batches[0].files.len(), 100); + assert_eq!(batches[1].files.len(), 100); + assert_eq!(batches[2].files.len(), 50); + } + + #[test] + fn test_chunk_into_batches_respects_byte_cap() { + let payload = vec![b'x'; 512 * 1024]; // 512 KB each + let entries: Vec = (0..10) + .map(|i| make_entry(&format!("f{i}.rs"), &payload)) + .collect(); + let batches = chunk_into_batches(entries, vec![]); + // 10 × 512 KB = 5 MB; 2 MB cap → 4 files per batch (4×512KB=2MB) → 3 batches + assert!(batches.len() >= 3); + for b in &batches { + let total: usize = b.files.iter().map(|e| e.content.len()).sum(); + assert!(total <= MAX_BATCH_BYTES); + } + } + + #[test] + fn test_chunk_into_batches_places_deletes() { + // With files: deletes attach to the first batch + let entries = vec![make_entry("a.rs", b"x")]; + let deletes = vec!["old.rs".into()]; + let batches = chunk_into_batches(entries, deletes.clone()); + assert_eq!(batches.len(), 1); + assert_eq!(batches[0].deletes, deletes); + + // No files: a delete-only batch is created + let batches = chunk_into_batches(vec![], deletes.clone()); + assert_eq!(batches.len(), 1); + assert!(batches[0].files.is_empty()); + assert_eq!(batches[0].deletes, deletes); + + // No files, no deletes: empty + let batches = chunk_into_batches(vec![], vec![]); + assert!(batches.is_empty()); + } + + #[test] + fn test_gzip_encode_round_trip() { + let payload = b"the quick brown fox jumps over the lazy dog"; + let gz = gzip_encode(payload).unwrap(); + let back = gunzip(&gz); + assert_eq!(back, payload); + } + + #[test] + fn test_build_batch_ndjson_format() { + let batch = Batch { + files: vec![make_entry("a.rs", b"hello")], + deletes: vec!["old.rs".into()], + }; + let bytes = build_batch_ndjson(&batch); + let text = String::from_utf8(bytes).unwrap(); + let lines: Vec<&str> = text.lines().collect(); + assert_eq!(lines.len(), 2); + let line0: serde_json::Value = serde_json::from_str(lines[0]).unwrap(); + assert_eq!(line0["op"], "file"); + assert_eq!(line0["path"], "a.rs"); + assert_eq!(line0["sha256"], sha256_hex(b"hello")); + assert_eq!(line0["content"], BASE64_STANDARD.encode(b"hello")); + let line1: serde_json::Value = serde_json::from_str(lines[1]).unwrap(); + assert_eq!(line1["op"], "delete"); + assert_eq!(line1["path"], "old.rs"); + } + + // ─── mock-server tests ─── + + fn diff_response(sync_id: &str, need: &[&str], delete: &[&str]) -> serde_json::Value { + serde_json::json!({ + "sync_id": sync_id, + "need": need, + "delete": delete, + }) + } + + async fn mount_diff(server: &MockServer, body: serde_json::Value) { + Mock::given(method("POST")) + .and(path("/index/diff")) + .respond_with(ResponseTemplate::new(200).set_body_json(body)) + .mount(server) + .await; + } + + async fn mount_stream_accepted(server: &MockServer, files: usize, deletes: usize, bytes: u64) { + Mock::given(method("POST")) + .and(path("/index/stream")) + .respond_with(ResponseTemplate::new(202).set_body_json(serde_json::json!({ + "queued": { "files": files, "deletes": deletes, "bytes": bytes } + }))) + .mount(server) + .await; + } + + async fn mount_sync_complete_ok(server: &MockServer) { + Mock::given(method("POST")) + .and(path("/index/sync-complete")) + .respond_with(ResponseTemplate::new(200)) + .mount(server) + .await; + } + + async fn write_repo(tmp: &TempDir, files: &[(&str, &[u8])]) { + for (name, content) in files { + let path = tmp.path().join(name); + if let Some(parent) = path.parent() { + fs::create_dir_all(parent).unwrap(); + } + fs::write(path, content).unwrap(); + } + } + + #[tokio::test] + async fn test_full_sync_happy_path() { + let tmp = TempDir::new().unwrap(); + write_repo( + &tmp, + &[ + ("a.rs", b"alpha"), + ("b.rs", b"beta"), + ("c.rs", b"gamma"), + ], + ) + .await; + + let server = MockServer::start().await; + mount_diff( + &server, + diff_response("sync-1", &["a.rs", "b.rs", "c.rs"], &["old.rs"]), + ) + .await; + mount_stream_accepted(&server, 3, 1, 14).await; + mount_sync_complete_ok(&server).await; + + let streamer = Streamer::new_with_timings( + make_config(server.uri()), + Duration::from_secs(5), + Duration::from_millis(50), + ); + let stats = streamer + .full_sync(tmp.path(), make_filter(tmp.path()), CancellationToken::new()) + .await + .unwrap(); + assert_eq!(stats.files_hashed, 3); + assert_eq!(stats.files_uploaded, 3); + assert_eq!(stats.files_deleted, 1); + assert_eq!(stats.bytes_uploaded, 14); + } + + #[tokio::test] + async fn test_full_sync_empty_diff() { + let tmp = TempDir::new().unwrap(); + write_repo(&tmp, &[("a.rs", b"alpha")]).await; + + let server = MockServer::start().await; + mount_diff(&server, diff_response("sync-empty", &[], &[])).await; + // /stream and /sync-complete should NEVER be called. + Mock::given(method("POST")) + .and(path("/index/stream")) + .respond_with(ResponseTemplate::new(500)) + .expect(0) + .mount(&server) + .await; + Mock::given(method("POST")) + .and(path("/index/sync-complete")) + .respond_with(ResponseTemplate::new(500)) + .expect(0) + .mount(&server) + .await; + + let streamer = Streamer::new_with_timings( + make_config(server.uri()), + Duration::from_secs(5), + Duration::from_millis(50), + ); + let stats = streamer + .full_sync(tmp.path(), make_filter(tmp.path()), CancellationToken::new()) + .await + .unwrap(); + assert_eq!(stats.files_uploaded, 0); + assert_eq!(stats.files_deleted, 0); + } + + #[tokio::test] + async fn test_full_sync_410_restarts() { + let tmp = TempDir::new().unwrap(); + write_repo(&tmp, &[("a.rs", b"alpha")]).await; + + let server = MockServer::start().await; + mount_diff(&server, diff_response("sync-410", &["a.rs"], &[])).await; + Mock::given(method("POST")) + .and(path("/index/stream")) + .respond_with(ResponseTemplate::new(410).set_body_string("expired")) + .mount(&server) + .await; + + let streamer = Streamer::new_with_timings( + make_config(server.uri()), + Duration::from_secs(5), + Duration::from_millis(50), + ); + let err = streamer + .full_sync(tmp.path(), make_filter(tmp.path()), CancellationToken::new()) + .await + .unwrap_err(); + assert!(matches!(err, SyncError::SyncExpired { .. })); + } + + #[tokio::test] + async fn test_full_sync_sync_complete_timeout_treated_as_success() { + // After stream_batches succeeds, the data is already on the server. + // If /sync-complete polling exceeds the deadline, we treat it as a + // warning (not an error) so the badge doesn't go red for a healthy + // index. See watcher_manager plan item: "server-success / client-error + // asymmetry". + let tmp = TempDir::new().unwrap(); + write_repo(&tmp, &[("a.rs", b"alpha")]).await; + + let server = MockServer::start().await; + mount_diff(&server, diff_response("sync-timeout", &["a.rs"], &[])).await; + mount_stream_accepted(&server, 1, 0, 5).await; + Mock::given(method("POST")) + .and(path("/index/sync-complete")) + .respond_with(ResponseTemplate::new(202)) + .mount(&server) + .await; + + let streamer = Streamer::new_with_timings( + make_config(server.uri()), + Duration::from_millis(200), + Duration::from_millis(50), + ); + let stats = streamer + .full_sync(tmp.path(), make_filter(tmp.path()), CancellationToken::new()) + .await + .expect("deadline on /sync-complete should be treated as success"); + assert_eq!(stats.files_uploaded, 1); + } + + #[tokio::test] + async fn test_full_sync_stream_400_bad_request() { + let tmp = TempDir::new().unwrap(); + write_repo(&tmp, &[("a.rs", b"alpha")]).await; + + let server = MockServer::start().await; + mount_diff(&server, diff_response("sync-400", &["a.rs"], &[])).await; + Mock::given(method("POST")) + .and(path("/index/stream")) + .respond_with(ResponseTemplate::new(400).set_body_json(serde_json::json!({ + "error": "sha256_mismatch", + "details": "client hash does not match content" + }))) + .mount(&server) + .await; + + let streamer = Streamer::new_with_timings( + make_config(server.uri()), + Duration::from_secs(5), + Duration::from_millis(50), + ); + let err = streamer + .full_sync(tmp.path(), make_filter(tmp.path()), CancellationToken::new()) + .await + .unwrap_err(); + match err { + SyncError::BadRequest { reason, details } => { + assert_eq!(reason, "sha256_mismatch"); + assert_eq!(details, "client hash does not match content"); + } + other => panic!("expected BadRequest, got {other:?}"), + } + } + + #[tokio::test] + async fn test_full_sync_duplicate_batch() { + let tmp = TempDir::new().unwrap(); + write_repo(&tmp, &[("a.rs", b"alpha")]).await; + + let server = MockServer::start().await; + mount_diff(&server, diff_response("sync-dup", &["a.rs"], &[])).await; + Mock::given(method("POST")) + .and(path("/index/stream")) + .respond_with( + ResponseTemplate::new(200).set_body_json(serde_json::json!({ "duplicate": true })), + ) + .mount(&server) + .await; + mount_sync_complete_ok(&server).await; + + let streamer = Streamer::new_with_timings( + make_config(server.uri()), + Duration::from_secs(5), + Duration::from_millis(50), + ); + let stats = streamer + .full_sync(tmp.path(), make_filter(tmp.path()), CancellationToken::new()) + .await + .unwrap(); + assert_eq!(stats.duplicate_batches, 1); + assert_eq!(stats.files_uploaded, 0); + } + + /// State machine that returns 503 twice then 202. + struct FlakyStream { + count: std::sync::Mutex, + } + + impl Respond for FlakyStream { + fn respond(&self, _req: &Request) -> ResponseTemplate { + let mut c = self.count.lock().unwrap(); + *c += 1; + if *c <= 2 { + ResponseTemplate::new(503) + } else { + ResponseTemplate::new(202).set_body_json(serde_json::json!({ + "queued": { "files": 1, "deletes": 0, "bytes": 5 } + })) + } + } + } + + #[tokio::test] + async fn test_full_sync_retry_on_transient_error() { + let tmp = TempDir::new().unwrap(); + write_repo(&tmp, &[("a.rs", b"alpha")]).await; + + let server = MockServer::start().await; + mount_diff(&server, diff_response("sync-retry", &["a.rs"], &[])).await; + Mock::given(method("POST")) + .and(path("/index/stream")) + .respond_with(FlakyStream { + count: std::sync::Mutex::new(0), + }) + .mount(&server) + .await; + mount_sync_complete_ok(&server).await; + + let streamer = Streamer::new_with_timings( + make_config(server.uri()), + Duration::from_secs(5), + Duration::from_millis(50), + ); + let stats = streamer + .full_sync(tmp.path(), make_filter(tmp.path()), CancellationToken::new()) + .await + .unwrap(); + assert_eq!(stats.files_uploaded, 1); + } + + /// Capture inbound /diff bodies for assertion. + #[derive(Clone)] + struct CaptureBody { + inner: Arc>>, + status: u16, + body: serde_json::Value, + } + + impl Respond for CaptureBody { + fn respond(&self, req: &Request) -> ResponseTemplate { + *self.inner.lock().unwrap() = req.body.clone(); + ResponseTemplate::new(self.status).set_body_json(self.body.clone()) + } + } + + #[tokio::test] + async fn test_full_sync_diff_request_serialization() { + let tmp = TempDir::new().unwrap(); + write_repo(&tmp, &[("a.rs", b"alpha")]).await; + + let server = MockServer::start().await; + let captured = Arc::new(std::sync::Mutex::new(Vec::new())); + Mock::given(method("POST")) + .and(path("/index/diff")) + .and(header("content-encoding", "gzip")) + .respond_with(CaptureBody { + inner: captured.clone(), + status: 200, + body: diff_response("sync-x", &[], &[]), + }) + .mount(&server) + .await; + + let streamer = Streamer::new_with_timings( + make_config(server.uri()), + Duration::from_secs(5), + Duration::from_millis(50), + ); + streamer + .full_sync(tmp.path(), make_filter(tmp.path()), CancellationToken::new()) + .await + .unwrap(); + + let raw = captured.lock().unwrap().clone(); + let decoded = gunzip(&raw); + let json: serde_json::Value = serde_json::from_slice(&decoded).unwrap(); + // Identity lives in the body (WS3 contract). + assert_eq!(json["user_id"], "user-1"); + assert_eq!(json["workspace_id"], 42); + assert_eq!(json["machine_id"], "machine-1"); + assert_eq!(json["repo_path"], "/repo"); + // Default-omit for full sync. + assert!(json.get("incremental").is_none(), "full sync must not serialize 'incremental'"); + assert!(json.get("deletes").is_none(), "full sync must not serialize 'deletes'"); + assert_eq!(json["files"][0]["path"], "a.rs"); + } + + // ─── incremental tests ─── + + #[tokio::test] + async fn test_incremental_sync_happy_path() { + let tmp = TempDir::new().unwrap(); + write_repo(&tmp, &[("a.rs", b"new content")]).await; + + let server = MockServer::start().await; + let captured = Arc::new(std::sync::Mutex::new(Vec::new())); + Mock::given(method("POST")) + .and(path("/index/diff")) + .respond_with(CaptureBody { + inner: captured.clone(), + status: 200, + body: diff_response("sync-inc", &["a.rs"], &["old.rs"]), + }) + .mount(&server) + .await; + mount_stream_accepted(&server, 1, 1, 11).await; + mount_sync_complete_ok(&server).await; + + let streamer = Streamer::new_with_timings( + make_config(server.uri()), + Duration::from_secs(5), + Duration::from_millis(50), + ); + let stats = streamer + .incremental_sync( + &[tmp.path().join("a.rs")], + &[tmp.path().join("old.rs")], + tmp.path(), + CancellationToken::new(), + ) + .await + .unwrap(); + assert_eq!(stats.files_hashed, 1); + assert_eq!(stats.files_uploaded, 1); + assert_eq!(stats.files_deleted, 1); + + let raw = captured.lock().unwrap().clone(); + let json: serde_json::Value = serde_json::from_slice(&gunzip(&raw)).unwrap(); + assert_eq!(json["user_id"], "user-1"); + assert_eq!(json["workspace_id"], 42); + assert_eq!(json["incremental"], true); + assert_eq!(json["deletes"], serde_json::json!(["old.rs"])); + assert_eq!(json["files"][0]["path"], "a.rs"); + } + + #[tokio::test] + async fn test_incremental_sync_pure_delete_event() { + let tmp = TempDir::new().unwrap(); + + let server = MockServer::start().await; + let captured = Arc::new(std::sync::Mutex::new(Vec::new())); + Mock::given(method("POST")) + .and(path("/index/diff")) + .respond_with(CaptureBody { + inner: captured.clone(), + status: 200, + body: diff_response("sync-pure-del", &[], &["old.rs"]), + }) + .mount(&server) + .await; + mount_stream_accepted(&server, 0, 1, 0).await; + mount_sync_complete_ok(&server).await; + + let streamer = Streamer::new_with_timings( + make_config(server.uri()), + Duration::from_secs(5), + Duration::from_millis(50), + ); + let stats = streamer + .incremental_sync( + &[], + &[tmp.path().join("old.rs")], + tmp.path(), + CancellationToken::new(), + ) + .await + .unwrap(); + assert_eq!(stats.files_hashed, 0); + assert_eq!(stats.files_uploaded, 0); + assert_eq!(stats.files_deleted, 1); + + let raw = captured.lock().unwrap().clone(); + let json: serde_json::Value = serde_json::from_slice(&gunzip(&raw)).unwrap(); + assert_eq!(json["user_id"], "user-1"); + assert_eq!(json["workspace_id"], 42); + assert_eq!(json["incremental"], true); + assert_eq!(json["files"], serde_json::json!([])); + assert_eq!(json["deletes"], serde_json::json!(["old.rs"])); + } + + #[tokio::test] + async fn test_incremental_sync_noop_unchanged_hash() { + let tmp = TempDir::new().unwrap(); + write_repo(&tmp, &[("a.rs", b"unchanged")]).await; + + let server = MockServer::start().await; + mount_diff(&server, diff_response("sync-noop", &[], &[])).await; + // /stream and /sync-complete should NEVER be called + Mock::given(method("POST")) + .and(path("/index/stream")) + .respond_with(ResponseTemplate::new(500)) + .expect(0) + .mount(&server) + .await; + Mock::given(method("POST")) + .and(path("/index/sync-complete")) + .respond_with(ResponseTemplate::new(500)) + .expect(0) + .mount(&server) + .await; + + let streamer = Streamer::new_with_timings( + make_config(server.uri()), + Duration::from_secs(5), + Duration::from_millis(50), + ); + let stats = streamer + .incremental_sync( + &[tmp.path().join("a.rs")], + &[], + tmp.path(), + CancellationToken::new(), + ) + .await + .unwrap(); + assert_eq!(stats.files_hashed, 1); + assert_eq!(stats.files_uploaded, 0); + } + + #[tokio::test] + async fn test_incremental_sync_does_not_request_full_walk() { + // Repo has many files but incremental_sync is told only one of them changed. + // Verify only the named file is read (we don't actually walk the tree). + let tmp = TempDir::new().unwrap(); + write_repo( + &tmp, + &[ + ("a.rs", b"a"), + ("b.rs", b"b"), + ("c.rs", b"c"), + ("d.rs", b"d"), + ], + ) + .await; + + let server = MockServer::start().await; + let captured = Arc::new(std::sync::Mutex::new(Vec::new())); + Mock::given(method("POST")) + .and(path("/index/diff")) + .respond_with(CaptureBody { + inner: captured.clone(), + status: 200, + body: diff_response("sync-only-one", &[], &[]), + }) + .mount(&server) + .await; + + let streamer = Streamer::new_with_timings( + make_config(server.uri()), + Duration::from_secs(5), + Duration::from_millis(50), + ); + streamer + .incremental_sync( + &[tmp.path().join("b.rs")], + &[], + tmp.path(), + CancellationToken::new(), + ) + .await + .unwrap(); + + let raw = captured.lock().unwrap().clone(); + let json: serde_json::Value = serde_json::from_slice(&gunzip(&raw)).unwrap(); + let files = json["files"].as_array().unwrap(); + assert_eq!(files.len(), 1); + assert_eq!(files[0]["path"], "b.rs"); + } + + // ─── PR-2 follow-ups: C1 abort + C4 fail-fast 4xx ─── + + /// Mock /stream responder: returns 400 on the very first request and a + /// long-delayed 202 on every subsequent request. Simulates the scenario + /// where one batch fails fast while sibling batches are still in-flight, + /// exercising the abort_and_drain path in stream_batches (C1). + struct FirstFailsOthersSlow { + count: std::sync::Mutex, + delay: Duration, + } + + impl Respond for FirstFailsOthersSlow { + fn respond(&self, _req: &Request) -> ResponseTemplate { + let mut c = self.count.lock().unwrap(); + *c += 1; + if *c == 1 { + ResponseTemplate::new(400).set_body_json(serde_json::json!({ + "error": "forced_fail", + "details": "first batch intentionally fails" + })) + } else { + ResponseTemplate::new(202) + .set_delay(self.delay) + .set_body_json(serde_json::json!({ + "queued": { "files": 1, "deletes": 0, "bytes": 1 } + })) + } + } + } + + /// C1 — when one batch fails, the remaining in-flight batches must be + /// aborted instead of being detached and running against a dead sync_id. + /// We assert this by timing: the sibling batches are mocked with a 10s + /// delay; without abort the test would block for ~10s, with abort it + /// returns almost immediately. + #[tokio::test] + async fn test_stream_batches_aborts_siblings_on_error() { + use std::time::Instant; + + let tmp = TempDir::new().unwrap(); + // Write 150 tiny files → 2 batches (MAX_BATCH_FILES=100) so there's + // at least one sibling in-flight when the first response arrives. + let mut files: Vec<(String, Vec)> = Vec::new(); + for i in 0..150 { + files.push((format!("f{i:03}.rs"), b"x".to_vec())); + } + for (name, content) in &files { + fs::write(tmp.path().join(name), content).unwrap(); + } + + let server = MockServer::start().await; + let need: Vec = files.iter().map(|(n, _)| n.clone()).collect(); + let need_refs: Vec<&str> = need.iter().map(|s| s.as_str()).collect(); + mount_diff(&server, diff_response("sync-abort", &need_refs, &[])).await; + Mock::given(method("POST")) + .and(path("/index/stream")) + .respond_with(FirstFailsOthersSlow { + count: std::sync::Mutex::new(0), + delay: Duration::from_secs(10), + }) + .mount(&server) + .await; + + let streamer = Streamer::new_with_timings( + make_config(server.uri()), + Duration::from_secs(5), + Duration::from_millis(50), + ); + let started = Instant::now(); + let err = streamer + .full_sync(tmp.path(), make_filter(tmp.path()), CancellationToken::new()) + .await + .unwrap_err(); + let elapsed = started.elapsed(); + + assert!( + matches!(err, SyncError::BadRequest { .. }), + "expected BadRequest, got {err:?}" + ); + // Sibling delay is 10s. If abort is working, we return in well under + // that. 4s is a generous ceiling for CI variance. + assert!( + elapsed < Duration::from_secs(4), + "elapsed {elapsed:?} — siblings not aborted (C1 regression)" + ); + } + + /// C4 helper: single-file repo + /stream returning the given status. + /// Verifies we return `SyncError::AuthRequired` and the mock received + /// exactly one request (no retries). + async fn assert_stream_fails_fast(status: u16) { + let tmp = TempDir::new().unwrap(); + write_repo(&tmp, &[("a.rs", b"alpha")]).await; + + let server = MockServer::start().await; + mount_diff( + &server, + diff_response(&format!("sync-{status}"), &["a.rs"], &[]), + ) + .await; + Mock::given(method("POST")) + .and(path("/index/stream")) + .respond_with(ResponseTemplate::new(status)) + .expect(1) // no retries + .mount(&server) + .await; + + let streamer = Streamer::new_with_timings( + make_config(server.uri()), + Duration::from_secs(5), + Duration::from_millis(50), + ); + let err = streamer + .full_sync(tmp.path(), make_filter(tmp.path()), CancellationToken::new()) + .await + .unwrap_err(); + match err { + SyncError::AuthRequired { status: s } => assert_eq!(s, status), + other => panic!("expected AuthRequired({status}), got {other:?}"), + } + } + + #[tokio::test] + async fn test_stream_one_batch_fails_fast_on_401() { + assert_stream_fails_fast(401).await; + } + + #[tokio::test] + async fn test_stream_one_batch_fails_fast_on_403() { + assert_stream_fails_fast(403).await; + } + + #[tokio::test] + async fn test_stream_one_batch_fails_fast_on_404() { + assert_stream_fails_fast(404).await; + } + + /// Regression guard: 429 Too Many Requests is 4xx but canonically + /// retriable. Must not fall into the AuthRequired fail-fast arm. + struct Flaky429 { + count: std::sync::Mutex, + } + + impl Respond for Flaky429 { + fn respond(&self, _req: &Request) -> ResponseTemplate { + let mut c = self.count.lock().unwrap(); + *c += 1; + if *c <= 2 { + ResponseTemplate::new(429) + } else { + ResponseTemplate::new(202).set_body_json(serde_json::json!({ + "queued": { "files": 1, "deletes": 0, "bytes": 5 } + })) + } + } + } + + #[tokio::test] + async fn test_stream_one_batch_still_retries_429() { + let tmp = TempDir::new().unwrap(); + write_repo(&tmp, &[("a.rs", b"alpha")]).await; + + let server = MockServer::start().await; + mount_diff(&server, diff_response("sync-429", &["a.rs"], &[])).await; + Mock::given(method("POST")) + .and(path("/index/stream")) + .respond_with(Flaky429 { + count: std::sync::Mutex::new(0), + }) + .mount(&server) + .await; + mount_sync_complete_ok(&server).await; + + let streamer = Streamer::new_with_timings( + make_config(server.uri()), + Duration::from_secs(5), + Duration::from_millis(50), + ); + let stats = streamer + .full_sync(tmp.path(), make_filter(tmp.path()), CancellationToken::new()) + .await + .unwrap(); + assert_eq!(stats.files_uploaded, 1); + } + + /// C4 — /diff 401 returns AuthRequired (replaces old string-based Other). + #[tokio::test] + async fn test_diff_fails_fast_on_401() { + let tmp = TempDir::new().unwrap(); + write_repo(&tmp, &[("a.rs", b"alpha")]).await; + + let server = MockServer::start().await; + Mock::given(method("POST")) + .and(path("/index/diff")) + .respond_with(ResponseTemplate::new(401)) + .expect(1) + .mount(&server) + .await; + + let streamer = Streamer::new_with_timings( + make_config(server.uri()), + Duration::from_secs(5), + Duration::from_millis(50), + ); + let err = streamer + .full_sync(tmp.path(), make_filter(tmp.path()), CancellationToken::new()) + .await + .unwrap_err(); + assert!( + matches!(err, SyncError::AuthRequired { status: 401 }), + "expected AuthRequired(401), got {err:?}" + ); + } + + // ─── 1.3.28 fix regression tests ─── + + #[tokio::test] + async fn test_sync_complete_410_treated_as_success() { + // /sync-complete returning 410 means the server already finalized and + // GC'd the sync_id record — data IS indexed. Previously surfaced as + // SyncError::SyncExpired → badge red. Now treated as Ok. + let tmp = TempDir::new().unwrap(); + write_repo(&tmp, &[("a.rs", b"alpha")]).await; + + let server = MockServer::start().await; + mount_diff(&server, diff_response("sync-sc-410", &["a.rs"], &[])).await; + mount_stream_accepted(&server, 1, 0, 5).await; + Mock::given(method("POST")) + .and(path("/index/sync-complete")) + .respond_with(ResponseTemplate::new(410).set_body_string("already finalized")) + .mount(&server) + .await; + + let streamer = Streamer::new_with_timings( + make_config(server.uri()), + Duration::from_secs(5), + Duration::from_millis(50), + ); + let stats = streamer + .full_sync(tmp.path(), make_filter(tmp.path()), CancellationToken::new()) + .await + .expect("410 on /sync-complete should be a success"); + assert_eq!(stats.files_uploaded, 1); + } + + #[tokio::test] + async fn test_call_diff_retries_on_429_then_succeeds() { + // First 2 /diff calls return 429 (concurrent sync), 3rd returns 200. + // With 3 retries and exponential backoff starting at 2s, we use very + // short-lived mocks via up_to_n_times. + let tmp = TempDir::new().unwrap(); + write_repo(&tmp, &[("a.rs", b"alpha")]).await; + + let server = MockServer::start().await; + // First response: 429 + Mock::given(method("POST")) + .and(path("/index/diff")) + .respond_with(ResponseTemplate::new(429).set_body_string("busy")) + .up_to_n_times(1) + .mount(&server) + .await; + // Subsequent responses: 200 with normal diff body + mount_diff(&server, diff_response("retry-ok", &[], &[])).await; + + let streamer = Streamer::new_with_timings( + make_config(server.uri()), + Duration::from_secs(30), + Duration::from_millis(50), + ); + // full_sync should succeed after the retry (empty need/delete → no stream/poll) + let stats = streamer + .full_sync(tmp.path(), make_filter(tmp.path()), CancellationToken::new()) + .await + .expect("should retry past 429 and succeed"); + assert_eq!(stats.files_hashed, 1); + } + + #[tokio::test] + async fn test_incremental_sync_cancellation_returns_cancelled_error() { + // Cancellation must surface as SyncError::Cancelled, not as a generic + // error. The watcher_manager layer then maps Cancelled to Indexed + // (not Error) so the badge doesn't flash red on repo switch. + let tmp = TempDir::new().unwrap(); + write_repo(&tmp, &[("a.rs", b"alpha")]).await; + + // Cancel immediately — incremental_sync checks at the top of its loop + // before the first fs::read. + let cancel = CancellationToken::new(); + cancel.cancel(); + + let server = MockServer::start().await; + let streamer = Streamer::new(make_config(server.uri())); + let changed = vec![tmp.path().join("a.rs")]; + let err = streamer + .incremental_sync(&changed, &[], tmp.path(), cancel) + .await + .unwrap_err(); + assert!( + matches!(err, SyncError::Cancelled), + "expected Cancelled, got {err:?}" + ); + } +} diff --git a/apps/desktop/src-tauri/src/context_watcher/watcher_manager.rs b/apps/desktop/src-tauri/src/context_watcher/watcher_manager.rs new file mode 100644 index 00000000..4e61d915 --- /dev/null +++ b/apps/desktop/src-tauri/src/context_watcher/watcher_manager.rs @@ -0,0 +1,554 @@ +use serde::Serialize; +use std::collections::HashMap; +use std::path::{Path, PathBuf}; +use std::sync::Arc; +use std::time::Duration; +use tauri::Emitter; +use tokio::sync::{Mutex, RwLock, Semaphore}; + +use tokio::task::{JoinHandle, JoinSet}; +use tokio_util::sync::CancellationToken; + +use super::file_watcher::{ChangeBatch, FileWatcher}; +use super::ignore_filter::IgnoreFilter; +use super::streamer::{Streamer, StreamerConfig, SyncError}; + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +#[derive(Debug, Clone, Serialize)] +#[serde(tag = "status")] +pub enum IndexWatcherStatus { + #[serde(rename = "not_indexed")] + NotIndexed, + #[serde(rename = "indexing")] + Indexing, + #[serde(rename = "indexed")] + Indexed { file_count: Option }, + #[serde(rename = "error")] + Error { reason: String, details: String }, +} + +struct RepoWatcher { + cancel_token: CancellationToken, + sync_task: JoinHandle<()>, + _file_watcher: FileWatcher, // kept alive to hold the notify::RecommendedWatcher + status: IndexWatcherStatus, +} + +// --------------------------------------------------------------------------- +// WatcherManager +// --------------------------------------------------------------------------- + +pub struct WatcherManager { + watchers: Arc>>, + /// Stable per-machine id — the SAME value `commands::machine_id` resolves, + /// so the watcher's collection key matches the search client's. + machine_id: String, + /// Shared settings DB (`supercoder.db`). Holds the `watched_repos` table and + /// the `context_engine` settings the streamer reads live. + db: Arc, + app_handle: tauri::AppHandle, + /// Per-repo serialization mutex. Ensures only one full_sync or + /// incremental_sync runs per repo_path at a time. + sync_locks: Arc>>>>, +} + +impl WatcherManager { + /// Create a watcher manager. `machine_id` must be the same value the search + /// client uses (`commands::machine_id`) so sync and queries hit the same + /// collection. `db` is the shared settings DB. + pub fn new( + machine_id: String, + db: Arc, + app_handle: tauri::AppHandle, + ) -> Self { + Self { + watchers: Arc::new(RwLock::new(HashMap::new())), + machine_id, + db, + app_handle, + sync_locks: Arc::new(RwLock::new(HashMap::new())), + } + } + + /// Get (or create) the per-repo sync mutex. + async fn get_sync_lock(&self, repo_path: &str) -> Arc> { + let mut locks = self.sync_locks.write().await; + locks + .entry(repo_path.to_string()) + .or_insert_with(|| Arc::new(Mutex::new(()))) + .clone() + } + + /// Build a `Streamer` for the given repo. Reads the context-engine settings + /// live from the DB; returns `None` when the feature is disabled or the base + /// URL is empty (a stale watcher then no-ops). + async fn build_streamer(&self, repo_path: &str) -> Option { + let settings = crate::agent_bridge::commands::read_context_engine_db(&self.db); + if !settings.enabled { + return None; + } + let base = crate::agent_bridge::commands::normalize_base_url(&settings.base_url); + if base.is_empty() { + return None; + } + Some(Streamer::new(StreamerConfig { + // chat-desktop ran behind an APISIX gateway that stripped `/api/v1`; + // the OSS server serves the index routes under `/api/v1`, so we append + // it here and leave streamer.rs' `{url}/index/...` construction intact. + context_engine_url: format!("{base}/api/v1"), + // Local single-user identity — no accounts, no auth gateway. + user_id: "local".to_string(), + workspace_id: 0, + machine_id: self.machine_id.clone(), + repo_path: repo_path.to_string(), + auth_token: String::new(), + })) + } + + /// Start watching a repo directory. Always runs a fresh `full_sync` first + /// (plan §3.8 — catches offline edits). + pub async fn start_watching(self: &Arc, repo_path: &str) -> Result<(), String> { + if !crate::agent_bridge::commands::read_context_engine_db(&self.db).enabled { + return Ok(()); + } + + let repo = PathBuf::from(repo_path); + if !repo.is_dir() { + return Err(format!("Not a directory: {repo_path}")); + } + + // If already watching, bump DB timestamp and run a fresh full_sync + // to catch offline edits (plan §3.8, decision #11). We reuse the + // existing cancel_token so stop_watching can interrupt this sync. + let existing_cancel = { + let watchers = self.watchers.read().await; + watchers.get(repo_path).map(|w| w.cancel_token.clone()) + }; + if let Some(cancel) = existing_cancel { + self.upsert_repo_db(repo_path); + let filter = Arc::new(IgnoreFilter::new(&repo)); + let status = match self.run_full_sync(repo_path, filter, cancel).await { + Ok(s) => s, + Err(e) => { + log::warn!( + "[ContextWatcher] Re-watch full_sync failed for {repo_path}: {e}" + ); + IndexWatcherStatus::Error { + reason: "sync_failed".to_string(), + details: e, + } + } + }; + self.update_cached_status(repo_path, status).await; + return Ok(()); + } + + let filter = Arc::new(IgnoreFilter::new(&repo)); + let cancel_token = CancellationToken::new(); + + // Run the initial full_sync inline so the caller (and UI) sees the + // indexing→indexed transition before the watcher starts. + let initial_status = match self + .run_full_sync(repo_path, Arc::clone(&filter), cancel_token.clone()) + .await + { + Ok(status) => status, + Err(e) => { + log::warn!("[ContextWatcher] Initial full_sync failed for {repo_path}: {e}"); + // Continue anyway — the file watcher will pick up incremental changes + // and keep retrying. Reflect the error in the cached status. + IndexWatcherStatus::Error { + reason: "sync_failed".to_string(), + details: e, + } + } + }; + + // Start the file watcher + let (watcher, batch_rx) = + FileWatcher::start(repo.clone(), Arc::clone(&filter), cancel_token.clone()) + .map_err(|e| e.to_string())?; + + // Spawn sync worker with an Arc clone of self + let me = Arc::clone(self); + let repo_path_owned = repo_path.to_string(); + let cancel_clone = cancel_token.clone(); + let sync_task = tokio::spawn(async move { + sync_worker(me, repo_path_owned, batch_rx, cancel_clone).await; + }); + + // Store watcher (FileWatcher must be kept alive to hold notify::RecommendedWatcher) + { + let mut watchers = self.watchers.write().await; + watchers.insert( + repo_path.to_string(), + RepoWatcher { + cancel_token, + sync_task, + _file_watcher: watcher, + status: initial_status, + }, + ); + } + + self.upsert_repo_db(repo_path); + + log::info!("[ContextWatcher] Now watching {repo_path}"); + Ok(()) + } + + /// Run a full sync against the context engine. Emits status events and + /// returns the terminal `IndexWatcherStatus` for caching. + async fn run_full_sync( + &self, + repo_path: &str, + filter: Arc, + cancel: CancellationToken, + ) -> Result { + let streamer = match self.build_streamer(repo_path).await { + Some(s) => s, + None => return Err("streamer not available (context engine disabled)".into()), + }; + + let lock = self.get_sync_lock(repo_path).await; + let _guard = lock.lock().await; + + self.emit_status(repo_path, &IndexWatcherStatus::Indexing); + + let repo_root = Path::new(repo_path); + match streamer.full_sync(repo_root, filter, cancel).await { + Ok(stats) => { + log::info!( + "[ContextWatcher] full_sync done repo={} hashed={} uploaded={} deleted={} bytes={} duration={:?}", + repo_path, + stats.files_hashed, + stats.files_uploaded, + stats.files_deleted, + stats.bytes_uploaded, + stats.duration, + ); + let status = IndexWatcherStatus::Indexed { + file_count: Some(stats.files_hashed as u64), + }; + self.emit_status(repo_path, &status); + Ok(status) + } + Err(SyncError::Cancelled) => { + // Cancellation is expected during repo-switch / shutdown — not an error. + // Reset to NotIndexed so the next start_watching retries cleanly, and + // emit it to clear any "Indexing..." spinner the UI may have cached. + log::info!("[ContextWatcher] full_sync cancelled for {repo_path}"); + let status = IndexWatcherStatus::NotIndexed; + self.emit_status(repo_path, &status); + Ok(status) + } + Err(e) => { + let (reason, details) = classify_sync_error(&e); + log::warn!( + "[ContextWatcher] full_sync failed for {}: {} ({})", + repo_path, + reason, + details + ); + let status = IndexWatcherStatus::Error { + reason: reason.clone(), + details: details.clone(), + }; + self.emit_status(repo_path, &status); + Err(format!("{reason}: {details}")) + } + } + } + + /// Run an incremental sync triggered by a file-watcher batch. Emits status + /// events and returns the terminal `IndexWatcherStatus`. + async fn run_incremental_sync( + &self, + repo_path: &str, + changed: &[PathBuf], + deleted: &[PathBuf], + cancel: CancellationToken, + ) -> IndexWatcherStatus { + let streamer = match self.build_streamer(repo_path).await { + Some(s) => s, + None => { + return IndexWatcherStatus::Error { + reason: "unavailable".into(), + details: "streamer not configured".into(), + } + } + }; + + let lock = self.get_sync_lock(repo_path).await; + let _guard = lock.lock().await; + + self.emit_status(repo_path, &IndexWatcherStatus::Indexing); + + let repo_root = Path::new(repo_path); + match streamer + .incremental_sync(changed, deleted, repo_root, cancel) + .await + { + Ok(stats) => { + log::info!( + "[ContextWatcher] incremental_sync done repo={} uploaded={} deleted={}", + repo_path, + stats.files_uploaded, + stats.files_deleted + ); + let status = IndexWatcherStatus::Indexed { file_count: None }; + self.emit_status(repo_path, &status); + status + } + Err(SyncError::Cancelled) => { + // Cancellation is expected during repo-switch / shutdown. Roll back + // to Indexed (file_count=None) so the UI clears the "Indexing..." spinner + // and reflects that the server's index is still live from the last sync. + log::info!("[ContextWatcher] incremental_sync cancelled for {repo_path}"); + let status = IndexWatcherStatus::Indexed { file_count: None }; + self.emit_status(repo_path, &status); + status + } + Err(e) => { + let (reason, details) = classify_sync_error(&e); + log::warn!( + "[ContextWatcher] incremental_sync failed for {}: {} ({})", + repo_path, + reason, + details + ); + let status = IndexWatcherStatus::Error { reason, details }; + self.emit_status(repo_path, &status); + status + } + } + } + + /// Stop watching a repo. Cancels gracefully and waits up to 5s for + /// in-flight uploads before hard-aborting the sync task. Also GCs the + /// per-repo sync_locks entry (see C5). + pub async fn stop_watching(&self, repo_path: &str) { + let watcher = { + let mut watchers = self.watchers.write().await; + watchers.remove(repo_path) + }; + if let Some(w) = watcher { + shutdown_watcher(w, Duration::from_secs(5), repo_path).await; + } + // Unconditionally GC the lock map entry — even if no watcher was + // registered, `get_sync_lock` may have populated it. + self.sync_locks.write().await.remove(repo_path); + } + + /// Stop all watchers (app quit). Shutdowns run concurrently with a 1s + /// grace period per watcher, so total quit time stays bounded regardless + /// of repo count. + pub async fn stop_all(&self) { + let drained: Vec<(String, RepoWatcher)> = { + let mut watchers = self.watchers.write().await; + watchers.drain().collect() + }; + if drained.is_empty() { + return; + } + let mut set: JoinSet<()> = JoinSet::new(); + for (path, w) in drained { + set.spawn(async move { + shutdown_watcher(w, Duration::from_secs(1), &path).await; + }); + } + while set.join_next().await.is_some() {} + // Bulk GC all sync_lock entries — stop_all drains every watcher. + self.sync_locks.write().await.clear(); + } + + /// Get cached status for a repo. + pub async fn get_status(&self, repo_path: &str) -> Option { + let watchers = self.watchers.read().await; + watchers.get(repo_path).map(|w| w.status.clone()) + } + + /// Auto-start watchers for recently-active repos on app launch. + /// Skips if identity/auth token are not yet set (pre-login) — will be called + /// again after login via `save_auth_credentials`. + pub async fn auto_start(self: &Arc) { + if !crate::agent_bridge::commands::read_context_engine_db(&self.db).enabled { + return; + } + + // Cleanup stale repos + { + let conn = self.db.conn.lock(); + let _ = super::db::cleanup_stale_repos(&conn); + } + + // Get active repos + let repos = { + let conn = self.db.conn.lock(); + super::db::get_active_watched_repos(&conn).unwrap_or_default() + }; + + const AUTO_START_CONCURRENCY: usize = 4; + let sem = Arc::new(Semaphore::new(AUTO_START_CONCURRENCY)); + let mut set = JoinSet::new(); + + for repo_path in repos { + if !Path::new(&repo_path).is_dir() { + log::info!("[ContextWatcher] Skipping missing repo: {repo_path}"); + continue; + } + let mgr = Arc::clone(self); + let permit = Arc::clone(&sem); + set.spawn(async move { + let _permit = permit.acquire().await.expect("semaphore closed"); + if let Err(e) = mgr.start_watching(&repo_path).await { + log::warn!("[ContextWatcher] Failed to auto-start for {repo_path}: {e}"); + } + }); + } + + while set.join_next().await.is_some() {} + } + + // ----------------------------------------------------------------------- + // Internal helpers + // ----------------------------------------------------------------------- + + fn emit_status(&self, repo_path: &str, status: &IndexWatcherStatus) { + #[derive(Serialize, Clone)] + struct StatusEvent { + repo_path: String, + #[serde(flatten)] + status: IndexWatcherStatus, + } + + let _ = self.app_handle.emit( + "context-watcher-status", + StatusEvent { + repo_path: repo_path.to_string(), + status: status.clone(), + }, + ); + } + + fn upsert_repo_db(&self, repo_path: &str) { + let conn = self.db.conn.lock(); + let _ = super::db::upsert_watched_repo(&conn, repo_path); + } + + /// Update the cached status for a repo. No-op if the repo isn't being watched. + async fn update_cached_status(&self, repo_path: &str, status: IndexWatcherStatus) { + let mut watchers = self.watchers.write().await; + if let Some(w) = watchers.get_mut(repo_path) { + w.status = status; + } + } +} + +// --------------------------------------------------------------------------- +// Shutdown helper +// --------------------------------------------------------------------------- + +/// Cancel the watcher's token, wait up to `grace` for a clean exit, then +/// hard-abort the spawned sync task if the grace period elapses. Clones the +/// abort handle before awaiting so the timeout's consumption of the +/// JoinHandle doesn't prevent us from aborting. +async fn shutdown_watcher(w: RepoWatcher, grace: Duration, label: &str) { + w.cancel_token.cancel(); + let abort_handle = w.sync_task.abort_handle(); + match tokio::time::timeout(grace, w.sync_task).await { + Ok(Ok(())) => log::info!("[ContextWatcher] {label}: clean shutdown"), + Ok(Err(e)) if e.is_cancelled() => { + log::info!("[ContextWatcher] {label}: task cancelled") + } + Ok(Err(e)) => log::warn!("[ContextWatcher] {label}: task error: {e}"), + Err(_) => { + log::warn!("[ContextWatcher] {label}: grace expired, aborting"); + abort_handle.abort(); + } + } +} + +// --------------------------------------------------------------------------- +// Sync worker (spawned per repo) +// --------------------------------------------------------------------------- + +async fn sync_worker( + manager: Arc, + repo_path: String, + mut batch_rx: tokio::sync::mpsc::Receiver, + cancel_token: CancellationToken, +) { + loop { + tokio::select! { + _ = cancel_token.cancelled() => break, + batch = batch_rx.recv() => { + let batch = match batch { + Some(b) => b, + None => break, + }; + if batch.created_or_modified.is_empty() && batch.deleted.is_empty() { + continue; + } + + // Keep cached status in lockstep with emitted events so that a + // get_status query mid-sync returns Indexing rather than stale Indexed. + manager + .update_cached_status(&repo_path, IndexWatcherStatus::Indexing) + .await; + + let status = manager + .run_incremental_sync( + &repo_path, + &batch.created_or_modified, + &batch.deleted, + cancel_token.clone(), + ) + .await; + manager.update_cached_status(&repo_path, status).await; + } + } + } +} + +// --------------------------------------------------------------------------- +// Error classification +// --------------------------------------------------------------------------- + +/// Map a `SyncError` to a (reason, details) tuple for the `Error` status event. +/// `reason` is a stable machine-readable tag; `details` is human-readable. +fn classify_sync_error(err: &SyncError) -> (String, String) { + match err { + SyncError::RepoTooLarge { bytes, max } => ( + "repo_too_large".into(), + format!("{bytes} bytes (max {max})"), + ), + SyncError::TooManyFiles { count, max } => ( + "too_many_files".into(), + format!("{count} files (max {max})"), + ), + SyncError::RequestTooLarge { bytes, max } => ( + "request_too_large".into(), + format!("{bytes} bytes gzipped (max {max})"), + ), + SyncError::Cancelled => ("cancelled".into(), "sync cancelled".into()), + SyncError::DeadlineExceeded => ( + "deadline_exceeded".into(), + "sync deadline exceeded".into(), + ), + SyncError::SyncExpired { reason } => ("sync_expired".into(), reason.clone()), + SyncError::BadRequest { reason, details } => { + (format!("bad_request:{reason}"), details.clone()) + } + SyncError::AuthRequired { status } => { + ("auth_required".into(), format!("HTTP {status}")) + } + SyncError::Http(e) => ("http_error".into(), e.to_string()), + SyncError::Io(e) => ("io_error".into(), e.to_string()), + SyncError::Serialization(e) => ("serialization_error".into(), e.to_string()), + SyncError::Other(msg) => ("other".into(), msg.clone()), + } +} diff --git a/apps/desktop/src-tauri/src/engine_control.rs b/apps/desktop/src-tauri/src/engine_control.rs new file mode 100644 index 00000000..00d932c2 --- /dev/null +++ b/apps/desktop/src-tauri/src/engine_control.rs @@ -0,0 +1,539 @@ +//! App-managed context-engine lifecycle. +//! +//! In `app` mode the desktop app owns the docker-compose stack: it pulls the +//! published images, runs `docker compose up -d`, waits for `/api/health`, and +//! stops the stack on quit. In `user` mode this controller is inert — the user +//! runs their own backend and the app just connects to its URL (Phase 4/5). +//! +//! Everything downstream (the file watcher, the search/graph client) is +//! mode-agnostic: it reads `base_url` from the settings DB. In app mode this +//! controller writes the resolved `http://127.0.0.1:` there on start. + +use std::path::PathBuf; +use std::process::Stdio; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; +use std::time::Duration; + +use parking_lot::Mutex; +use serde::Serialize; +use tauri::{AppHandle, Emitter, Manager, State}; +use tokio::io::{AsyncBufReadExt, BufReader}; +use tokio_util::sync::CancellationToken; + +use crate::Database; + +/// Compose project for the app-managed stack. Distinct from the hand-run dev +/// stack (`supercoder-context-engine-dev`) so the two never collide. +const PROJECT: &str = "supercoder-context-engine"; +const DEFAULT_PORT: u16 = 8106; + +// Settings-DB keys. +const CE_KEY: &str = "context_engine"; // shared with agent_bridge::commands +const APP_PORT_KEY: &str = "context_engine_app_port"; +const OPENAI_KEY_KEY: &str = "context_engine_openai_key"; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] +#[serde(rename_all = "lowercase")] +pub enum EngineMode { + User, + App, +} + +/// Resolve the lifecycle mode once at startup. Explicit env wins; otherwise the +/// build profile decides (dev → user, release → app). +pub fn resolve_mode() -> EngineMode { + match std::env::var("SUPERCODER_CE_MODE").ok().as_deref() { + Some("app") => EngineMode::App, + Some("user") => EngineMode::User, + _ => { + if cfg!(debug_assertions) { + EngineMode::User + } else { + EngineMode::App + } + } + } +} + +/// Lifecycle status, mirrored to the frontend via the `engine:status` event. +#[derive(Debug, Clone, Serialize)] +#[serde(tag = "state", rename_all = "snake_case")] +pub enum EngineStatus { + /// Docker CLI/daemon/compose missing or unreachable. + DockerMissing { reason: String }, + Stopped, + /// Pulling images / creating containers. `line` is the latest compose line. + Pulling { line: String }, + /// Containers up; waiting for `/api/health`. + Starting, + Running { base_url: String }, + Error { reason: String, logs_tail: Option }, +} + +pub struct EngineController { + mode: EngineMode, + app: AppHandle, + db: Arc, + status: Mutex, + /// Cancels an in-flight `start()` (compose up + health wait). + cancel: Mutex>, + /// True while a `start()` is in flight, so concurrent starts (auto-start + + /// a user click) can't race and clobber each other's cancel token. + starting: AtomicBool, +} + +impl EngineController { + pub fn new(mode: EngineMode, app: AppHandle, db: Arc) -> Self { + Self { + mode, + app, + db, + status: Mutex::new(EngineStatus::Stopped), + cancel: Mutex::new(None), + starting: AtomicBool::new(false), + } + } + + pub fn mode(&self) -> EngineMode { + self.mode + } + + pub fn status(&self) -> EngineStatus { + self.status.lock().clone() + } + + fn set_status(&self, s: EngineStatus) { + *self.status.lock() = s.clone(); + let _ = self.app.emit("engine:status", &s); + } + + // ── docker plumbing ─────────────────────────────────────────────────── + + async fn run_docker(&self, args: &[&str]) -> Result { + let mut cmd = tokio::process::Command::new("docker"); + cmd.args(args); + git_ops::no_window::no_window_tokio(&mut cmd); + cmd.output().await.map_err(|e| { + if e.kind() == std::io::ErrorKind::NotFound { + "Docker CLI not found on PATH. Install Docker Desktop and retry.".to_string() + } else { + e.to_string() + } + }) + } + + /// Locate the bundled dist compose. Env override (for local testing) → + /// bundled resource → dev fallback (walk up from the executable). + fn compose_file(&self) -> Result { + if let Ok(p) = std::env::var("SUPERCODER_CE_COMPOSE_FILE") { + let pb = PathBuf::from(p); + if pb.exists() { + return Ok(pb); + } + } + if let Ok(dir) = self.app.path().resource_dir() { + for cand in ["docker-compose.dist.yaml", "resources/docker-compose.dist.yaml"] { + let pb = dir.join(cand); + if pb.exists() { + return Ok(pb); + } + } + } + // Dev (`tauri dev`): walk up from the binary to the repo checkout. + if let Ok(exe) = std::env::current_exe() { + let mut cur = exe.parent().map(PathBuf::from); + while let Some(dir) = cur { + let pb = dir.join("services/context-engine/docker-compose.dist.yaml"); + if pb.exists() { + return Ok(pb); + } + cur = dir.parent().map(PathBuf::from); + } + } + Err("could not locate docker-compose.dist.yaml (bundled resource missing)".to_string()) + } + + /// Base `docker compose -p -f ` argv as owned strings. + fn compose_argv(&self, file: &str, extra: &[&str]) -> Vec { + let mut v = vec![ + "compose".to_string(), + "-p".to_string(), + PROJECT.to_string(), + "-f".to_string(), + file.to_string(), + ]; + v.extend(extra.iter().map(|s| s.to_string())); + v + } + + /// `docker compose -p ` argv WITHOUT `-f`. Compose resolves the + /// project from running-container labels, so teardown/inspection works even + /// if the bundled compose file is missing or the app was moved. + fn compose_project_argv(&self, extra: &[&str]) -> Vec { + let mut v = vec!["compose".to_string(), "-p".to_string(), PROJECT.to_string()]; + v.extend(extra.iter().map(|s| s.to_string())); + v + } + + // ── preflight + port ────────────────────────────────────────────────── + + /// Docker CLI present + daemon up + compose v2 available. + pub async fn preflight(&self) -> Result<(), String> { + // `docker version` exits non-zero (with a server-side error) when the + // daemon is down, even though the client part succeeds. + let v = self + .run_docker(&["version", "--format", "{{.Server.Version}}"]) + .await?; + if !v.status.success() { + return Err("Docker daemon is not running. Start Docker and retry.".to_string()); + } + let c = self.run_docker(&["compose", "version"]).await?; + if !c.status.success() { + return Err("Docker Compose v2 is required (`docker compose`).".to_string()); + } + Ok(()) + } + + async fn project_running(&self) -> bool { + match self + .run_docker(&["compose", "-p", PROJECT, "ps", "-q"]) + .await + { + Ok(o) => o.status.success() && !o.stdout.is_empty(), + Err(_) => false, + } + } + + fn port_free(port: u16) -> bool { + std::net::TcpListener::bind(("127.0.0.1", port)).is_ok() + } + + fn saved_port(&self) -> Option { + self.db + .get_setting(APP_PORT_KEY) + .ok() + .flatten() + .and_then(|s| s.parse().ok()) + } + + /// 8106 if it's free or already ours; else the next free port in a small + /// range; else an error (foreign process holding the range). + async fn resolve_port(&self) -> Result { + if self.project_running().await { + return Ok(self.saved_port().unwrap_or(DEFAULT_PORT)); + } + if Self::port_free(DEFAULT_PORT) { + return Ok(DEFAULT_PORT); + } + for p in (DEFAULT_PORT + 1)..=(DEFAULT_PORT + 20) { + if Self::port_free(p) { + return Ok(p); + } + } + Err(format!( + "port {DEFAULT_PORT} (and the next 20) are all in use by other processes" + )) + } + + // ── settings-DB helpers ───────────────────────────────────────────────── + + /// Point the shared context-engine settings at the controller-owned URL so + /// the watcher + search client use it without knowing the mode. + fn set_base_url(&self, url: &str) { + let mut v: serde_json::Value = self + .db + .get_setting(CE_KEY) + .ok() + .flatten() + .and_then(|s| serde_json::from_str(&s).ok()) + .unwrap_or_else(|| serde_json::json!({ "enabled": true, "base_url": url })); + v["base_url"] = serde_json::json!(url); + if let Ok(s) = serde_json::to_string(&v) { + let _ = self.db.set_setting(CE_KEY, &s); + } + } + + pub fn set_openai_key(&self, key: &str) -> Result<(), String> { + self.db.set_setting(OPENAI_KEY_KEY, key) + } + + pub fn has_openai_key(&self) -> bool { + self.db + .get_setting(OPENAI_KEY_KEY) + .ok() + .flatten() + .map(|s| !s.trim().is_empty()) + .unwrap_or(false) + } + + // ── lifecycle ─────────────────────────────────────────────────────────── + + /// Bring the stack up: preflight → resolve port → `compose up -d` (streaming + /// progress) → wait healthy. Idempotent (`up -d` adopts a running stack). + pub async fn start(&self) -> Result<(), String> { + if self.mode != EngineMode::App { + return Err("engine start is only available in app mode".to_string()); + } + // Serialize starts: the first wins, a concurrent caller bails out. + if self.starting.swap(true, Ordering::SeqCst) { + return Err("engine is already starting".to_string()); + } + struct StartGuard<'a>(&'a AtomicBool); + impl Drop for StartGuard<'_> { + fn drop(&mut self) { + self.0.store(false, Ordering::SeqCst); + } + } + let _start_guard = StartGuard(&self.starting); + self.set_status(EngineStatus::Starting); + + if let Err(e) = self.preflight().await { + self.set_status(EngineStatus::DockerMissing { reason: e.clone() }); + return Err(e); + } + + let port = match self.resolve_port().await { + Ok(p) => p, + Err(e) => { + self.set_status(EngineStatus::Error { reason: e.clone(), logs_tail: None }); + return Err(e); + } + }; + let _ = self.db.set_setting(APP_PORT_KEY, &port.to_string()); + let base_url = format!("http://127.0.0.1:{port}"); + self.set_base_url(&base_url); + + let file = self.compose_file()?; + let file_str = file.to_string_lossy().to_string(); + let key = self + .db + .get_setting(OPENAI_KEY_KEY) + .ok() + .flatten() + .unwrap_or_default(); + + let token = CancellationToken::new(); + *self.cancel.lock() = Some(token.clone()); + + self.set_status(EngineStatus::Pulling { + line: "Pulling images & starting containers…".to_string(), + }); + + let argv = self.compose_argv( + &file_str, + &["up", "-d", "--remove-orphans", "--pull", "missing"], + ); + let mut cmd = tokio::process::Command::new("docker"); + cmd.args(&argv); + cmd.env("SUPERCODER_OPENAI_API_KEY", &key); + cmd.env("SUPERCODER_PORT", port.to_string()); + // Pin the engine images to this app's version so an installed vX.Y.Z app + // pulls the matching engine (the tag CI publishes). An explicit env + // override wins, so a local unpushed build can still be tested. + let ver = env!("CARGO_PKG_VERSION"); + cmd.env( + "SUPERCODER_CE_IMAGE", + std::env::var("SUPERCODER_CE_IMAGE").unwrap_or_else(|_| { + format!("ghcr.io/transformeroptimus/supercoder/context-engine:v{ver}") + }), + ); + cmd.env( + "SUPERCODER_CE_MIGRATE_IMAGE", + std::env::var("SUPERCODER_CE_MIGRATE_IMAGE").unwrap_or_else(|_| { + format!("ghcr.io/transformeroptimus/supercoder/context-engine-migrate:v{ver}") + }), + ); + cmd.stdout(Stdio::piped()).stderr(Stdio::piped()); + git_ops::no_window::no_window_tokio(&mut cmd); + + let mut child = cmd + .spawn() + .map_err(|e| format!("failed to launch docker compose: {e}"))?; + + // Stream compose stdout+stderr → progress events; keep the stderr tail so + // a failed `up` can surface the real cause, not just "up failed". + let stderr_tail = Arc::new(Mutex::new(Vec::::new())); + if let Some(out) = child.stdout.take() { + self.spawn_line_relay(out, None); + } + if let Some(err) = child.stderr.take() { + self.spawn_line_relay(err, Some(stderr_tail.clone())); + } + + let exit = tokio::select! { + r = child.wait() => r.map_err(|e| e.to_string())?, + _ = token.cancelled() => { + let _ = child.start_kill(); + let _ = self.stop().await; + let e = "startup cancelled".to_string(); + self.set_status(EngineStatus::Stopped); + return Err(e); + } + }; + + if !exit.success() { + let tail = self.logs(120).await.ok(); + let stderr = stderr_tail.lock().join("\n"); + let reason = if stderr.trim().is_empty() { + "docker compose up failed".to_string() + } else { + format!("docker compose up failed:\n{}", stderr.trim()) + }; + self.set_status(EngineStatus::Error { reason: reason.clone(), logs_tail: tail }); + return Err(reason); + } + + self.set_status(EngineStatus::Starting); + match self.wait_health(&base_url, &token).await { + Ok(()) => { + self.set_status(EngineStatus::Running { base_url }); + Ok(()) + } + Err(e) => { + let tail = self.logs(120).await.ok(); + self.set_status(EngineStatus::Error { reason: e.clone(), logs_tail: tail }); + Err(e) + } + } + } + + fn spawn_line_relay(&self, reader: R, sink: Option>>>) + where + R: tokio::io::AsyncRead + Unpin + Send + 'static, + { + let app = self.app.clone(); + tokio::spawn(async move { + let mut lines = BufReader::new(reader).lines(); + while let Ok(Some(line)) = lines.next_line().await { + let _ = app.emit("engine:progress", &line); + if let Some(s) = &sink { + let mut g = s.lock(); + g.push(line); + let overflow = g.len().saturating_sub(20); + if overflow > 0 { + g.drain(0..overflow); + } + } + } + }); + } + + /// Poll `/api/health` until it answers 2xx or ~3 min elapse. + async fn wait_health(&self, base_url: &str, token: &CancellationToken) -> Result<(), String> { + let client = reqwest::Client::builder() + .timeout(Duration::from_secs(4)) + .build() + .map_err(|e| e.to_string())?; + let url = format!("{base_url}/api/health"); + for _ in 0..60 { + if token.is_cancelled() { + return Err("startup cancelled".to_string()); + } + if let Ok(r) = client.get(&url).send().await { + if r.status().is_success() { + return Ok(()); + } + } + // Wake immediately on cancellation instead of sitting out the sleep. + tokio::select! { + _ = token.cancelled() => return Err("startup cancelled".to_string()), + _ = tokio::time::sleep(Duration::from_secs(3)) => {} + } + } + Err("context engine did not become healthy within 3 minutes".to_string()) + } + + /// Stop containers, keep volumes (fast restart). Cancels any in-flight start. + pub async fn stop(&self) -> Result<(), String> { + if let Some(t) = self.cancel.lock().take() { + t.cancel(); + } + // Short stop timeout: the worker has no SIGTERM handler and would + // otherwise hold the default ~10s grace before being killed, making quit + // feel slow. 3s lets the datastores flush; volumes are kept either way. + let argv = self.compose_project_argv(&["stop", "-t", "3"]); + let refs: Vec<&str> = argv.iter().map(String::as_str).collect(); + self.run_docker(&refs).await?; + self.set_status(EngineStatus::Stopped); + Ok(()) + } + + /// `down` (optionally `-v` to drop the indexed-data volumes). + pub async fn down(&self, remove_data: bool) -> Result<(), String> { + if let Some(t) = self.cancel.lock().take() { + t.cancel(); + } + let extra: &[&str] = if remove_data { + &["down", "-v", "--remove-orphans"] + } else { + &["down", "--remove-orphans"] + }; + let argv = self.compose_project_argv(extra); + let refs: Vec<&str> = argv.iter().map(String::as_str).collect(); + self.run_docker(&refs).await?; + self.set_status(EngineStatus::Stopped); + Ok(()) + } + + /// Tail of the combined compose logs (for surfacing a failure cause). + pub async fn logs(&self, tail: usize) -> Result { + let tailn = tail.to_string(); + let argv = self.compose_project_argv(&["logs", "--tail", &tailn, "--no-color"]); + let refs: Vec<&str> = argv.iter().map(String::as_str).collect(); + let o = self.run_docker(&refs).await?; + let mut s = String::from_utf8_lossy(&o.stdout).into_owned(); + s.push_str(&String::from_utf8_lossy(&o.stderr)); + Ok(s) + } +} + +// ── Tauri commands ────────────────────────────────────────────────────────── + +type Ctl<'a> = State<'a, Arc>; + +#[tauri::command] +pub fn agent_engine_mode(controller: Ctl<'_>) -> EngineMode { + controller.mode() +} + +#[tauri::command] +pub fn agent_engine_status(controller: Ctl<'_>) -> EngineStatus { + controller.status() +} + +#[tauri::command] +pub async fn agent_engine_preflight(controller: Ctl<'_>) -> Result<(), String> { + controller.preflight().await +} + +#[tauri::command] +pub async fn agent_engine_start( + controller: Ctl<'_>, + watcher: State<'_, Arc>, +) -> Result<(), String> { + controller.start().await?; + // Backend is healthy — (re)attach the live watchers for known repos. + watcher.auto_start().await; + Ok(()) +} + +#[tauri::command] +pub async fn agent_engine_stop(controller: Ctl<'_>) -> Result<(), String> { + controller.stop().await +} + +#[tauri::command] +pub async fn agent_engine_remove(remove_data: bool, controller: Ctl<'_>) -> Result<(), String> { + controller.down(remove_data).await +} + +#[tauri::command] +pub fn agent_engine_has_key(controller: Ctl<'_>) -> bool { + controller.has_openai_key() +} + +#[tauri::command] +pub fn agent_engine_set_key(key: String, controller: Ctl<'_>) -> Result<(), String> { + controller.set_openai_key(key.trim()) +} diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs new file mode 100644 index 00000000..0718e30d --- /dev/null +++ b/apps/desktop/src-tauri/src/lib.rs @@ -0,0 +1,705 @@ +use parking_lot::Mutex; +use rusqlite::Connection; +use serde::Serialize; +use std::path::PathBuf; +use std::sync::Arc; +use tauri::Manager; + +pub mod agent_bridge; +pub mod context_watcher; +pub mod engine_control; + +/// Local application data directory: `/.supercoder`. +/// Holds the settings DB, the agent DB, skills/subagents, and the checkpoint root. +pub fn app_data_dir() -> PathBuf { + dirs::data_local_dir() + .unwrap_or_else(|| PathBuf::from(".")) + .join(".supercoder") +} + +// ── Database (local settings store) ─────────────────────────────────────────── + +pub struct Database { + pub(crate) conn: Mutex, +} + +impl Database { + pub fn new() -> Result { + let db_dir = app_data_dir(); + std::fs::create_dir_all(&db_dir).map_err(|e| e.to_string())?; + let db_path = db_dir.join("supercoder.db"); + + let conn = Connection::open(&db_path).map_err(|e| e.to_string())?; + conn.execute_batch( + "CREATE TABLE IF NOT EXISTS settings ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL, + updated_at TEXT DEFAULT CURRENT_TIMESTAMP + );", + ) + .map_err(|e| e.to_string())?; + + Ok(Self { + conn: Mutex::new(conn), + }) + } + + pub fn get_setting(&self, key: &str) -> Result, String> { + let conn = self.conn.lock(); + let mut stmt = conn + .prepare("SELECT value FROM settings WHERE key = ?") + .map_err(|e| e.to_string())?; + let result: Option = stmt.query_row([key], |row| row.get(0)).ok(); + Ok(result) + } + + pub fn set_setting(&self, key: &str, value: &str) -> Result<(), String> { + let conn = self.conn.lock(); + conn.execute( + "INSERT OR REPLACE INTO settings (key, value, updated_at) VALUES (?, ?, CURRENT_TIMESTAMP)", + [key, value], + ) + .map_err(|e| e.to_string())?; + Ok(()) + } + + pub fn delete_setting(&self, key: &str) -> Result<(), String> { + let conn = self.conn.lock(); + conn.execute("DELETE FROM settings WHERE key = ?", [key]) + .map_err(|e| e.to_string())?; + Ok(()) + } +} + +// ── AppState ──────────────────────────────────────────────────────────────── + +pub struct AppState { + pub db: Arc, +} + +// ── File I/O ────────────────────────────────────────────────────────────────── + +#[tauri::command] +async fn read_file_text(path: String) -> Result, String> { + let p = std::path::Path::new(&path); + if !p.exists() { + return Ok(None); + } + std::fs::read_to_string(p) + .map(Some) + .map_err(|e| format!("Failed to read file: {e}")) +} + +#[tauri::command] +async fn read_file_bytes(path: String) -> Result, String> { + std::fs::read(&path).map_err(|e| format!("Failed to read file: {e}")) +} + +/// Save raw bytes to a temp file and return the path. +/// Used for clipboard-pasted images that need a local path for the agent. +#[tauri::command] +fn save_temp_file(file_bytes: Vec, file_name: String) -> Result { + let temp_dir = std::env::temp_dir().join("supercoder-pastes"); + std::fs::create_dir_all(&temp_dir).map_err(|e| format!("Failed to create temp dir: {e}"))?; + let dest = temp_dir.join(&file_name); + std::fs::write(&dest, &file_bytes).map_err(|e| format!("Failed to write temp file: {e}"))?; + Ok(dest.to_string_lossy().to_string()) +} + +/// Download a file from a URL and save it to the user's Downloads folder. +/// Returns the full path of the saved file. +#[tauri::command] +async fn download_file(url: String, file_name: String) -> Result { + let downloads_dir = dirs::download_dir() + .unwrap_or_else(|| dirs::home_dir().unwrap_or_else(|| PathBuf::from(".")).join("Downloads")); + + std::fs::create_dir_all(&downloads_dir).map_err(|e| format!("Failed to create downloads dir: {}", e))?; + + // Avoid overwriting: if file exists, append (1), (2), etc. + let mut dest = downloads_dir.join(&file_name); + if dest.exists() { + let stem = dest.file_stem().and_then(|s| s.to_str()).unwrap_or(&file_name).to_string(); + let ext = dest.extension().and_then(|s| s.to_str()).map(|s| format!(".{}", s)).unwrap_or_default(); + let mut counter = 1u32; + loop { + dest = downloads_dir.join(format!("{} ({}){}", stem, counter, ext)); + if !dest.exists() { + break; + } + counter += 1; + } + } + + log::info!("[Download] {} -> {:?}", url, dest); + + let client = reqwest::Client::new(); + let response = client + .get(&url) + .send() + .await + .map_err(|e| format!("Download failed: {}", e))?; + + if !response.status().is_success() { + return Err(format!("Download failed with status: {}", response.status())); + } + + let bytes = response + .bytes() + .await + .map_err(|e| format!("Failed to read response: {}", e))?; + + tokio::fs::write(&dest, &bytes) + .await + .map_err(|e| format!("Failed to write file: {}", e))?; + + let path_str = dest.to_string_lossy().to_string(); + log::info!("[Download] Saved to {}", path_str); + Ok(path_str) +} + +/// Fetch a URL and return the body as a string (bypasses browser CORS). +#[tauri::command] +async fn fetch_url(url: String) -> Result { + // SSRF guard: only http(s), and refuse loopback/private/link-local hosts so + // webview- or content-supplied URLs can't probe internal services or read + // file:// resources. + let parsed = reqwest::Url::parse(&url).map_err(|e| format!("invalid url: {e}"))?; + match parsed.scheme() { + "http" | "https" => {} + other => return Err(format!("unsupported url scheme: {other}")), + } + match parsed.host_str() { + Some(h) if !is_blocked_fetch_host(h) => {} + Some(_) => return Err("refusing to fetch loopback/private/link-local address".into()), + None => return Err("url has no host".into()), + } + let resp = reqwest::get(parsed).await.map_err(|e| format!("fetch failed: {e}"))?; + if !resp.status().is_success() { + return Err(format!("HTTP {}", resp.status())); + } + resp.text().await.map_err(|e| format!("read body failed: {e}")) +} + +/// Defense-in-depth SSRF filter for `fetch_url`. Blocks localhost names and +/// literal loopback/private/link-local IPs. Does NOT resolve DNS names to IPs — +/// full protection would require a custom connector; this covers the common cases. +fn is_blocked_fetch_host(host: &str) -> bool { + let h = host + .trim_start_matches('[') + .trim_end_matches(']') + .to_ascii_lowercase(); + if h == "localhost" || h.ends_with(".localhost") || h.ends_with(".local") { + return true; + } + match h.parse::() { + Ok(std::net::IpAddr::V4(v4)) => { + v4.is_loopback() || v4.is_private() || v4.is_link_local() || v4.is_unspecified() + } + Ok(std::net::IpAddr::V6(v6)) => { + let s = v6.segments(); + v6.is_loopback() + || v6.is_unspecified() + || (s[0] & 0xfe00) == 0xfc00 // unique-local fc00::/7 + || (s[0] & 0xffc0) == 0xfe80 // link-local fe80::/10 + } + Err(_) => false, + } +} + +// ── OS integration ──────────────────────────────────────────────────────────── + +#[tauri::command] +async fn open_in_vscode(path: String) -> Result<(), String> { + #[cfg(target_os = "macos")] + { + std::process::Command::new("open") + .args(["-a", "Visual Studio Code", &path]) + .spawn() + .map_err(|e| e.to_string())?; + } + #[cfg(target_os = "windows")] + { + std::process::Command::new("cmd") + .args(["/c", "code", &path]) + .spawn() + .map_err(|e| e.to_string())?; + } + #[cfg(target_os = "linux")] + { + std::process::Command::new("code") + .arg(&path) + .spawn() + .map_err(|e| e.to_string())?; + } + Ok(()) +} + +#[tauri::command] +async fn open_in_terminal(path: String) -> Result<(), String> { + #[cfg(target_os = "macos")] + { + std::process::Command::new("open") + .args(["-a", "Terminal", &path]) + .spawn() + .map_err(|e| e.to_string())?; + } + #[cfg(target_os = "windows")] + { + // Set the working dir instead of interpolating `path` into a shell + // string, so a folder name with shell metacharacters can't inject. + std::process::Command::new("cmd") + .args(["/c", "start", "cmd"]) + .current_dir(&path) + .spawn() + .map_err(|e| e.to_string())?; + } + #[cfg(target_os = "linux")] + { + // Set the working dir instead of interpolating `path` into the shell + // string passed to `xterm -e`, so the path can't inject commands. + std::process::Command::new("xterm") + .args(["-e", "bash"]) + .current_dir(&path) + .spawn() + .map_err(|e| e.to_string())?; + } + Ok(()) +} + +#[tauri::command] +async fn open_in_finder(path: String) -> Result<(), String> { + #[cfg(target_os = "macos")] + { + std::process::Command::new("open") + .arg(&path) + .spawn() + .map_err(|e| e.to_string())?; + } + #[cfg(target_os = "windows")] + { + std::process::Command::new("explorer") + .arg(&path) + .spawn() + .map_err(|e| e.to_string())?; + } + #[cfg(target_os = "linux")] + { + std::process::Command::new("xdg-open") + .arg(&path) + .spawn() + .map_err(|e| e.to_string())?; + } + Ok(()) +} + +#[cfg(target_os = "macos")] +#[link(name = "ApplicationServices", kind = "framework")] +extern "C" { + fn AXIsProcessTrusted() -> bool; +} + +#[tauri::command] +fn check_accessibility_permission() -> bool { + #[cfg(target_os = "macos")] + { + unsafe { AXIsProcessTrusted() } + } + #[cfg(not(target_os = "macos"))] + true +} + +#[tauri::command] +fn open_accessibility_settings() -> Result<(), String> { + #[cfg(target_os = "macos")] + { + std::process::Command::new("open") + .arg("x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility") + .spawn() + .map_err(|e| e.to_string())?; + } + Ok(()) +} + +#[derive(Serialize)] +struct FileNode { + title: String, + key: String, + is_leaf: bool, + children: Vec, +} + +fn build_file_tree(dir: &std::path::Path, prefix: &str) -> Vec { + let mut nodes = Vec::new(); + let entries = match std::fs::read_dir(dir) { + Ok(e) => e, + Err(_) => return nodes, + }; + + let mut items: Vec<_> = entries.filter_map(|e| e.ok()).collect(); + items.sort_by_key(|e| { + let is_dir = e.file_type().map(|ft| ft.is_dir()).unwrap_or(false); + (!is_dir, e.file_name()) + }); + + for entry in items { + let name = entry.file_name().to_string_lossy().to_string(); + // Skip hidden files/dirs and common non-useful dirs + if name.starts_with('.') || name == "node_modules" || name == "target" || name == "__pycache__" { + continue; + } + let key = if prefix.is_empty() { + name.clone() + } else { + format!("{}/{}", prefix, name) + }; + let is_dir = entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false); + let children = if is_dir { + build_file_tree(&entry.path(), &key) + } else { + vec![] + }; + nodes.push(FileNode { + title: name, + key, + is_leaf: !is_dir, + children, + }); + } + nodes +} + +#[tauri::command] +async fn list_directory_tree(path: String) -> Result, String> { + let dir = std::path::Path::new(&path); + if !dir.is_dir() { + return Err(format!("Not a directory: {}", path)); + } + Ok(build_file_tree(dir, "")) +} + +// ── Git operations ────────────────────────────────────────────────────────── + +#[tauri::command] +async fn git_branches(repo_path: String) -> Result, String> { + git_ops::branches(std::path::Path::new(&repo_path)) + .await + .map_err(|e| e.to_string()) +} + +/// List tracked files via `git ls-files` (fast, respects .gitignore). +#[tauri::command] +async fn git_ls_files(repo_path: String) -> Result, String> { + let mut cmd = tokio::process::Command::new("git"); + cmd.args(["ls-files"]).current_dir(&repo_path); + git_ops::no_window::no_window_tokio(&mut cmd); + let output = cmd + .output() + .await + .map_err(|e| format!("Failed to run git ls-files: {e}"))?; + if !output.status.success() { + return Err(format!( + "git ls-files failed: {}", + String::from_utf8_lossy(&output.stderr) + )); + } + let files: Vec = String::from_utf8_lossy(&output.stdout) + .lines() + .map(|s| s.to_string()) + .collect(); + Ok(files) +} + +#[tauri::command] +async fn git_status(repo_path: String) -> Result { + git_ops::status(std::path::Path::new(&repo_path)) + .await + .map_err(|e| e.to_string()) +} + +#[tauri::command] +async fn git_diff(repo_path: String) -> Result { + git_ops::diff(std::path::Path::new(&repo_path), None, false, None) + .await + .map_err(|e| e.to_string()) +} + +#[tauri::command] +async fn git_commit(repo_path: String, message: String) -> Result { + git_ops::commit(std::path::Path::new(&repo_path), &message, None) + .await + .map_err(|e| e.to_string()) +} + +#[tauri::command] +async fn git_push(repo_path: String, branch: String) -> Result { + git_ops::push(std::path::Path::new(&repo_path), Some(&branch), None) + .await + .map_err(|e| e.to_string()) +} + +#[tauri::command] +async fn git_log(repo_path: String, count: u32) -> Result, String> { + git_ops::log(std::path::Path::new(&repo_path), Some(count)) + .await + .map_err(|e| e.to_string()) +} + +#[tauri::command] +async fn git_create_branch( + repo_path: String, + name: String, + from: Option, +) -> Result<(), String> { + git_ops::create_branch(std::path::Path::new(&repo_path), &name, from.as_deref()) + .await + .map_err(|e| e.to_string()) +} + +#[tauri::command] +async fn git_switch_branch(repo_path: String, name: String) -> Result<(), String> { + git_ops::switch_branch(std::path::Path::new(&repo_path), &name) + .await + .map_err(|e| e.to_string()) +} + +#[tauri::command] +async fn git_create_pr( + repo_path: String, + title: String, + body: String, + branch: String, + base: String, +) -> Result { + git_ops::pr::create(std::path::Path::new(&repo_path), &title, &body, &branch, &base) + .await + .map_err(|e| e.to_string()) +} + +// ── App entry ─────────────────────────────────────────────────────────────── + +#[cfg_attr(mobile, tauri::mobile_entry_point)] +pub fn run() { + // Load environment-specific .env file, then base .env and .env.local as fallbacks. + let app_env = option_env!("VITE_APP_ENV").unwrap_or("development"); + let _ = dotenvy::from_filename(format!("../.env.{}", app_env)); + let _ = dotenvy::from_filename("../.env.local"); + let _ = dotenvy::dotenv(); + + env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init(); + + tauri::Builder::default() + // Single-instance must be registered first: only one app process may own + // the (app-managed) context-engine stack lifecycle. A second launch just + // focuses the existing window. + .plugin(tauri_plugin_single_instance::init(|app, _argv, _cwd| { + if let Some(w) = app.get_webview_window("main") { + let _ = w.unminimize(); + let _ = w.show(); + let _ = w.set_focus(); + } + })) + .plugin(tauri_plugin_shell::init()) + .plugin(tauri_plugin_pty::init()) + .plugin(tauri_plugin_notification::init()) + .plugin(tauri_plugin_dialog::init()) + .setup(|app| { + let db = Arc::new(Database::new().expect("Failed to create database")); + + // Ensure the settings-DB tables the backend owns exist. + { + let conn = db.conn.lock(); + agent_bridge::permissions::ensure_permissions_table(&conn) + .expect("Failed to create agent_permissions table"); + context_watcher::db::ensure_watched_repos_table(&conn) + .expect("Failed to create watched_repos table"); + } + + // Resolve the stable machine_id once (same value `commands::machine_id` + // resolves) so the watcher and the search client share a collection key. + let machine_id = { + let existing = db + .get_setting("machine_id") + .ok() + .flatten() + .filter(|s| !s.is_empty()); + match existing { + Some(id) => id, + None => { + let id = uuid::Uuid::new_v4().to_string(); + let _ = db.set_setting("machine_id", &id); + id + } + } + }; + + // File-watcher subsystem: keeps the context-engine index live as the + // user edits. Always built (reads `enabled` live from settings, so the + // Settings toggle can start/stop it at runtime). + let watcher_manager = Arc::new(context_watcher::WatcherManager::new( + machine_id, + Arc::clone(&db), + app.handle().clone(), + )); + app.manage(Arc::clone(&watcher_manager)); + + // App-managed context-engine lifecycle controller (inert in user mode). + let engine_mode = engine_control::resolve_mode(); + let engine_controller = Arc::new(engine_control::EngineController::new( + engine_mode, + app.handle().clone(), + Arc::clone(&db), + )); + app.manage(Arc::clone(&engine_controller)); + + // On launch: in app mode bring the stack up first (when the feature is + // enabled) so the watcher's full-sync has a backend to talk to; then + // re-watch repos used in the last 7 days. + { + let wm = Arc::clone(&watcher_manager); + let ctl = Arc::clone(&engine_controller); + let enabled = agent_bridge::commands::read_context_engine_db(&db).enabled; + tauri::async_runtime::spawn(async move { + if ctl.mode() == engine_control::EngineMode::App && enabled { + if let Err(e) = ctl.start().await { + log::warn!("[engine] app-managed start failed: {e}"); + } + } + wm.auto_start().await; + }); + } + + let state = AppState { db }; + app.manage(state); + + // Agent DB + checkpoint root, both under the app data dir. + let data_dir = app_data_dir(); + let agent_db = agent_bridge::db::AgentDb::new(&data_dir) + .expect("Failed to create agent database"); + let agent_db_arc = Arc::new(agent_db); + + let checkpoint_root = data_dir.join("checkpoints"); + std::fs::create_dir_all(&checkpoint_root) + .expect("Failed to create checkpoint root"); + + let agent_state = + agent_bridge::commands::AgentState::new(Arc::clone(&agent_db_arc), checkpoint_root); + app.manage(agent_state); + + let window = app.get_webview_window("main").unwrap(); + if let Some(monitor) = window.current_monitor().unwrap_or(None) { + let size = monitor.size(); + let scale = monitor.scale_factor(); + let screen_w = size.width as f64 / scale; + let screen_h = size.height as f64 / scale; + let w = screen_w * 0.80; + let h = screen_h * 0.80; + let x = (screen_w - w) / 2.0; + let y = (screen_h - h) / 2.0; + let _ = window.set_size(tauri::LogicalSize::new(w, h)); + let _ = window.set_position(tauri::LogicalPosition::new(x, y)); + } + Ok(()) + }) + .invoke_handler(tauri::generate_handler![ + open_in_vscode, + open_in_terminal, + open_in_finder, + check_accessibility_permission, + open_accessibility_settings, + list_directory_tree, + git_ls_files, + fetch_url, + read_file_text, + read_file_bytes, + save_temp_file, + download_file, + git_branches, + git_status, + git_diff, + git_commit, + git_push, + git_log, + git_create_branch, + git_switch_branch, + git_create_pr, + agent_bridge::commands::agent_create_session, + agent_bridge::commands::agent_rename_session, + agent_bridge::commands::agent_set_session_model, + agent_bridge::commands::agent_delete_session, + agent_bridge::commands::agent_send_message, + agent_bridge::commands::agent_start_coding_from_plan, + agent_bridge::commands::agent_cancel_session, + agent_bridge::commands::agent_approve_tool, + agent_bridge::commands::agent_get_messages, + agent_bridge::commands::agent_get_context_usage, + agent_bridge::commands::agent_clear_context, + agent_bridge::commands::agent_compact_context, + agent_bridge::commands::agent_get_diff, + agent_bridge::commands::agent_get_working_diff, + agent_bridge::commands::agent_list_sessions, + agent_bridge::commands::agent_list_providers, + agent_bridge::commands::agent_add_provider, + agent_bridge::commands::agent_update_provider, + agent_bridge::commands::agent_delete_provider, + agent_bridge::commands::agent_set_model_selection, + agent_bridge::commands::agent_fetch_provider_models, + agent_bridge::commands::agent_verify_provider, + agent_bridge::commands::agent_resolve_model_capability, + agent_bridge::commands::agent_list_models, + agent_bridge::commands::agent_get_context_engine, + agent_bridge::commands::agent_set_context_engine, + agent_bridge::commands::agent_context_engine_status, + agent_bridge::commands::agent_context_engine_repos, + agent_bridge::commands::agent_context_engine_delete_repo, + engine_control::agent_engine_mode, + engine_control::agent_engine_status, + engine_control::agent_engine_preflight, + engine_control::agent_engine_start, + engine_control::agent_engine_stop, + engine_control::agent_engine_remove, + engine_control::agent_engine_has_key, + engine_control::agent_engine_set_key, + context_watcher::commands::context_watcher_start, + context_watcher::commands::context_watcher_stop, + context_watcher::commands::context_watcher_status, + agent_bridge::commands::agent_get_permissions, + agent_bridge::commands::agent_set_permission, + agent_bridge::commands::agent_list_skills, + agent_bridge::commands::agent_set_skill_enabled, + agent_bridge::commands::agent_get_skills_paths, + agent_bridge::commands::agent_list_subagents, + agent_bridge::commands::agent_set_subagent_enabled, + agent_bridge::commands::agent_get_subagents_paths, + agent_bridge::commands::agent_list_checkpoints, + agent_bridge::commands::agent_get_turn_diff, + agent_bridge::commands::agent_get_full_diff, + agent_bridge::commands::agent_restore_checkpoint, + agent_bridge::commands::agent_rewind_to_message, + ]) + .build(tauri::generate_context!()) + .expect("error while building tauri application") + .run(|app_handle, event| { + // Clean up only on actual process termination — Cmd+Q / dock-quit on + // macOS, window-close on Windows/Linux. (A macOS window-X keeps the app + // alive in the dock, so we must NOT tear down there.) + if let tauri::RunEvent::Exit = event { + if let Some(wm) = + app_handle.try_state::>() + { + let wm = wm.inner().clone(); + tauri::async_runtime::block_on(async move { wm.stop_all().await }); + } + // App-managed stack: stop containers (keep volumes). + if let Some(ctl) = + app_handle.try_state::>() + { + if ctl.mode() == engine_control::EngineMode::App { + let ctl = ctl.inner().clone(); + tauri::async_runtime::block_on(async move { + let _ = ctl.stop().await; + }); + } + } + } + }); +} diff --git a/apps/desktop/src-tauri/src/main.rs b/apps/desktop/src-tauri/src/main.rs new file mode 100644 index 00000000..faf886e4 --- /dev/null +++ b/apps/desktop/src-tauri/src/main.rs @@ -0,0 +1,6 @@ +// Prevents additional console window on Windows in release +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +fn main() { + supercoder_lib::run() +} diff --git a/apps/desktop/src-tauri/tauri.conf.json b/apps/desktop/src-tauri/tauri.conf.json new file mode 100644 index 00000000..0819cefe --- /dev/null +++ b/apps/desktop/src-tauri/tauri.conf.json @@ -0,0 +1,61 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "productName": "SuperCoder", + "version": "0.1.0", + "identifier": "com.supercoder.desktop", + "build": { + "beforeDevCommand": "npm run dev", + "devUrl": "http://localhost:1420", + "beforeBuildCommand": "npm run build", + "frontendDist": "../dist" + }, + "app": { + "withGlobalTauri": true, + "macOSPrivateApi": true, + "windows": [ + { + "title": "SuperCoder", + "label": "main", + "width": 900, + "height": 680, + "resizable": true, + "fullscreen": false, + "dragDropEnabled": false + } + ], + "security": { + "csp": null + } + }, + "bundle": { + "active": true, + "targets": "all", + "resources": { + "../../../services/context-engine/docker-compose.dist.yaml": "docker-compose.dist.yaml" + }, + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png", + "icons/icon.icns", + "icons/icon.ico" + ], + "macOS": { + "entitlements": "Entitlements.plist", + "infoPlist": "Info.plist" + }, + "linux": { + "deb": { + "depends": [ + "libwebkit2gtk-4.1-0", + "libgtk-3-0" + ] + } + } + }, + "plugins": { + "shell": { + "open": true + } + } +} diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx new file mode 100644 index 00000000..160515d1 --- /dev/null +++ b/apps/desktop/src/App.tsx @@ -0,0 +1,42 @@ +import { useEffect, useRef } from 'react'; +import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; +import { ThemeProvider } from './context/ThemeContext'; +import { AntDThemeProvider } from './providers/AntDThemeProvider'; +import TopBar from './components/TopBar'; +import Workspace from './pages/Workspace'; +import Settings from './pages/Settings'; +import { useAppStore } from './store'; +import { useAgentEvents } from './hooks/useAgentEvents'; + +function App() { + const loadProviders = useAppStore((s) => s.loadProviders); + const initialized = useRef(false); + + // Listen for local agent events (Tauri → store), once. + useAgentEvents(); + + useEffect(() => { + if (initialized.current) return; + initialized.current = true; + loadProviders(); + }, [loadProviders]); + + return ( + + + +
+ + + } /> + } /> + } /> + +
+
+
+
+ ); +} + +export default App; diff --git a/gui/public/fonts/proxima-nova-2/Mark-Simonson-Proxima-Nova-Semibold-Italic.otf b/apps/desktop/src/assets/fonts/proxima-nova/Mark-Simonson-Proxima-Nova-Black-Italic.otf similarity index 100% rename from gui/public/fonts/proxima-nova-2/Mark-Simonson-Proxima-Nova-Semibold-Italic.otf rename to apps/desktop/src/assets/fonts/proxima-nova/Mark-Simonson-Proxima-Nova-Black-Italic.otf diff --git a/workspace-service/templates/django/myapp/__init__.py b/apps/desktop/src/assets/fonts/proxima-nova/Mark-Simonson-Proxima-Nova-Black.otf similarity index 100% rename from workspace-service/templates/django/myapp/__init__.py rename to apps/desktop/src/assets/fonts/proxima-nova/Mark-Simonson-Proxima-Nova-Black.otf diff --git a/gui/public/fonts/proxima-nova-2/Mark-Simonson-Proxima-Nova-Extrabold-Italic.otf b/apps/desktop/src/assets/fonts/proxima-nova/Mark-Simonson-Proxima-Nova-Bold-Italic.otf similarity index 100% rename from gui/public/fonts/proxima-nova-2/Mark-Simonson-Proxima-Nova-Extrabold-Italic.otf rename to apps/desktop/src/assets/fonts/proxima-nova/Mark-Simonson-Proxima-Nova-Bold-Italic.otf diff --git a/gui/public/fonts/proxima-nova-2/Mark-Simonson-Proxima-Nova-Bold.otf b/apps/desktop/src/assets/fonts/proxima-nova/Mark-Simonson-Proxima-Nova-Bold.otf similarity index 100% rename from gui/public/fonts/proxima-nova-2/Mark-Simonson-Proxima-Nova-Bold.otf rename to apps/desktop/src/assets/fonts/proxima-nova/Mark-Simonson-Proxima-Nova-Bold.otf diff --git a/gui/public/fonts/proxima-nova-2/Mark-Simonson-Proxima-Nova-Black-Italic.otf b/apps/desktop/src/assets/fonts/proxima-nova/Mark-Simonson-Proxima-Nova-Extrabold-Italic.otf similarity index 100% rename from gui/public/fonts/proxima-nova-2/Mark-Simonson-Proxima-Nova-Black-Italic.otf rename to apps/desktop/src/assets/fonts/proxima-nova/Mark-Simonson-Proxima-Nova-Extrabold-Italic.otf diff --git a/gui/public/fonts/proxima-nova-2/Mark-Simonson-Proxima-Nova-Extrabold.otf b/apps/desktop/src/assets/fonts/proxima-nova/Mark-Simonson-Proxima-Nova-Extrabold.otf similarity index 100% rename from gui/public/fonts/proxima-nova-2/Mark-Simonson-Proxima-Nova-Extrabold.otf rename to apps/desktop/src/assets/fonts/proxima-nova/Mark-Simonson-Proxima-Nova-Extrabold.otf diff --git a/gui/public/fonts/proxima-nova-2/Mark-Simonson-Proxima-Nova-Light-Italic.otf b/apps/desktop/src/assets/fonts/proxima-nova/Mark-Simonson-Proxima-Nova-Light-Italic.otf similarity index 100% rename from gui/public/fonts/proxima-nova-2/Mark-Simonson-Proxima-Nova-Light-Italic.otf rename to apps/desktop/src/assets/fonts/proxima-nova/Mark-Simonson-Proxima-Nova-Light-Italic.otf diff --git a/gui/public/fonts/proxima-nova-2/Mark-Simonson-Proxima-Nova-Light.otf b/apps/desktop/src/assets/fonts/proxima-nova/Mark-Simonson-Proxima-Nova-Light.otf similarity index 100% rename from gui/public/fonts/proxima-nova-2/Mark-Simonson-Proxima-Nova-Light.otf rename to apps/desktop/src/assets/fonts/proxima-nova/Mark-Simonson-Proxima-Nova-Light.otf diff --git a/gui/public/fonts/proxima-nova-2/Mark-Simonson-Proxima-Nova-Regular-Italic.otf b/apps/desktop/src/assets/fonts/proxima-nova/Mark-Simonson-Proxima-Nova-Regular-Italic.otf similarity index 100% rename from gui/public/fonts/proxima-nova-2/Mark-Simonson-Proxima-Nova-Regular-Italic.otf rename to apps/desktop/src/assets/fonts/proxima-nova/Mark-Simonson-Proxima-Nova-Regular-Italic.otf diff --git a/gui/public/fonts/proxima-nova-2/Mark-Simonson-Proxima-Nova-Regular.otf b/apps/desktop/src/assets/fonts/proxima-nova/Mark-Simonson-Proxima-Nova-Regular.otf similarity index 100% rename from gui/public/fonts/proxima-nova-2/Mark-Simonson-Proxima-Nova-Regular.otf rename to apps/desktop/src/assets/fonts/proxima-nova/Mark-Simonson-Proxima-Nova-Regular.otf diff --git a/workspace-service/templates/django/myapp/migrations/__init__.py b/apps/desktop/src/assets/fonts/proxima-nova/Mark-Simonson-Proxima-Nova-Semibold-Italic.otf similarity index 100% rename from workspace-service/templates/django/myapp/migrations/__init__.py rename to apps/desktop/src/assets/fonts/proxima-nova/Mark-Simonson-Proxima-Nova-Semibold-Italic.otf diff --git a/gui/public/fonts/proxima-nova-2/Mark-Simonson-Proxima-Nova-Semibold.otf b/apps/desktop/src/assets/fonts/proxima-nova/Mark-Simonson-Proxima-Nova-Semibold.otf similarity index 100% rename from gui/public/fonts/proxima-nova-2/Mark-Simonson-Proxima-Nova-Semibold.otf rename to apps/desktop/src/assets/fonts/proxima-nova/Mark-Simonson-Proxima-Nova-Semibold.otf diff --git a/gui/public/fonts/proxima-nova-2/Mark-Simonson-Proxima-Nova-Thin-Italic.otf b/apps/desktop/src/assets/fonts/proxima-nova/Mark-Simonson-Proxima-Nova-Thin-Italic.otf similarity index 100% rename from gui/public/fonts/proxima-nova-2/Mark-Simonson-Proxima-Nova-Thin-Italic.otf rename to apps/desktop/src/assets/fonts/proxima-nova/Mark-Simonson-Proxima-Nova-Thin-Italic.otf diff --git a/gui/public/fonts/proxima-nova-2/Mark-Simonson-Proxima-Nova-Thin.otf b/apps/desktop/src/assets/fonts/proxima-nova/Mark-Simonson-Proxima-Nova-Thin.otf similarity index 100% rename from gui/public/fonts/proxima-nova-2/Mark-Simonson-Proxima-Nova-Thin.otf rename to apps/desktop/src/assets/fonts/proxima-nova/Mark-Simonson-Proxima-Nova-Thin.otf diff --git a/apps/desktop/src/assets/fonts/space-mono/SpaceMono-Bold.ttf b/apps/desktop/src/assets/fonts/space-mono/SpaceMono-Bold.ttf new file mode 100644 index 00000000..2c4f2682 Binary files /dev/null and b/apps/desktop/src/assets/fonts/space-mono/SpaceMono-Bold.ttf differ diff --git a/apps/desktop/src/assets/fonts/space-mono/SpaceMono-BoldItalic.ttf b/apps/desktop/src/assets/fonts/space-mono/SpaceMono-BoldItalic.ttf new file mode 100644 index 00000000..94fe471c Binary files /dev/null and b/apps/desktop/src/assets/fonts/space-mono/SpaceMono-BoldItalic.ttf differ diff --git a/apps/desktop/src/assets/fonts/space-mono/SpaceMono-Italic.ttf b/apps/desktop/src/assets/fonts/space-mono/SpaceMono-Italic.ttf new file mode 100644 index 00000000..9bca1029 Binary files /dev/null and b/apps/desktop/src/assets/fonts/space-mono/SpaceMono-Italic.ttf differ diff --git a/apps/desktop/src/assets/fonts/space-mono/SpaceMono-Regular.ttf b/apps/desktop/src/assets/fonts/space-mono/SpaceMono-Regular.ttf new file mode 100644 index 00000000..1cfa3653 Binary files /dev/null and b/apps/desktop/src/assets/fonts/space-mono/SpaceMono-Regular.ttf differ diff --git a/apps/desktop/src/assets/icons/superagilogo.svg b/apps/desktop/src/assets/icons/superagilogo.svg new file mode 100644 index 00000000..0021b31b --- /dev/null +++ b/apps/desktop/src/assets/icons/superagilogo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/desktop/src/assets/images/icons/coder-logo.svg b/apps/desktop/src/assets/images/icons/coder-logo.svg new file mode 100644 index 00000000..a273a6c7 --- /dev/null +++ b/apps/desktop/src/assets/images/icons/coder-logo.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/desktop/src/components/Avatar.tsx b/apps/desktop/src/components/Avatar.tsx new file mode 100644 index 00000000..e1b1668e --- /dev/null +++ b/apps/desktop/src/components/Avatar.tsx @@ -0,0 +1,24 @@ +interface AvatarProps { + initials: string; + color: string; + size?: 'xs' | 'sm' | 'md' | 'lg'; + className?: string; +} + +const sizeClasses = { + xs: 'w-6 h-6 text-[10px]', + sm: 'w-8 h-8 text-xs', + md: 'w-10 h-10 text-sm', + lg: 'w-12 h-12 text-base', +}; + +export default function Avatar({ initials, color, size = 'md', className = '' }: AvatarProps) { + return ( +
+ {initials} +
+ ); +} diff --git a/apps/desktop/src/components/TopBar.tsx b/apps/desktop/src/components/TopBar.tsx new file mode 100644 index 00000000..44057a10 --- /dev/null +++ b/apps/desktop/src/components/TopBar.tsx @@ -0,0 +1,58 @@ +import { Sun, Moon, Monitor } from 'lucide-react'; +import { Tooltip } from 'antd'; +import { useTheme, type ThemeMode } from '../context/ThemeContext'; + +const NEXT_MODE: Record = { light: 'dark', dark: 'system', system: 'light' }; +const MODE_LABEL: Record = { light: 'Light', dark: 'Dark', system: 'System' }; + +function ThemeToggle() { + const { mode, setMode } = useTheme(); + const Icon = mode === 'light' ? Sun : mode === 'dark' ? Moon : Monitor; + return ( + + + + ); +} + +function CoderLogo({ className }: { className?: string }) { + return ( + + + + + + + + + + + + + + + ); +} + +export default function TopBar() { + return ( +
+
+ + SuperCoder +
+
+ +
+
+ ); +} diff --git a/apps/desktop/src/components/agent/AgentConversationItem/AgentConversationItem.module.css b/apps/desktop/src/components/agent/AgentConversationItem/AgentConversationItem.module.css new file mode 100644 index 00000000..fa449ec9 --- /dev/null +++ b/apps/desktop/src/components/agent/AgentConversationItem/AgentConversationItem.module.css @@ -0,0 +1,23 @@ +.item_button { + width: calc(100% - 16px); +} + +.agent_tag { + display: flex; + padding: 2px 4px; + align-items: center; + gap: 6px; + border-radius: 24px; + border: 1px solid #B343CB; + background: linear-gradient(99deg, rgba(179, 67, 203, 0.16) 0%, rgba(112, 149, 235, 0.16) 100%), #FFF; + font-size: 9px; + font-weight: 600; + line-height: normal; +} + +.agent_tag_text { + background: linear-gradient(97deg, #B343CB -3.09%, #7095EB 99.3%); + background-clip: text; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; +} diff --git a/apps/desktop/src/components/agent/AgentConversationItem/types.ts b/apps/desktop/src/components/agent/AgentConversationItem/types.ts new file mode 100644 index 00000000..15c00886 --- /dev/null +++ b/apps/desktop/src/components/agent/AgentConversationItem/types.ts @@ -0,0 +1,7 @@ +import type { Agent } from '../../../types/agent'; + +export interface AgentConversationItemProps { + agent: Agent; + isActive: boolean; + onClick: () => void; +} diff --git a/apps/desktop/src/components/agent/AgentInput/AgentInput.tsx b/apps/desktop/src/components/agent/AgentInput/AgentInput.tsx new file mode 100644 index 00000000..744c3467 --- /dev/null +++ b/apps/desktop/src/components/agent/AgentInput/AgentInput.tsx @@ -0,0 +1,771 @@ +import { useState, useCallback, useMemo, useEffect, useRef } from "react"; +import { open } from "@tauri-apps/plugin-dialog"; +import { invoke } from "@tauri-apps/api/core"; +import { X, FileText, Code, File as FileIcon, Bot, MessageCircle, Map, Plus, MoreHorizontal, CircleDashed } from "lucide-react"; +import { useAppStore } from "@/store"; +import { useAgentSend } from "@/hooks/useAgentSend"; +import { saveDraft, loadDraft, clearDraft } from "@/utils/drafts"; +import { agentTauriService } from "@/services/agentTauriService"; +import InputShell from "@/components/common/InputShell/InputShell"; +import ActionChip from "@/components/common/ActionChip/ActionChip"; +import PermissionSettingsModal from "../PermissionSettingsModal/PermissionSettingsModal"; +import SkillsDialog from "../SkillsDialog/SkillsDialog"; +import SubagentsDialog from "../SubagentsDialog/SubagentsDialog"; +import { Segmented, Progress, Tooltip, Popover } from "antd"; +import { themedMessage } from "@/providers/AntDThemeProvider"; +import type { PermissionLevel, SubagentListEntry } from "@/types/agentContract"; +import type { Attachment } from "@/types/chat"; +import type { WidthTier } from "@/components/common/InputShell/types"; +import ModelPicker from "@/components/agent/ModelPicker/ModelPicker"; +import type { AgentInputProps } from "./types"; + +const PERMISSION_LABELS: Record = { + AutoApproveAll: "Full auto", + ApproveDestructive: "Balanced", + ApproveEverything: "Ask all", +}; + +interface PendingAttachment { + id: string; + file: File; + file_name: string; + uploading: boolean; + error?: string; + uploaded?: Attachment; +} + +function isImage(name: string): boolean { + return /\.(png|jpe?g|gif|webp|svg|bmp|ico)$/i.test(name); +} + +/** Infer an image MIME type from a filename. Files built from raw bytes have an + * empty `type`, which the backend's `image/*` filter would drop. */ +function mimeFromName(name: string): string { + const ext = name.split(".").pop()?.toLowerCase() ?? ""; + const map: Record = { + png: "image/png", + jpg: "image/jpeg", + jpeg: "image/jpeg", + gif: "image/gif", + webp: "image/webp", + svg: "image/svg+xml", + bmp: "image/bmp", + ico: "image/x-icon", + }; + return map[ext] ?? ""; +} + +/** Read a File as a base64 data: URL (local attachments, no upload server). */ +function fileToDataUrl(file: File): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => resolve(reader.result as string); + reader.onerror = () => reject(reader.error); + reader.readAsDataURL(file); + }); +} + +type Mode = "ask" | "plan" | "coding"; + +export default function AgentInput({ sessionId, folderPath, agentName = "the agent" }: AgentInputProps) { + const [text, setText] = useState(""); + const [attachments, setAttachments] = useState([]); + const [mode, setMode] = useState( + () => (useAppStore.getState().sessions.find((s) => s.id === sessionId)?.mode as Mode) ?? "coding", + ); + const [showPermissions, setShowPermissions] = useState(false); + const [showSkills, setShowSkills] = useState(false); + const [showSubagents, setShowSubagents] = useState(false); + const [permissionLevel, setPermissionLevel] = useState("ApproveDestructive"); + const [widthTier, setWidthTier] = useState("wide"); + + const [fileList, setFileList] = useState([]); + const [subagentList, setSubagentList] = useState([]); + const [fileQuery, setFileQuery] = useState(""); + const [filePickerStartIdx, setFilePickerStartIdx] = useState(-1); + const [showFilePicker, setShowFilePicker] = useState(false); + const [selectedFileIdx, setSelectedFileIdx] = useState(0); + const fileDropdownRef = useRef(null); + const textareaRef = useRef(null); + + const SLASH_COMMANDS = useMemo( + () => [ + { name: "clear", description: "Clear LLM context for this session" }, + { name: "compact", description: "Compact LLM context (summarize older messages)" }, + { name: "skills", description: "Manage skills available to the agent" }, + { name: "agents", description: "Manage subagents available to the agent" }, + ], + [], + ); + const [isCompacting, setIsCompacting] = useState(false); + const [showCommandPicker, setShowCommandPicker] = useState(false); + const [commandQuery, setCommandQuery] = useState(""); + const [selectedCommandIdx, setSelectedCommandIdx] = useState(0); + const commandDropdownRef = useRef(null); + + const activeModel = useAppStore((s) => s.selection.active?.model ?? null); + const tokenUsage = useAppStore((s) => s.tokenUsage[sessionId]); + // Vision capability for the active model gates the image-attach affordances. + const supportsImages = useAppStore((s) => s.activeCapability?.supportsImages ?? false); + const [dragActive, setDragActive] = useState(false); + + // Load git-tracked files for @ picker. + useEffect(() => { + if (!folderPath) { + setFileList([]); + return; + } + invoke("git_ls_files", { repoPath: folderPath }) + .then(setFileList) + .catch(() => setFileList([])); + }, [folderPath]); + + // Load enabled subagents. + useEffect(() => { + agentTauriService + .listSubagents(folderPath ?? null) + .then((list) => setSubagentList(list.filter((s) => s.enabled))) + .catch(() => setSubagentList([])); + }, [folderPath]); + + const filteredCommands = useMemo(() => { + if (!showCommandPicker) return []; + if (!commandQuery) return SLASH_COMMANDS; + const q = commandQuery.toLowerCase(); + return SLASH_COMMANDS.filter((c) => c.name.toLowerCase().startsWith(q)); + }, [showCommandPicker, commandQuery, SLASH_COMMANDS]); + + const executeCommand = useCallback( + async (commandName: string) => { + if (isCompacting) return; + setShowCommandPicker(false); + setText(""); + + if (commandName === "skills") { + setShowSkills(true); + return; + } + if (commandName === "agents") { + setShowSubagents(true); + return; + } + + if (commandName === "clear") { + try { + await agentTauriService.clearContext(sessionId); + useAppStore.getState().clearTokenUsage(sessionId); + if (folderPath) useAppStore.getState().clearCompletedPlan(folderPath); + themedMessage.success("Context cleared"); + } catch (err) { + console.error("[AgentInput] Failed to clear context:", err); + themedMessage.error("Failed to clear context"); + } + } else if (commandName === "compact") { + const toastKey = "compact-toast"; + setIsCompacting(true); + themedMessage.loading("Compacting context...", toastKey); + try { + const result = await agentTauriService.compactContext(sessionId); + if (result === "nothing_to_compact") { + themedMessage.info("Not enough context to compact", toastKey); + } else if (result === "truncated") { + themedMessage.warning("Context truncated (summarization failed)", toastKey); + useAppStore.getState().clearTokenUsage(sessionId); + } else { + themedMessage.success("Context compacted", toastKey); + useAppStore.getState().clearTokenUsage(sessionId); + } + } catch (err) { + console.error("[AgentInput] Failed to compact context:", err); + themedMessage.error("Failed to compact context", toastKey); + } finally { + setIsCompacting(false); + } + } + }, + [sessionId, folderPath, isCompacting], + ); + + type PickerItem = + | { kind: "subagent"; name: string; description: string } + | { kind: "file"; path: string }; + + const pickerItems = useMemo(() => { + if (!showFilePicker) return []; + const q = fileQuery.toLowerCase(); + const subs: PickerItem[] = []; + for (const s of subagentList) { + if (!q || s.name.toLowerCase().includes(q)) { + subs.push({ kind: "subagent", name: s.name, description: s.description }); + } + } + const files: PickerItem[] = []; + const fileBudget = Math.max(0, 15 - subs.length); + for (const f of fileList) { + if (files.length >= fileBudget) break; + if (!q || f.toLowerCase().includes(q)) files.push({ kind: "file", path: f }); + } + return [...subs, ...files]; + }, [fileList, subagentList, fileQuery, showFilePicker]); + + useEffect(() => { + if (!showFilePicker) return; + const handler = (e: MouseEvent) => { + if (fileDropdownRef.current && !fileDropdownRef.current.contains(e.target as Node)) { + setShowFilePicker(false); + } + }; + document.addEventListener("mousedown", handler); + return () => document.removeEventListener("mousedown", handler); + }, [showFilePicker]); + + // Load persisted context usage on session/model change. + useEffect(() => { + useAppStore.getState().clearTokenUsage(sessionId); + agentTauriService + .getContextUsage(sessionId) + .then((usage) => { + if (usage) useAppStore.getState().setTokenUsage(sessionId, usage.total_tokens, usage.context_limit); + }) + .catch(() => {}); + }, [sessionId, activeModel]); + + useEffect(() => { + if (!showCommandPicker) return; + const handler = (e: MouseEvent) => { + if (commandDropdownRef.current && !commandDropdownRef.current.contains(e.target as Node)) { + setShowCommandPicker(false); + } + }; + document.addEventListener("mousedown", handler); + return () => document.removeEventListener("mousedown", handler); + }, [showCommandPicker]); + + const { send, cancel, isBusy, isSending } = useAgentSend({ sessionId }); + + // Draft + mode keyed by session. + useEffect(() => { + const draft = loadDraft(sessionId); + setText(draft?.text ?? ""); + const sessionMode = useAppStore.getState().sessions.find((s) => s.id === sessionId)?.mode as Mode | undefined; + if (sessionMode) setMode(sessionMode); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [sessionId]); + + const handleModeChange = useCallback( + (m: Mode) => { + setMode(m); + useAppStore.getState().setSessionMode(sessionId, m); + }, + [sessionId], + ); + + const draftTimerRef = useRef>(); + const handleTextChange = useCallback( + (newText: string, cursorPos?: number) => { + setText(newText); + + const slashMatch = newText.match(/^\/(\S*)$/); + if (slashMatch) { + setCommandQuery(slashMatch[1]); + setShowCommandPicker(true); + setSelectedCommandIdx(0); + } else { + setShowCommandPicker(false); + } + + if (fileList.length > 0 || subagentList.length > 0) { + const pos = cursorPos ?? newText.length; + const before = newText.slice(0, pos); + const atMatch = before.match(/@([^\s]*)$/); + if (atMatch) { + setFileQuery(atMatch[1]); + setFilePickerStartIdx(pos - atMatch[0].length); + setShowFilePicker(true); + setSelectedFileIdx(0); + } else { + setShowFilePicker(false); + } + } + + if (draftTimerRef.current) clearTimeout(draftTimerRef.current); + draftTimerRef.current = setTimeout(() => saveDraft(sessionId, newText), 1000); + }, + [sessionId, fileList.length, subagentList.length], + ); + + const isUploading = attachments.some((a) => a.uploading); + + const handleSend = useCallback(async () => { + const trimmed = text.trim(); + const uploadedAtts = attachments.filter((a) => a.uploaded).map((a) => a.uploaded!); + if ((!trimmed && uploadedAtts.length === 0) || isSending || isUploading) return; + setText(""); + setAttachments([]); + if (draftTimerRef.current) clearTimeout(draftTimerRef.current); + clearDraft(sessionId); + await send(trimmed, mode, uploadedAtts.length > 0 ? uploadedAtts : undefined); + }, [text, attachments, isSending, isUploading, send, sessionId, mode]); + + const handleCancel = useCallback(async () => { + await cancel(); + }, [cancel]); + + // Convert a local file to a data: URL attachment (no upload server). + const trackAttachment = useCallback(async (file: File) => { + const id = `att-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`; + const mediaType = file.type || "application/octet-stream"; + setAttachments((prev) => [...prev, { id, file, file_name: file.name, uploading: true }]); + try { + const dataUrl = await fileToDataUrl(file); + setAttachments((prev) => + prev.map((a) => + a.id === id + ? { + ...a, + uploading: false, + uploaded: { url: dataUrl, file_name: file.name, media_type: mediaType }, + } + : a, + ), + ); + } catch { + setAttachments((prev) => prev.map((a) => (a.id === id ? { ...a, uploading: false, error: "Failed to read file" } : a))); + } + }, []); + + const handleUploadClick = useCallback(async () => { + const selected = await open({ + directory: false, + multiple: true, + title: "Select images", + // Only images are supported (the backend forwards image/* attachments only). + filters: [{ name: "Images", extensions: ["png", "jpg", "jpeg", "gif", "webp", "svg", "bmp", "ico"] }], + }); + if (!selected) return; + const paths = Array.isArray(selected) ? selected : [selected]; + for (const filePath of paths) { + const fileName = filePath.split("/").pop() || filePath; + // Guard against a non-image slipping through (e.g. the OS "All Files" override). + if (!isImage(fileName)) continue; + try { + const bytes = await invoke("read_file_bytes", { path: filePath }).catch(() => null); + if (bytes) { + // NOTE: File created from raw bytes has no MIME type, so file.type is "". + // Infer it from the extension so the backend's image/* filter keeps it. + const file = new File([new Uint8Array(bytes)], fileName, { type: mimeFromName(fileName) }); + await trackAttachment(file); + } + } catch { + /* ignore */ + } + } + }, [trackAttachment]); + + const removeAttachment = (id: string) => setAttachments((prev) => prev.filter((a) => a.id !== id)); + + const handlePaste = useCallback( + (e: React.ClipboardEvent) => { + // Only intercept pasted images when the active model supports vision. + if (!supportsImages) return; + const items = e.clipboardData?.items; + if (!items) return; + for (let i = 0; i < items.length; i++) { + const item = items[i]; + if (item.type.startsWith("image/")) { + e.preventDefault(); + const file = item.getAsFile(); + if (!file) continue; + const ext = item.type.split("/")[1] || "png"; + trackAttachment(new File([file], `pasted-image-${Date.now()}.${ext}`, { type: file.type })); + return; + } + } + }, + [trackAttachment, supportsImages], + ); + + const handleDrop = useCallback( + (e: React.DragEvent) => { + e.preventDefault(); + setDragActive(false); + if (!supportsImages) return; + const files = Array.from(e.dataTransfer?.files ?? []); + for (const file of files) { + if (file.type.startsWith("image/") || isImage(file.name)) trackAttachment(file); + } + }, + [trackAttachment, supportsImages], + ); + + const handleDragOver = useCallback( + (e: React.DragEvent) => { + if (!supportsImages) return; + e.preventDefault(); + setDragActive(true); + }, + [supportsImages], + ); + + const handleDragLeave = useCallback((e: React.DragEvent) => { + e.preventDefault(); + setDragActive(false); + }, []); + + const insertPickerItem = useCallback( + (item: PickerItem) => { + const before = text.slice(0, filePickerStartIdx); + const after = text.slice(filePickerStartIdx + 1 + fileQuery.length); + const insert = item.kind === "subagent" ? `@${item.name}` : item.path; + setText(`${before}${insert} ${after}`); + setShowFilePicker(false); + requestAnimationFrame(() => textareaRef.current?.focus()); + }, + [text, filePickerStartIdx, fileQuery], + ); + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (showCommandPicker && filteredCommands.length > 0) { + switch (e.key) { + case "ArrowDown": + e.preventDefault(); + setSelectedCommandIdx((p) => Math.min(p + 1, filteredCommands.length - 1)); + return; + case "ArrowUp": + e.preventDefault(); + setSelectedCommandIdx((p) => Math.max(p - 1, 0)); + return; + case "Tab": + case "Enter": + e.preventDefault(); + executeCommand(filteredCommands[selectedCommandIdx].name); + return; + case "Escape": + e.preventDefault(); + setShowCommandPicker(false); + return; + } + } + + if (showFilePicker && pickerItems.length > 0) { + switch (e.key) { + case "ArrowDown": + e.preventDefault(); + setSelectedFileIdx((p) => Math.min(p + 1, pickerItems.length - 1)); + return; + case "ArrowUp": + e.preventDefault(); + setSelectedFileIdx((p) => Math.max(p - 1, 0)); + return; + case "Tab": + case "Enter": + e.preventDefault(); + insertPickerItem(pickerItems[selectedFileIdx]); + return; + case "Escape": + e.preventDefault(); + setShowFilePicker(false); + return; + } + } else if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault(); + handleSend(); + } + }; + + const loadPermissionLevel = useCallback(async () => { + try { + const config = await agentTauriService.getPermissions(folderPath ?? undefined); + setPermissionLevel(config.level as PermissionLevel); + } catch { + /* fallback */ + } + }, [folderPath]); + + useEffect(() => { + loadPermissionLevel(); + }, [loadPermissionLevel]); + + const filePickerDropdown = + showFilePicker && pickerItems.length > 0 ? ( +
+ {pickerItems.map((item, i) => { + const active = i === selectedFileIdx; + const className = `w-full flex items-center gap-2 px-3 py-1.5 text-left text-sm transition-colors ${ + active ? "bg-blue-50 dark:bg-blue-900/20" : "hover:bg-[var(--hover-bg)]" + }`; + if (item.kind === "subagent") { + return ( + + ); + } + const fileName = item.path.split("/").pop() ?? item.path; + const dirPath = item.path.includes("/") ? item.path.slice(0, item.path.lastIndexOf("/")) : ""; + return ( + + ); + })} +
+ ) : null; + + const commandPickerDropdown = + showCommandPicker && filteredCommands.length > 0 ? ( +
+ {filteredCommands.map((cmd, i) => ( + + ))} +
+ ) : null; + + const permissionLabel = PERMISSION_LABELS[permissionLevel] ?? "Balanced"; + const isNarrow = widthTier === "narrow"; + const isMedium = widthTier === "medium"; + + const contextIndicator = (() => { + if (!tokenUsage) return null; + const { totalTokens, contextLimit } = tokenUsage; + if (!totalTokens) return null; + const fmt = (n: number) => (n >= 1_000_000 ? `${(n / 1_000_000).toFixed(1)}M` : n >= 1000 ? `${(n / 1000).toFixed(0)}k` : String(n)); + // Unknown context window → dashed ring + raw token count (no gauge, no %). + if (!contextLimit || contextLimit === 0) { + return ( + +
+ + {fmt(totalTokens)} +
+
+ ); + } + const rawPercent = (totalTokens / contextLimit) * 100; + const usagePercent = rawPercent > 0 && rawPercent < 1 ? 1 : Math.round(rawPercent); + const displayPercent = rawPercent > 0 && rawPercent < 10 ? rawPercent.toFixed(1) : String(usagePercent); + const usageColor = usagePercent > 80 ? "#f5222d" : usagePercent > 60 ? "#faad14" : "#52c41a"; + return ( + +
+ + {displayPercent}% +
+
+ ); + })(); + + // Image attach is the only attachment the backend forwards, so the upload + // affordance is shown only when the active model supports vision. + const uploadButton = supportsImages ? ( + + ) : null; + + const permissionChipFull = ( + } + onClick={() => setShowPermissions(true)} + /> + ); + + const permissionChipIcon = ( + + ); + + const modeSegmented = ( + {!isNarrow && Ask}, value: "Ask" }, + { label: {!isNarrow && Plan}, value: "Plan" }, + { label: {!isNarrow && Code}, value: "Code" }, + ]} + onChange={(val) => handleModeChange(val === "Plan" ? "plan" : val === "Code" ? "coding" : "ask")} + style={{ fontSize: 12, backgroundColor: "var(--white-opacity-10)" }} + /> + ); + + // Narrow tier: model picker, permission, and context usage collapse into a "…" menu. + const overflowMenu = ( + + + {permissionChipFull} + {contextIndicator ?? ( + No context usage yet + )} + + } + > + + + ); + + const toolbarLeftNode = isNarrow ? ( + <> + {uploadButton} + {overflowMenu} + + ) : ( + <> + {uploadButton} + + {isMedium ? permissionChipIcon : permissionChipFull} + + ); + + const toolbarRightNode = ( +
+ {!isNarrow && contextIndicator} + {modeSegmented} +
+ ); + + const attachmentPreviews = + attachments.length > 0 ? ( +
+ {attachments.map((att) => ( +
+ {isImage(att.file_name) && att.file.size > 0 ? ( + {att.file_name} + ) : ( + + )} +
+ {att.file_name} + {att.uploading && Reading...} + {att.error && {att.error}} +
+ +
+ ))} +
+ ) : null; + + return ( + <> +
+ {dragActive && ( +
+ Drop image to attach +
+ )} + handleTextChange(e.target.value, e.target.selectionStart ?? undefined)} + textareaRef={textareaRef} + onKeyDown={handleKeyDown} + onPaste={handlePaste} + placeholder={`Ask ${agentName} to do something...`} + onSend={isBusy ? handleCancel : handleSend} + sendDisabled={ + isBusy ? false : (!text.trim() && attachments.every((a) => !a.uploaded)) || isSending || isUploading || isCompacting + } + isStop={isBusy} + onWidthChange={setWidthTier} + innerContent={ + <> + {commandPickerDropdown} + {filePickerDropdown} + {attachmentPreviews} + + } + toolbarRight={toolbarRightNode} + toolbarLeft={toolbarLeftNode} + /> +
+ { + setShowPermissions(false); + loadPermissionLevel(); + }} + projectPath={folderPath} + /> + setShowSkills(false)} workingDir={folderPath} /> + setShowSubagents(false)} workingDir={folderPath} /> + + ); +} diff --git a/apps/desktop/src/components/agent/AgentInput/types.ts b/apps/desktop/src/components/agent/AgentInput/types.ts new file mode 100644 index 00000000..6703ec05 --- /dev/null +++ b/apps/desktop/src/components/agent/AgentInput/types.ts @@ -0,0 +1,8 @@ +export interface AgentInputProps { + /** The session this composer sends into. */ + sessionId: string; + /** The session's project folder (used for @file/skills/subagents lookup). */ + folderPath: string | null; + /** Display name for the placeholder. */ + agentName?: string; +} diff --git a/apps/desktop/src/components/agent/AgentMessageBubble/AgentMessageBubble.tsx b/apps/desktop/src/components/agent/AgentMessageBubble/AgentMessageBubble.tsx new file mode 100644 index 00000000..91a359cf --- /dev/null +++ b/apps/desktop/src/components/agent/AgentMessageBubble/AgentMessageBubble.tsx @@ -0,0 +1,97 @@ +import { useState } from "react"; +import Markdown from "../../common/Markdown"; +import ImageLightbox from "./ImageLightbox"; +import AgentThinkingCollapsible from "../AgentThinkingCollapsible/AgentThinkingCollapsible"; +import RewindDropdown, { type RewindAgentProps } from "../AgentThreadPanel/RewindDropdown"; +import type { AgentMessage } from "@/types/agent"; +import thinkingStyles from "../AgentThinking/AgentThinking.module.css"; + +interface Props { + message: AgentMessage; + onRewindAgent?: RewindAgentProps; +} + +export default function AgentMessageBubble({ message, onRewindAgent }: Props) { + const [hovered, setHovered] = useState(false); + const [preview, setPreview] = useState(null); + const isUser = message.role === "user"; + const isStreaming = message.id.endsWith("-streaming"); + + // ── Streaming placeholders (agent "thinking") ────────────────────────── + if (message.text === "__thinking__") { + return ( +
+ Thinking +
+ ); + } + if (message.text?.toLowerCase().startsWith("__thinking_subagent:")) { + const name = message.text.slice("__thinking_subagent:".length, -2).trim() || "..."; + return ( +
+ Running subagent {name} +
+ ); + } + + // ── User → right-aligned bubble (iMessage style) ─────────────────────── + if (isUser) { + const images = message.images ?? []; + return ( +
setHovered(true)} + onMouseLeave={() => setHovered(false)} + > + {hovered && onRewindAgent && ( +
+ +
+ )} +
+ {images.length > 0 && ( +
+ {images.map((src, i) => ( + {`attachment setPreview(src)} + className="max-h-48 max-w-full rounded-xl border border-gray-200 dark:border-dark-border cursor-zoom-in" + /> + ))} +
+ )} + {message.text && ( +
+ {message.text} +
+ )} +
+ {preview && setPreview(null)} />} +
+ ); + } + + // ── Agent → plain left-aligned text (no avatar / name) ───────────────── + // Thinking chips come from message.thinking — set live from streaming tool + // events, and reconstructed from the SQLite tool_call rows on reload. + const thinkingToolCalls = message.thinking?.toolCalls ?? []; + const thinkingDuration = message.thinking?.durationSeconds ?? 0; + const bodyText = message.text; + + return ( +
+ {thinkingToolCalls.length > 0 && ( + + )} + {bodyText && ( + + {bodyText} + + )} +
+ ); +} diff --git a/apps/desktop/src/components/agent/AgentMessageBubble/ImageLightbox.tsx b/apps/desktop/src/components/agent/AgentMessageBubble/ImageLightbox.tsx new file mode 100644 index 00000000..a36e7c4d --- /dev/null +++ b/apps/desktop/src/components/agent/AgentMessageBubble/ImageLightbox.tsx @@ -0,0 +1,43 @@ +import { useEffect } from "react"; +import { createPortal } from "react-dom"; +import { X } from "lucide-react"; + +interface Props { + src: string; + onClose: () => void; +} + +/** Minimal image preview: full-screen image on a dim backdrop. Click anywhere + * or press Esc to close. No toolbar, no zoom controls. */ +export default function ImageLightbox({ src, onClose }: Props) { + useEffect(() => { + const onKey = (e: KeyboardEvent) => { + if (e.key === "Escape") onClose(); + }; + window.addEventListener("keydown", onKey); + return () => window.removeEventListener("keydown", onKey); + }, [onClose]); + + return createPortal( +
+ + preview e.stopPropagation()} + /> +
, + document.body, + ); +} diff --git a/apps/desktop/src/components/agent/AgentThinking/AgentThinking.module.css b/apps/desktop/src/components/agent/AgentThinking/AgentThinking.module.css new file mode 100644 index 00000000..eaa27b4c --- /dev/null +++ b/apps/desktop/src/components/agent/AgentThinking/AgentThinking.module.css @@ -0,0 +1,45 @@ +.thinking_text { + font-size: 14px; + font-weight: 500; + line-height: 17px; + background: linear-gradient( + 90deg, + var(--text-secondary, #6b7280) 0%, + var(--text-secondary, #6b7280) 35%, + var(--text-primary, #111827) 50%, + var(--text-secondary, #6b7280) 65%, + var(--text-secondary, #6b7280) 100% + ); + background-size: 200% 100%; + background-clip: text; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + animation: thinking_shimmer 2s linear infinite; +} + +.thinking_text::after { + content: "."; + -webkit-text-fill-color: var(--text-secondary, #6b7280); + animation: thinking_dots 1.5s steps(1) infinite; +} + +@keyframes thinking_shimmer { + 0% { + background-position: 200% 0; + } + 100% { + background-position: -200% 0; + } +} + +@keyframes thinking_dots { + 0% { + content: "."; + } + 33% { + content: ".."; + } + 66% { + content: "..."; + } +} diff --git a/apps/desktop/src/components/agent/AgentThinkingCollapsible/AgentThinkingCollapsible.tsx b/apps/desktop/src/components/agent/AgentThinkingCollapsible/AgentThinkingCollapsible.tsx new file mode 100644 index 00000000..4224c7c6 --- /dev/null +++ b/apps/desktop/src/components/agent/AgentThinkingCollapsible/AgentThinkingCollapsible.tsx @@ -0,0 +1,229 @@ +import { useMemo } from "react"; +import { Collapse } from "antd"; +import { Loader2, Check, X } from "lucide-react"; +import type { AgentThinkingCollapsibleProps } from "./types"; + +function formatDuration(seconds: number): string { + if (seconds < 60) return `${seconds} seconds`; + const mins = Math.floor(seconds / 60); + const secs = seconds % 60; + if (mins < 60) { + return secs > 0 ? `${mins}m ${secs}s` : `${mins} minutes`; + } + const hrs = Math.floor(mins / 60); + const remainMins = mins % 60; + return remainMins > 0 ? `${hrs}h ${remainMins}m` : `${hrs} hours`; +} + +function cleanArgsSummary(toolName: string, raw: string): string { + // Persisted subagent thinking step — fallback called cleanArgsSummary(toolName, toolName). + // Chip label already shows "Subagent: "; return empty to avoid the + // duplicated "Subagent: code-explorer Subagent: code-explorer" render. + if (raw === toolName && toolName.startsWith("Subagent: ")) { + return ""; + } + const trimmed = raw.trim(); + + // Context-engine tools: the chip label already says "Semantic search" / + // "Querying graph", so never echo the bare tool name. Pull a human summary + // from the args — a search/graph query, else a graph lookup target + // (function_name, qualified by query_type), else a file path. When there's + // nothing useful (or args are just the tool name), show only the label. + if (toolName === "codebase_search" || toolName === "codebase_graph") { + const q = trimmed.match(/"query"\s*:\s*"([^"]+)"/); + if (q) return q[1]; + const fn = trimmed.match(/"function_name"\s*:\s*"([^"]+)"/); + if (fn) { + const qt = trimmed.match(/"query_type"\s*:\s*"([^"]+)"/); + return qt ? `${qt[1].replace(/_/g, " ")} of ${fn[1]}` : fn[1]; + } + const fp = trimmed.match(/"file_path"\s*:\s*"([^"]+)"/); + if (fp) return fp[1]; + // Bare tool name or unparseable JSON args → label only. + return raw === toolName || trimmed.startsWith("{") ? "" : raw; + } + + if (trimmed.startsWith("{") || trimmed.startsWith("[") || trimmed.startsWith('"')) { + // TodoWrite — extract first todo content via regex (JSON is often truncated) + if (toolName === "TodoWrite" || trimmed.includes('"todos"')) { + const match = trimmed.match(/"content"\s*:\s*"([^"]+)"/); + if (match) return `Updating todos: ${match[1]}`; + return "Updating todos"; + } + if (toolName === "TodoRead") return "Reading todos"; + return toolName.startsWith("{") ? "Processing..." : toolName; + } + return raw; +} + +const TOOL_COLORS: Record = { + Read: { text: '#3b82f6', bg: 'rgba(59,130,246,0.12)' }, + Glob: { text: '#3b82f6', bg: 'rgba(59,130,246,0.12)' }, + Grep: { text: '#3b82f6', bg: 'rgba(59,130,246,0.12)' }, + Search: { text: '#3b82f6', bg: 'rgba(59,130,246,0.12)' }, + codebase_search: { text: '#06b6d4', bg: 'rgba(6,182,212,0.12)' }, + codebase_graph: { text: '#06b6d4', bg: 'rgba(6,182,212,0.12)' }, + Writ: { text: '#f59e0b', bg: 'rgba(245,158,11,0.12)' }, + Edit: { text: '#f59e0b', bg: 'rgba(245,158,11,0.12)' }, + Bash: { text: '#22c55e', bg: 'rgba(34,197,94,0.12)' }, + Run: { text: '#22c55e', bg: 'rgba(34,197,94,0.12)' }, + Check: { text: '#22c55e', bg: 'rgba(34,197,94,0.12)' }, + Review: { text: '#22c55e', bg: 'rgba(34,197,94,0.12)' }, + Show: { text: '#22c55e', bg: 'rgba(34,197,94,0.12)' }, + Todo: { text: '#a855f7', bg: 'rgba(168,85,247,0.12)' }, + Updating: { text: '#a855f7', bg: 'rgba(168,85,247,0.12)' }, + Sav: { text: '#ec4899', bg: 'rgba(236,72,153,0.12)' }, + Saving: { text: '#ec4899', bg: 'rgba(236,72,153,0.12)' }, + skill: { text: '#0891b2', bg: 'rgba(8,145,178,0.12)' }, + subagent: { text: '#a855f7', bg: 'rgba(168,85,247,0.12)' }, +}; + +const DEFAULT_TAG_STYLE = { text: 'var(--text-secondary)', bg: 'var(--bg-secondary)' }; + +// Friendly display labels for snake_case tool names +const TOOL_LABELS: Record = { + codebase_search: 'Semantic search', + codebase_graph: 'Querying graph', + skill: 'Skill', +}; + +// Multi-word prefix matchers — used when toolName comes from persisted thinking +// text (e.g. "Semantic search: main entry point...") instead of structured live +// events. Order matters: more specific prefixes must come before generic ones +// (e.g. "Reading todos" before "Reading"). Both current and legacy phrasings +// are listed so old persisted messages keep their colored chips. +const PREFIX_MATCHERS: Array<{ prefix: string; label: string; colorKey: string }> = [ + // codebase tools (cyan) + { prefix: 'Semantic search', label: 'Semantic search', colorKey: 'codebase_search' }, + { prefix: 'Searching codebase', label: 'Semantic search', colorKey: 'codebase_search' }, // legacy + { prefix: 'Querying graph', label: 'Querying graph', colorKey: 'codebase_graph' }, + // skills (teal) + { prefix: 'Loading skill', label: 'Loading skill', colorKey: 'skill' }, + // todos (purple) — must precede "Reading"/"Updating" + { prefix: 'Reading todos', label: 'Reading todos', colorKey: 'Todo' }, + { prefix: 'Updating todos', label: 'Updating todos', colorKey: 'Todo' }, + // plan (pink/orange) + { prefix: 'Saving plan', label: 'Saving plan', colorKey: 'Sav' }, + { prefix: 'Editing plan', label: 'Editing plan', colorKey: 'Edit' }, + // file ops + { prefix: 'Reading', label: 'Reading', colorKey: 'Read' }, + { prefix: 'Writing', label: 'Writing', colorKey: 'Writ' }, + { prefix: 'Editing', label: 'Editing', colorKey: 'Edit' }, + { prefix: 'Searching for', label: 'Searching', colorKey: 'Search' }, + // session/PR + { prefix: 'Starting session', label: 'Starting', colorKey: 'Run' }, + { prefix: 'Creating PR', label: 'Creating PR', colorKey: 'Run' }, +]; + +/** Extract short label + tag style from toolName (which may be "Reading services/foo.go") */ +function getToolLabel(toolName: string): { label: string; style: { text: string; bg: string } } { + if (toolName.startsWith("{") || toolName.startsWith("[")) { + return { label: "Tool", style: TOOL_COLORS.Todo }; + } + // "Subagent: " — used for BOTH live events (via agent:subagent_start + // → addToolCall with toolName="Subagent: ") and persisted thinking + // markers (step text baked in by the Rust relay at SubagentStart). Both + // paths share this format so the chip renders identically. + if (toolName.startsWith("Subagent: ")) { + const agentName = toolName.slice("Subagent: ".length); + return { label: `Subagent: ${agentName}`, style: TOOL_COLORS.subagent }; + } + // Direct match (e.g. "Read", "Edit", "Bash", "codebase_search") — live streaming + if (TOOL_COLORS[toolName]) { + const label = TOOL_LABELS[toolName] ?? toolName; + return { label, style: TOOL_COLORS[toolName] }; + } + // Multi-word prefix match — persisted thinking text path + for (const { prefix, label, colorKey } of PREFIX_MATCHERS) { + if (toolName.startsWith(prefix)) { + return { label, style: TOOL_COLORS[colorKey] ?? DEFAULT_TAG_STYLE }; + } + } + // Last-resort first-word fallback (e.g. "Reading" → match "Read") + const firstWord = toolName.split(/\s/)[0]; + const key = Object.keys(TOOL_COLORS).find( + (k) => firstWord.toLowerCase().startsWith(k.toLowerCase()), + ); + if (key) return { label: firstWord, style: TOOL_COLORS[key] }; + return { label: firstWord, style: DEFAULT_TAG_STYLE }; +} + +const STATUS_ICONS: Record = { + running: ( + + ), + success: ( + + ), + error: ( + + ), +}; + +export default function AgentThinkingCollapsible( + props: AgentThinkingCollapsibleProps, +) { + const { toolCalls } = props; + + const elapsedSeconds = useMemo(() => { + if ("durationSeconds" in props && props.durationSeconds !== undefined) { + return props.durationSeconds; + } + if (props.isThinking) return 0; + return Math.round((Date.now() - props.startedAt) / 1000); + }, [props]); + + if (toolCalls.length === 0) return null; + + return ( +
+ + Thought for {formatDuration(elapsedSeconds)} + + ), + children: ( +
+ {toolCalls.map((tc) => { + const { label, style: tagStyle } = getToolLabel(tc.toolName); + const summary = tc.argsSummary + ? cleanArgsSummary(tc.toolName, tc.argsSummary) + : cleanArgsSummary(tc.toolName, tc.toolName); + return ( +
+ {STATUS_ICONS[tc.status] ?? STATUS_ICONS.error} + + {label} + + {summary} + {tc.summary && ( + + {tc.summary} + + )} +
+ ); + })} +
+ ), + }, + ]} + /> +
+ ); +} diff --git a/apps/desktop/src/components/agent/AgentThinkingCollapsible/types.ts b/apps/desktop/src/components/agent/AgentThinkingCollapsible/types.ts new file mode 100644 index 00000000..8168c13e --- /dev/null +++ b/apps/desktop/src/components/agent/AgentThinkingCollapsible/types.ts @@ -0,0 +1,23 @@ +export interface ToolCallItem { + toolCallId: string; + toolName: string; + argsSummary: string; + status: string; + summary?: string; +} + +export interface StreamingProps { + startedAt: number; + isThinking: boolean; + toolCalls: ToolCallItem[]; + durationSeconds?: never; +} + +export interface FinalizedProps { + durationSeconds: number; + toolCalls: ToolCallItem[]; + startedAt?: never; + isThinking?: never; +} + +export type AgentThinkingCollapsibleProps = StreamingProps | FinalizedProps; diff --git a/apps/desktop/src/components/agent/AgentThreadPanel/AgentThreadPanel.module.css b/apps/desktop/src/components/agent/AgentThreadPanel/AgentThreadPanel.module.css new file mode 100644 index 00000000..303755fe --- /dev/null +++ b/apps/desktop/src/components/agent/AgentThreadPanel/AgentThreadPanel.module.css @@ -0,0 +1,87 @@ +.panel { + width: 100%; + min-width: 0; + border-left: 1px solid var(--border-color-8); + display: flex; + flex-direction: column; + height: 100%; + min-height: 0; + /* Responsive side gap: scales with column width, capped at 96px so it + shrinks when the diff panel is open and the chat column is narrow. */ + --side-gap: clamp(20px, 7%, 96px); +} + +.messages { + flex: 1; + min-width: 0; + min-height: 0; + overflow-y: auto; + overflow-x: hidden; + padding: 16px var(--side-gap); + display: flex; + flex-direction: column; + gap: 4px; +} + +.inner { + width: 100%; + margin: 0 auto; + display: flex; + flex-direction: column; + gap: 4px; +} + +.message_group { + display: flex; + flex-direction: column; + min-width: 0; +} + +.artifacts { + display: flex; + flex-direction: column; + gap: 8px; + padding: 0 16px 8px; +} + +.input_area { + /* (side-gap - 20px) here + InputShell's own 20px padding = side-gap, + so the input box left edge lines up with the message text. */ + padding: 4px calc(var(--side-gap) - 20px) 0; + flex-shrink: 0; +} + +.diff_card { + margin-inline: var(--side-gap); +} + +.tool_indicator { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 48px; + color: var(--text-secondary); + font-size: 13px; +} + +.tool_spinner { + width: 14px; + height: 14px; + border: 2px solid var(--border-color-8); + border-top-color: var(--text-secondary); + border-radius: 50%; + animation: spin 0.8s linear infinite; + flex-shrink: 0; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +.tool_text { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} diff --git a/apps/desktop/src/components/agent/AgentThreadPanel/AgentThreadPanel.tsx b/apps/desktop/src/components/agent/AgentThreadPanel/AgentThreadPanel.tsx new file mode 100644 index 00000000..1cbc6c7b --- /dev/null +++ b/apps/desktop/src/components/agent/AgentThreadPanel/AgentThreadPanel.tsx @@ -0,0 +1,381 @@ +import { useCallback, useEffect, useRef, useState } from "react"; +import { Alert, Skeleton } from "antd"; +import { Maximize2, Code } from "lucide-react"; +import { useAppStore } from "@/store"; +import AgentMessageBubble from "../AgentMessageBubble/AgentMessageBubble"; +import ArtifactRenderer from "../artifacts/ArtifactRenderer"; +import AgentInput from "../AgentInput/AgentInput"; +import ApprovalBanner from "./ApprovalBanner"; +import QuestionBanner from "./QuestionBanner"; +import TodoProgress from "./TodoProgress"; +import PlanBanner from "./PlanBanner"; +import RewindEditor from "./RewindEditor"; +import ThreadPanelHeader from "./components/ThreadPanelHeader/ThreadPanelHeader"; +import { buildAgentMessage } from "@/utils/agentMessageAdapter"; +import { agentTauriService } from "@/services/agentTauriService"; +import { createInitialStreamingState } from "@/store/agentSlice"; +import type { AgentMessage } from "@/types/agent"; +import type { TodoItem } from "@/types/agentContract"; +import styles from "./AgentThreadPanel.module.css"; + +const EMPTY_TODOS: TodoItem[] = []; + +/** Pull the media type out of a `data:;base64,…` URL (defaults to png). */ +function dataUrlMediaType(url: string): string { + return /^data:([^;,]+)/.exec(url)?.[1] || "image/png"; +} + +export default function AgentThreadPanel() { + const activeAgentThreadId = useAppStore((s) => s.activeAgentThreadId); + const agentThreads = useAppStore((s) => s.agentThreads); + const expandArtifactDiff = useAppStore((s) => s.expandArtifactDiff); + const pendingApprovals = useAppStore((s) => s.pendingApprovals); + const activeSessionIds = useAppStore((s) => s.activeSessionIds); + const removePendingApproval = useAppStore((s) => s.removePendingApproval); + const pendingQuestion = useAppStore((s) => (activeAgentThreadId ? s.pendingQuestions[activeAgentThreadId] : null)); + const clearPendingQuestion = useAppStore((s) => s.clearPendingQuestion); + const todos = useAppStore((s) => (activeAgentThreadId ? s.agentTodos[activeAgentThreadId] ?? EMPTY_TODOS : EMPTY_TODOS)); + + const streaming = useAppStore((s) => (activeAgentThreadId ? s.agentStreaming[activeAgentThreadId] : null)); + const isLoadingThread = useAppStore((s) => (activeAgentThreadId ? !!s.agentThreadLoading[activeAgentThreadId] : false)); + + const messagesEndRef = useRef(null); + const thread = activeAgentThreadId ? agentThreads[activeAgentThreadId] : null; + + const [submittingId, setSubmittingId] = useState(null); + const [editingMessageId, setEditingMessageId] = useState(null); + const [editingText, setEditingText] = useState(""); + // Image data-URLs kept for the message being edited (user can remove them). + const [editingImages, setEditingImages] = useState([]); + const isRewindingRef = useRef(false); + + const rewindThread = useAppStore((s) => s.rewindThread); + const addMessageToThread = useAppStore((s) => s.addMessageToThread); + const setActiveSession = useAppStore((s) => s.setActiveSession); + const addCheckpoint = useAppStore((s) => s.addCheckpoint); + + const handleStartEdit = useCallback((msgId: string, text: string, images?: string[]) => { + setEditingMessageId(msgId); + setEditingText(text); + setEditingImages(images ?? []); + }, []); + + const handleCancelEdit = useCallback(() => { + setEditingMessageId(null); + setEditingText(""); + setEditingImages([]); + }, []); + + const handleRemoveEditingImage = useCallback((idx: number) => { + setEditingImages((prev) => prev.filter((_, i) => i !== idx)); + }, []); + + const handleRewind = useCallback( + async (restoreCode: boolean) => { + if (!editingMessageId || !activeAgentThreadId || !thread || isRewindingRef.current) return; + const targetIdx = thread.messages.findIndex((m) => m.id === editingMessageId); + if (targetIdx < 0) return; + const newText = editingText.trim(); + const keptImages = editingImages; + // Allow resending with only images (no text), but not a fully empty message. + if (!newText && keptImages.length === 0) return; + + // Backend rewind needs the SQLite row id (numeric). Only available after `done`. + const sqliteId = Number(editingMessageId); + if (!Number.isFinite(sqliteId)) { + console.warn("[AgentThreadPanel] message not yet persisted — cannot rewind"); + return; + } + + const attachments = keptImages.map((url) => ({ + url, + file_name: "image", + media_type: dataUrlMediaType(url), + })); + + isRewindingRef.current = true; + rewindThread(activeAgentThreadId, targetIdx); + addMessageToThread( + activeAgentThreadId, + buildAgentMessage( + `rewind-user-${Date.now()}`, + newText, + "user", + activeAgentThreadId, + "", + undefined, + undefined, + keptImages.length > 0 ? keptImages : undefined, + ), + ); + setEditingMessageId(null); + setEditingText(""); + setEditingImages([]); + useAppStore.getState().setAgentStreaming(activeAgentThreadId, createInitialStreamingState()); + + try { + const { session_id } = await agentTauriService.rewindToMessage( + activeAgentThreadId, + sqliteId, + restoreCode, + newText, + attachments.length > 0 ? attachments : undefined, + ); + setActiveSession(activeAgentThreadId, session_id); + } catch (err) { + console.error("[AgentThreadPanel] Rewind failed:", err); + } finally { + isRewindingRef.current = false; + } + }, + [editingMessageId, editingText, editingImages, activeAgentThreadId, thread, rewindThread, addMessageToThread, setActiveSession], + ); + + const handleApprovalResponse = useCallback( + async (toolCallId: string, approved: boolean) => { + if (!thread || submittingId) return; + setSubmittingId(toolCallId); + try { + const sessionId = activeSessionIds[thread.id] ?? thread.id; + await agentTauriService.approveToolCall(sessionId, toolCallId, approved); + removePendingApproval(toolCallId); + } catch (err) { + console.error("[AgentThreadPanel] approveToolCall failed:", err); + } finally { + setSubmittingId(null); + } + }, + [thread, activeSessionIds, removePendingApproval, submittingId], + ); + + useEffect(() => { + messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); + }, [thread?.messages.length, streaming?.textBuffer, thread?.total_additions, thread?.total_deletions]); + + // Load checkpoints when the thread opens. + useEffect(() => { + if (!activeAgentThreadId) return; + let cancelled = false; + agentTauriService + .listCheckpoints(activeAgentThreadId) + .then((checkpoints) => { + if (!cancelled) checkpoints.forEach((cp) => addCheckpoint(activeAgentThreadId!, cp)); + }) + .catch((err) => console.warn("[AgentThreadPanel] Failed to load checkpoints:", err)); + return () => { + cancelled = true; + }; + }, [activeAgentThreadId, addCheckpoint]); + + // Load persisted token usage on open. + useEffect(() => { + if (!activeAgentThreadId) return; + agentTauriService + .getContextUsage(activeAgentThreadId) + .then((usage) => { + if (usage) useAppStore.getState().setTokenUsage(activeAgentThreadId!, usage.total_tokens, usage.context_limit); + }) + .catch(() => {}); + }, [activeAgentThreadId]); + + // Keep the header / sticky-card diff totals in sync with the real working-tree + // diff (the same source the Code Changes panel uses). Refetch on open, after + // each completed turn (checkpoint added), and when streaming ends. + const checkpointCount = thread?.checkpoints.length ?? 0; + const isStreaming = !!streaming?.isStreaming; + useEffect(() => { + if (!activeAgentThreadId) return; + let cancelled = false; + agentTauriService + .getFullDiff(activeAgentThreadId) + .then((d) => { + if (!cancelled) useAppStore.getState().setThreadDiffStats(activeAgentThreadId!, d.insertions, d.deletions, d.files_changed); + }) + .catch(() => {}); + return () => { + cancelled = true; + }; + }, [activeAgentThreadId, checkpointCount, isStreaming]); + + if (!thread) return null; + + const handleExpandArtifact = (artifactId: string) => expandArtifactDiff(thread.id, artifactId); + + const threadApprovals = Object.values(pendingApprovals).filter((a) => a.threadId === thread.id); + + const streamingMessage: AgentMessage | null = streaming?.textBuffer + ? { + id: `${thread.id}-streaming`, + thread_id: thread.id, + agent_id: "", + role: "agent", + text: streaming.textBuffer, + artifacts: [], + created_at: new Date().toISOString(), + } + : null; + + return ( +
+ 0 || thread.total_deletions > 0 + ? () => expandArtifactDiff(thread.id, `diff-full-${thread.id}`) + : undefined + } + /> + {thread.sourcePlanText && } + {isLoadingThread && thread.messages.length === 0 && ( +
+ {[70, 45, 80, 35, 60].map((width, i) => ( +
+ +
+ +
+
+ ))} +
+ )} +
+
+ {thread.messages.map((msg) => { + const isEditing = msg.id === editingMessageId; + const isUserMsg = msg.role === "user"; + const canRewind = isUserMsg && !streaming?.isStreaming && !isRewindingRef.current && Number.isFinite(Number(msg.id)); + + return ( +
+ {isEditing ? ( + + ) : ( + handleStartEdit(msg.id, msg.text, msg.images), + isCodingSession: !!thread.is_coding_session, + hasCheckpoints: (thread.checkpoints?.length ?? 0) > 1, + isStreaming: !!streaming?.isStreaming, + isRewinding: isRewindingRef.current, + } + : undefined + } + /> + )} + {!isEditing && msg.artifacts.length > 0 && ( +
+ {msg.artifacts.map((artifact) => ( + + ))} +
+ )} +
+ ); + })} + + {streamingMessage && ( +
+ +
+ )} + + {streaming?.error && ( +
+ +
+ )} + + {streaming?.isStreaming && !streaming.textBuffer && (() => { + const runningSubagent = streaming.toolCalls.find( + (tc) => tc.status === "running" && tc.toolName.toLowerCase().startsWith("subagent:"), + ); + const thinkingText = runningSubagent + ? `__thinking_subagent:${runningSubagent.toolName.slice(runningSubagent.toolName.indexOf(":") + 1).trim()}__` + : "__thinking__"; + return ( +
+ +
+ ); + })()} + + {threadApprovals.map((approval) => ( + handleApprovalResponse(approval.toolCallId, true)} + onDeny={() => handleApprovalResponse(approval.toolCallId, false)} + disabled={submittingId === approval.toolCallId} + /> + ))} + + {pendingQuestion && activeAgentThreadId && ( + { + clearPendingQuestion(activeAgentThreadId); + agentTauriService.sendMessage(activeAgentThreadId, answer); + }} + onSkip={() => { + clearPendingQuestion(activeAgentThreadId); + agentTauriService.sendMessage(activeAgentThreadId, "Skip — proceed with your best judgment."); + }} + /> + )} + + {todos.length > 0 && } + +
+
+
+ + {(thread.total_additions > 0 || thread.total_deletions > 0) && (() => { + const fileCount = thread.files_changed ?? new Set(thread.checkpoints?.flatMap((cp) => cp.files ?? []) ?? []).size; + return ( +
expandArtifactDiff(thread.id, `diff-full-${thread.id}`)} + > + + Code Changes + {fileCount} file{fileCount !== 1 ? "s" : ""} + +{thread.total_additions} + -{thread.total_deletions} + +
+ ); + })()} +
+ +
+
+ ); +} diff --git a/apps/desktop/src/components/agent/AgentThreadPanel/ApprovalBanner.module.css b/apps/desktop/src/components/agent/AgentThreadPanel/ApprovalBanner.module.css new file mode 100644 index 00000000..dfee0e73 --- /dev/null +++ b/apps/desktop/src/components/agent/AgentThreadPanel/ApprovalBanner.module.css @@ -0,0 +1,56 @@ +.banner { + margin: 8px 4px; + padding: 12px; + border-radius: 10px; + background: rgba(245, 158, 11, 0.06); + border: 1px solid rgba(245, 158, 11, 0.2); + display: flex; + flex-direction: column; + gap: 8px; +} + +.header { + display: flex; + align-items: center; + gap: 6px; +} + +.label { + font-size: 13px; + font-weight: 600; + color: var(--text-primary); +} + +.description { + font-size: 12px; + color: var(--text-secondary); + line-height: 1.4; + margin: 0; +} + +.command_block { + border-radius: 6px; + background: var(--bg-secondary, #f3f4f6); + border: 1px solid var(--border-color, rgba(0, 0, 0, 0.08)); + overflow: hidden; +} + +.command { + margin: 0; + padding: 8px 10px; + font-size: 12px; + font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', 'Menlo', monospace; + color: var(--text-primary); + white-space: pre-wrap; + word-break: break-word; + line-height: 1.5; + max-height: 120px; + overflow-y: auto; +} + +.actions { + display: flex; + gap: 8px; + justify-content: flex-end; + padding-top: 2px; +} diff --git a/apps/desktop/src/components/agent/AgentThreadPanel/ApprovalBanner.tsx b/apps/desktop/src/components/agent/AgentThreadPanel/ApprovalBanner.tsx new file mode 100644 index 00000000..2c4ffb95 --- /dev/null +++ b/apps/desktop/src/components/agent/AgentThreadPanel/ApprovalBanner.tsx @@ -0,0 +1,153 @@ +import { useState, useEffect } from 'react'; +import { Code } from 'lucide-react'; +import type { PendingApproval } from '@/types/agentContract'; +import type { DiffLine } from '@/types/agent'; +import { generateEditPreviewLines, generateWritePreviewLines } from '@/utils/diffUtils'; +import { agentTauriService } from '@/services/agentTauriService'; + +const TOOL_LABELS: Record = { + bash: 'Run Command', + git: 'Git Operation', + write: 'Write File', + edit: 'Edit File', + grep: 'Search Files', + glob: 'Find Files', + read: 'Read File', + create_pr: 'Create Pull Request', + todo_write: 'Update Todos', + todo_read: 'Read Todos', +}; + +const COLLAPSED_LINES = 20; + +/** Clean up raw JSON strings into human-readable text */ +function cleanDescription(toolName: string, text: string): string { + const trimmed = text.trim(); + if (trimmed.startsWith('{') || trimmed.startsWith('[')) { + if (toolName === 'todo_write' || trimmed.includes('"todos"')) { + const match = trimmed.match(/"content"\s*:\s*"([^"]+)"/); + if (match) return `Updating todos: ${match[1]}`; + return 'Updating agent task list'; + } + if (toolName === 'todo_read') return 'Reading agent task list'; + // Generic JSON — don't show raw + return ''; + } + return text; +} + +/** Extract diff preview lines from tool args */ +function useDiffPreview(approval: PendingApproval): { filePath: string | null; allLines: DiffLine[] } { + const [writeExisting, setWriteExisting] = useState(undefined); + const { toolName, args } = approval; + + const isEdit = toolName === 'edit' && args?.oldString != null && args?.newString != null; + const isWrite = toolName === 'write' && args?.content != null; + const filePath = (args?.filePath as string) ?? null; + + // For write tool, async-load current file content + useEffect(() => { + if (!isWrite || !filePath) return; + let cancelled = false; + agentTauriService.readFileText(filePath).then((content) => { + if (!cancelled) setWriteExisting(content); + }).catch(() => { + if (!cancelled) setWriteExisting(null); + }); + return () => { cancelled = true; }; + }, [isWrite, filePath]); + + if (isEdit) { + return { filePath, allLines: generateEditPreviewLines(args.oldString as string, args.newString as string) }; + } + + if (isWrite && writeExisting !== undefined) { + return { filePath, allLines: generateWritePreviewLines(writeExisting, args.content as string) }; + } + + return { filePath: null, allLines: [] }; +} + +interface ApprovalBannerProps { + approval: PendingApproval; + onApprove: () => void; + onDeny: () => void; + disabled?: boolean; +} + +export default function ApprovalBanner({ approval, onApprove, onDeny, disabled }: ApprovalBannerProps) { + const [expanded, setExpanded] = useState(false); + const toolLabel = TOOL_LABELS[approval.toolName] ?? approval.toolName; + const description = approval.description ? cleanDescription(approval.toolName, approval.description) : ''; + const rawCommand = approval.rawCommand ? cleanDescription(approval.toolName, approval.rawCommand) : ''; + const { filePath, allLines } = useDiffPreview(approval); + const hasDiffPreview = allLines.length > 0; + const isCollapsible = allLines.length > COLLAPSED_LINES; + const visibleLines = expanded ? allLines : allLines.slice(0, COLLAPSED_LINES); + const hiddenCount = allLines.length - visibleLines.length; + + const summaryText = description || rawCommand || ''; + + return ( +
+
+ {/* Header row: tool label + description + actions */} +
+
+ {toolLabel} + {summaryText && ( + · {summaryText} + )} +
+
+ + +
+
+ + {/* Diff preview (only for edit/write tools) */} + {hasDiffPreview && ( +
+ {filePath && ( +
+ + {filePath} +
+ )} +
+ {visibleLines.map((line, i) => ( +
+ + {line.type === 'add' ? '+' : line.type === 'delete' ? '-' : ' '} + + {line.content} +
+ ))} + {isCollapsible && ( + + )} +
+
+ )} +
+
+ ); +} diff --git a/apps/desktop/src/components/agent/AgentThreadPanel/PlanBanner.module.css b/apps/desktop/src/components/agent/AgentThreadPanel/PlanBanner.module.css new file mode 100644 index 00000000..d8700279 --- /dev/null +++ b/apps/desktop/src/components/agent/AgentThreadPanel/PlanBanner.module.css @@ -0,0 +1,42 @@ +.banner { + margin: 8px; + border-radius: 8px; + background: rgba(139, 92, 246, 0.04); + border: 1px solid rgba(139, 92, 246, 0.2); + overflow: hidden; + flex-shrink: 0; +} + +.header { + display: flex; + align-items: center; + gap: 6px; + padding: 8px 12px; + border: none; + background: none; + cursor: pointer; + width: 100%; +} + +.chevron { + font-size: 10px; + color: var(--text-tertiary, #9ca3af); +} + +.title { + font-size: 12px; + font-weight: 600; + color: var(--text-primary); +} + +.body { + padding: 0 12px 10px; + max-height: 400px; + overflow-y: auto; +} + +.markdown { + font-size: 13px; + color: var(--text-primary); + line-height: 1.6; +} diff --git a/apps/desktop/src/components/agent/AgentThreadPanel/PlanBanner.tsx b/apps/desktop/src/components/agent/AgentThreadPanel/PlanBanner.tsx new file mode 100644 index 00000000..6d65a5c6 --- /dev/null +++ b/apps/desktop/src/components/agent/AgentThreadPanel/PlanBanner.tsx @@ -0,0 +1,25 @@ +import { useState } from 'react'; +import Markdown from '../../common/Markdown'; +import styles from './PlanBanner.module.css'; + +interface PlanBannerProps { + planText: string; +} + +export default function PlanBanner({ planText }: PlanBannerProps) { + const [expanded, setExpanded] = useState(false); + + return ( +
+ + {expanded && ( +
+ {planText} +
+ )} +
+ ); +} diff --git a/apps/desktop/src/components/agent/AgentThreadPanel/QuestionBanner.module.css b/apps/desktop/src/components/agent/AgentThreadPanel/QuestionBanner.module.css new file mode 100644 index 00000000..5e22aa39 --- /dev/null +++ b/apps/desktop/src/components/agent/AgentThreadPanel/QuestionBanner.module.css @@ -0,0 +1,69 @@ +.banner { + margin: 8px 4px; + padding: 12px; + border-radius: 10px; + background: rgba(59, 130, 246, 0.06); + border: 1px solid rgba(59, 130, 246, 0.2); + display: flex; + flex-direction: column; + gap: 8px; +} + +.label { + font-size: 13px; + font-weight: 600; + color: var(--text-primary); +} + +.question { + font-size: 13px; + color: var(--text-primary); + line-height: 1.5; + margin: 0; +} + +.options { + display: flex; + flex-direction: column; + gap: 4px; +} + +.option { + display: flex; + align-items: center; + gap: 8px; + font-size: 13px; + color: var(--text-primary); + cursor: pointer; + padding: 4px 0; +} + +.option input[type="radio"] { + accent-color: #3b82f6; +} + +.input { + width: 100%; + padding: 8px 10px; + font-size: 13px; + border-radius: 6px; + border: 1px solid var(--white-opacity-8); + background: var(--white-opacity-4); + color: var(--text-primary); + outline: none; +} + +.input:focus { + border-color: rgba(59, 130, 246, 0.5); +} + +.input::placeholder { + color: var(--white-opacity-20); +} + +.actions { + display: flex; + gap: 8px; + justify-content: flex-end; + padding-top: 2px; +} diff --git a/apps/desktop/src/components/agent/AgentThreadPanel/QuestionBanner.tsx b/apps/desktop/src/components/agent/AgentThreadPanel/QuestionBanner.tsx new file mode 100644 index 00000000..c7b45cf3 --- /dev/null +++ b/apps/desktop/src/components/agent/AgentThreadPanel/QuestionBanner.tsx @@ -0,0 +1,98 @@ +import { useState } from 'react'; +import { Alert } from 'antd'; + +interface QuestionBannerProps { + question: string; + options?: string[]; + onAnswer: (answer: string) => void; + onSkip: () => void; + disabled?: boolean; +} + +export default function QuestionBanner({ question, options, onAnswer, onSkip, disabled }: QuestionBannerProps) { + const [selectedOption, setSelectedOption] = useState(null); + const [customText, setCustomText] = useState(''); + + const handleSubmit = () => { + const answer = selectedOption || customText.trim(); + if (answer) { + onAnswer(answer); + } + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + handleSubmit(); + } + }; + + return ( +
+ +

+ {question} +

+ + {options && options.length > 0 && ( +
+ {options.map((opt) => ( + + ))} +
+ )} + + { setCustomText(e.target.value); setSelectedOption(null); }} + onKeyDown={handleKeyDown} + disabled={disabled} + /> + +
+ + +
+
+ } + /> +
+ ); +} diff --git a/apps/desktop/src/components/agent/AgentThreadPanel/RewindDropdown.tsx b/apps/desktop/src/components/agent/AgentThreadPanel/RewindDropdown.tsx new file mode 100644 index 00000000..f0c088db --- /dev/null +++ b/apps/desktop/src/components/agent/AgentThreadPanel/RewindDropdown.tsx @@ -0,0 +1,34 @@ +import { Pencil } from "lucide-react"; + +export interface RewindAgentProps { + onEditAndResend?: () => void; + isCodingSession: boolean; + hasCheckpoints: boolean; + isStreaming: boolean; + isRewinding: boolean; +} + +interface RewindDropdownProps extends RewindAgentProps { + onPopupChange?: (open: boolean) => void; +} + +export default function RewindDropdown({ + onEditAndResend, + isStreaming, + isRewinding, +}: RewindDropdownProps) { + const disabled = isStreaming || isRewinding; + + if (!onEditAndResend) return null; + + return ( + + ); +} diff --git a/apps/desktop/src/components/agent/AgentThreadPanel/RewindEditor.tsx b/apps/desktop/src/components/agent/AgentThreadPanel/RewindEditor.tsx new file mode 100644 index 00000000..545d15a7 --- /dev/null +++ b/apps/desktop/src/components/agent/AgentThreadPanel/RewindEditor.tsx @@ -0,0 +1,124 @@ +import React, { useRef, useCallback } from "react"; +import { Input, Dropdown } from "antd"; +import { X } from "lucide-react"; +import type { TextAreaRef } from "antd/es/input/TextArea"; +import type { MenuProps } from "antd"; + +const { TextArea } = Input; + +interface RewindEditorProps { + text: string; + /** Image data-URLs attached to the message being edited. */ + images?: string[]; + /** Remove the image at the given index from the resend. */ + onRemoveImage?: (idx: number) => void; + onChange: (text: string) => void; + onCancel: () => void; + onRewind: (restoreCode: boolean) => void; + isCodingSession: boolean; + isRewinding: boolean; +} + +export default function RewindEditor({ + text, + images = [], + onRemoveImage, + onChange, + onCancel, + onRewind, + isCodingSession, + isRewinding, +}: RewindEditorProps) { + const focusedRef = useRef(false); + const handleRef = useCallback((ref: TextAreaRef | null) => { + if (ref && !focusedRef.current) { + focusedRef.current = true; + ref.focus({ cursor: "end" }); + } + }, []); + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Escape") { + onCancel(); + } + }; + + // Resend is allowed with text OR at least one kept image. + const canResend = !!text.trim() || images.length > 0; + + return ( +
+ {images.length > 0 && ( +
+ {images.map((src, i) => ( +
+ {`attachment + {onRemoveImage && !isRewinding && ( + + )} +
+ ))} +
+ )} +