diff --git a/.dockerignore b/.dockerignore index 057b778c..eb3af06a 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,6 +1,9 @@ node_modules dist dist-ssr +root +workspace +.opencode .git .gitignore .vscode diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..b265b021 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[\*] +charset = utf-8 +end_of_line = lf +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/.env.example b/.env.example index 6acfae9a..27fa7ebc 100644 --- a/.env.example +++ b/.env.example @@ -13,8 +13,12 @@ MISTRAL_API_KEY= XAI_API_KEY= # ---- 端口 ---- -FRONTEND_PORT=3000 -API_PORT=4096 +GATEWAY_PORT=6658 +PREVIEW_PORT=6659 + +# ---- 预览域名(绑定独立域名解决前端资源路径问题)---- +# 例如 preview.example.com,Nginx 将此域名转发到 PREVIEW_PORT +PREVIEW_DOMAIN= # ---- 工作目录 ---- WORKSPACE=./workspace @@ -22,3 +26,21 @@ WORKSPACE=./workspace # ---- 安全(公网部署务必设置)---- OPENCODE_SERVER_PASSWORD= OPENCODE_SERVER_USERNAME=opencode + +# ---- 路由服务(由 gateway 内置)---- +PUBLIC_BASE_URL= +ROUTER_SCAN_INTERVAL=5 +ROUTER_TOKEN_LENGTH=12 +ROUTER_PORT_RANGE=3000-9999 +ROUTER_EXCLUDE_PORTS=4096 + +# ---- Backend 工具链镜像/运行时源(可选)---- +USE_CHINA_MIRROR=1 +APT_MIRROR=http://mirrors.tuna.tsinghua.edu.cn/debian +APT_SECURITY_MIRROR=http://mirrors.tuna.tsinghua.edu.cn/debian-security +NPM_CONFIG_REGISTRY=https://registry.npmmirror.com +NODEJS_ORG_MIRROR=https://npmmirror.com/mirrors/node +PIP_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple +PIP_TRUSTED_HOST=pypi.tuna.tsinghua.edu.cn +MISE_INSTALL_URL=https://mise.run +OPENCODE_INSTALL_URL=https://opencode.ai/install diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..7d304a8c --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,32 @@ +name: Build Validation + +on: + pull_request: + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: build-validation-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + validate: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + + - uses: actions/setup-node@v6 + with: + node-version: 22 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Validate app + run: npm run validate diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 00000000..54d7b1a1 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,46 @@ +name: Deploy to GitHub Pages + +on: + push: + branches: [dev] + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - uses: actions/setup-node@v6 + with: + node-version: 22 + cache: npm + + - run: npm ci + + - run: npm run build + env: + VITE_BASE_PATH: /OpenCodeUI/ + + - uses: actions/upload-pages-artifact@v5 + with: + path: dist + + deploy: + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - id: deployment + uses: actions/deploy-pages@v5 diff --git a/.github/workflows/docker-backend.yml b/.github/workflows/docker-backend.yml new file mode 100644 index 00000000..a8b050f1 --- /dev/null +++ b/.github/workflows/docker-backend.yml @@ -0,0 +1,66 @@ +name: Build & Push Backend Docker Image + +on: + push: + branches: [main] + paths: + - 'docker/Dockerfile.backend' + - 'docker/backend-entrypoint.sh' + schedule: + # 每天自动重建,跟进 opencode 最新版本 + - cron: '0 4 * * *' + workflow_dispatch: + +concurrency: + group: docker-backend-${{ github.ref }} + cancel-in-progress: true + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository_owner }}/opencodeui-backend + +jobs: + build-and-push: + runs-on: ubuntu-latest + timeout-minutes: 120 + permissions: + contents: read + packages: write + + steps: + - uses: actions/checkout@v6 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to GHCR + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Docker metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=raw,value=latest,enable={{is_default_branch}} + type=sha,prefix= + type=ref,event=branch + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + file: docker/Dockerfile.backend + platforms: linux/amd64,linux/arm64 + push: true + 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/docker-frontend.yml b/.github/workflows/docker-frontend.yml new file mode 100644 index 00000000..3f0a9e1c --- /dev/null +++ b/.github/workflows/docker-frontend.yml @@ -0,0 +1,71 @@ +name: Build & Push Frontend Docker Image + +on: + push: + branches: [main] + paths: + - 'src/**' + - 'public/**' + - 'index.html' + - 'package.json' + - 'package-lock.json' + - 'vite.config.ts' + - 'tsconfig*.json' + - 'docker/Dockerfile.frontend' + - 'docker/Caddyfile' + - 'docker/Caddyfile.standalone' + workflow_dispatch: + +concurrency: + group: docker-frontend-${{ github.ref }} + cancel-in-progress: true + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository_owner }}/opencodeui-frontend + +jobs: + build-and-push: + runs-on: ubuntu-latest + timeout-minutes: 30 + permissions: + contents: read + packages: write + + steps: + - uses: actions/checkout@v6 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to GHCR + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Docker metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=raw,value=latest,enable={{is_default_branch}} + type=sha,prefix= + type=ref,event=branch + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + file: docker/Dockerfile.frontend + build-args: | + NPM_REGISTRY=https://registry.npmjs.org/ + NPM_REGISTRY_FALLBACK=https://registry.npmmirror.com/ + platforms: linux/amd64,linux/arm64 + push: true + 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/docker-gateway.yml b/.github/workflows/docker-gateway.yml new file mode 100644 index 00000000..5bff2d3f --- /dev/null +++ b/.github/workflows/docker-gateway.yml @@ -0,0 +1,65 @@ +name: Build & Push Gateway Docker Image + +on: + push: + branches: [main] + paths: + - 'docker/Dockerfile.gateway' + - 'docker/Caddyfile.gateway' + - 'docker/entrypoint-gateway.sh' + - 'src-router/**' + workflow_dispatch: + +concurrency: + group: docker-gateway-${{ github.ref }} + cancel-in-progress: true + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository_owner }}/opencodeui-gateway + +jobs: + build-and-push: + runs-on: ubuntu-latest + timeout-minutes: 45 + permissions: + contents: read + packages: write + + steps: + - uses: actions/checkout@v6 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to GHCR + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Docker metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=raw,value=latest,enable={{is_default_branch}} + type=sha,prefix= + type=ref,event=branch + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + file: docker/Dockerfile.gateway + platforms: linux/amd64,linux/arm64 + push: true + 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.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..f1cac982 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,240 @@ +name: Release + +on: + push: + tags: + - 'v*' + +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: true + +jobs: + # ============================================ + # Desktop builds (Windows, macOS, Linux) + # ============================================ + build-desktop: + strategy: + fail-fast: false + matrix: + include: + - platform: macos-latest + args: '--target aarch64-apple-darwin' + arch: aarch64 + os: macos + - platform: macos-latest + args: '--target x86_64-apple-darwin' + arch: x86_64 + os: macos + - platform: ubuntu-22.04 + args: '' + arch: x86_64 + os: linux + - platform: windows-latest + args: '' + arch: x86_64 + os: windows + + runs-on: ${{ matrix.platform }} + steps: + - uses: actions/checkout@v6 + + - uses: actions/setup-node@v6 + with: + node-version: lts/* + cache: 'npm' + + - name: Install Rust stable + uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }} + + - name: Rust cache + uses: swatinem/rust-cache@v2 + with: + workspaces: './src-tauri -> target' + key: ${{ matrix.os }}-${{ matrix.arch }} + + - name: Install Linux dependencies + if: matrix.platform == 'ubuntu-22.04' + run: | + sudo apt-get update + sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf + + - run: npm ci + + # tauri-action 自动执行 beforeBuildCommand (npm run build) + cargo build + - name: Build desktop app + uses: tauri-apps/tauri-action@v0 + with: + args: ${{ matrix.args }} + + - name: Prepare artifacts + shell: bash + run: | + mkdir -p release-artifacts + BUNDLE_DIR="src-tauri/target/release/bundle" + if [[ "${{ matrix.os }}" == "macos" ]]; then + BUNDLE_DIR="src-tauri/target/${{ matrix.arch }}-apple-darwin/release/bundle" + fi + + if [[ "${{ matrix.os }}" == "windows" ]]; then + find "$BUNDLE_DIR/nsis" -name "*.exe" -exec cp {} release-artifacts/ \; 2>/dev/null || true + elif [[ "${{ matrix.os }}" == "macos" ]]; then + find "$BUNDLE_DIR/dmg" -name "*.dmg" -exec cp {} release-artifacts/ \; 2>/dev/null || true + elif [[ "${{ matrix.os }}" == "linux" ]]; then + find "$BUNDLE_DIR/deb" -name "*.deb" -exec cp {} release-artifacts/ \; 2>/dev/null || true + find "$BUNDLE_DIR/appimage" -name "*.AppImage" -exec cp {} release-artifacts/ \; 2>/dev/null || true + fi + + echo "Artifacts:" + ls -R release-artifacts + + - uses: actions/upload-artifact@v7 + with: + name: artifacts-desktop-${{ matrix.os }}-${{ matrix.arch }} + path: release-artifacts + if-no-files-found: warn + retention-days: 1 + + # ============================================ + # Android builds (arm64 + armv7 并行) + # ============================================ + build-android: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - target: aarch64 + rust_target: aarch64-linux-android + label: arm64 + - target: armv7 + rust_target: armv7-linux-androideabi + label: armv7 + steps: + - uses: actions/checkout@v6 + + - uses: actions/setup-node@v6 + with: + node-version: lts/* + cache: 'npm' + + - uses: actions/setup-java@v5 + with: + distribution: 'temurin' + java-version: '17' + + - uses: android-actions/setup-android@v3 + + - run: sdkmanager "ndk;29.0.14206865" + + - name: Install Rust stable + uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.rust_target }} + + - name: Rust cache + uses: swatinem/rust-cache@v2 + with: + workspaces: './src-tauri -> target' + key: android-${{ matrix.target }} + + - name: Gradle cache + uses: actions/cache@v5 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + src-tauri/gen/android/.gradle + key: gradle-${{ matrix.target }}-${{ hashFiles('src-tauri/gen/android/**/*.gradle*', 'src-tauri/gen/android/**/gradle-wrapper.properties') }} + restore-keys: gradle- + + - run: npm ci + + - name: Setup Android signing + run: | + cd src-tauri/gen/android + echo "keyAlias=${{ secrets.ANDROID_KEY_ALIAS }}" > keystore.properties + echo "password=${{ secrets.ANDROID_KEY_PASSWORD }}" >> keystore.properties + base64 -d <<< "${{ secrets.ANDROID_KEY_BASE64 }}" > $RUNNER_TEMP/keystore.jks + echo "storeFile=$RUNNER_TEMP/keystore.jks" >> keystore.properties + + - name: Build and sign APK + run: | + VERSION=${{ github.ref_name }} + BT="$ANDROID_HOME/build-tools/$(ls $ANDROID_HOME/build-tools/ | tail -1)" + + npx tauri android build --apk --target ${{ matrix.target }} + + SIGNED_APK=$(find src-tauri/gen/android/app/build/outputs/apk -path '*/release/*-signed*' -name '*.apk' -type f 2>/dev/null | head -1) + RELEASE_APK=$(find src-tauri/gen/android/app/build/outputs/apk -path '*/release/*.apk' -name '*.apk' -type f 2>/dev/null | head -1) + APK_PATH="${SIGNED_APK:-$RELEASE_APK}" + DEST="OpenCodeUI-${VERSION}-android-${{ matrix.label }}.apk" + + if $BT/apksigner verify "$APK_PATH" 2>/dev/null; then + cp "$APK_PATH" "$DEST" + else + $BT/zipalign -v 4 "$APK_PATH" "${APK_PATH}.aligned" + $BT/apksigner sign \ + --ks $RUNNER_TEMP/keystore.jks \ + --ks-key-alias ${{ secrets.ANDROID_KEY_ALIAS }} \ + --ks-pass pass:${{ secrets.ANDROID_KEY_PASSWORD }} \ + --key-pass pass:${{ secrets.ANDROID_KEY_PASSWORD }} \ + --out "$DEST" "${APK_PATH}.aligned" + fi + + mkdir -p release-artifacts + mv "$DEST" release-artifacts/ + env: + NDK_HOME: ${{ env.ANDROID_HOME }}/ndk/29.0.14206865 + + - uses: actions/upload-artifact@v7 + with: + name: artifacts-android-${{ matrix.label }} + path: release-artifacts/* + retention-days: 1 + + # ============================================ + # Publish Release + # ============================================ + publish-release: + permissions: + contents: write + runs-on: ubuntu-latest + needs: [build-desktop, build-android] + steps: + - uses: actions/checkout@v6 + + - uses: actions/download-artifact@v8 + with: + path: artifacts + pattern: artifacts-* + merge-multiple: true + + - name: Display artifacts + run: ls -R artifacts + + - name: Extract changelog + id: changelog + run: | + TAG="${{ github.ref_name }}" + BODY=$(sed -n "/^## \[${TAG}\]/,/^## \[/{ /^## \[${TAG}\]/d; /^## \[/d; p; }" CHANGELOG.md) + BODY=$(echo "$BODY" | sed -e '/./,$!d' -e :a -e '/^\n*$/{$d;N;ba;}') + if [ -z "$BODY" ]; then + BODY="No changelog entry found for ${TAG}." + fi + echo "$BODY" > /tmp/release-body.md + printf '\n---\n\n**Desktop:** Windows (.exe), macOS (.dmg), Linux (.deb, .AppImage)\n**Mobile:** Android (.apk) — arm64, armeabi-v7a\n' >> /tmp/release-body.md + + - name: Create Release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ github.ref_name }} + name: 'OpenCodeUI ${{ github.ref_name }}' + body_path: /tmp/release-body.md + files: artifacts/* + draft: false + prerelease: ${{ contains(github.ref_name, 'canary') || contains(github.ref_name, 'alpha') || contains(github.ref_name, 'beta') }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index adb148ec..d180eada 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,8 @@ node_modules dist dist-ssr *.local +yarn.lock +.yarn # Environment variables .env @@ -20,7 +22,15 @@ dist-ssr # Docker workspace/ +# Rust +src-tauri/target/ +src-router/target/ + +# Generated material icons (copied from node_modules at postinstall) +public/material-icons/ + # Editor directories and files +.zed/* .vscode/* !.vscode/extensions.json .idea diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..7397700f --- /dev/null +++ b/.prettierignore @@ -0,0 +1,5 @@ +dist +node_modules +public/material-icons +src-tauri/target +coverage diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 00000000..337e2a73 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,7 @@ +{ + "semi": false, + "singleQuote": true, + "trailingComma": "all", + "arrowParens": "avoid", + "printWidth": 120 +} diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 00000000..b8595ca3 --- /dev/null +++ b/.tool-versions @@ -0,0 +1,2 @@ +node 22.22.0 +rust 1.85.0 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..f3eda9ff --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,932 @@ +# Changelog + +## [v0.6.23] - 2026-06-27 + +- fix: constrain mobile dialog content height (52d6a5e) +- fix: constrain composer height (94ed590) +- fix: preserve message disclosure state (0fcc13f) + +## [v0.6.22] - 2026-06-27 + +- fix: stabilize streaming markdown highlights (a973d79) + +## [v0.6.21] - 2026-06-27 + +- fix: remeasure CodeMirror layout after collapse-to-expand animation (d6e5d5c) +- fix: stabilize recent sessions on startup (a4e88f7) +- fix: avoid expanding stale chat pages on resize (69ee821) +- fix: smooth streaming markdown updates (664abc4) +- fix: optimize streaming markdown rendering (f88b68e) +- fix: reduce chat virtualization reflow work (4e520cf) +- fix: ensure auto-approved permission state is always cleared (f657827) +- fix: clear replied permission requests locally (45aced2) +- fix: optimize streaming markdown rendering (665ebf4) +- fix: stabilize chat stream render props (c5a6dc6) +- fix: reduce streaming chat re-renders (ec71fb1) +- fix: prefer verified terminal mono fonts (78ecf8d) +- fix: stabilize mobile keyboard focus (570f74a) +- fix: avoid blank CodeMirror content when expanding collapsed inputs (da64f85) +- fix: respect user scroll position in sub-session auto-scroll (a795405) +- fix: expand accordion content immediately, not one frame late (a08014f) +- fix: break infinite re-render cycle when moving terminal between panels (73d998d) + +## [v0.6.20] - 2026-06-22 + +- fix: bottom panel tabs should not have top safe-area padding (34a6d98) +- fix: stabilize fullscreen overlays (7e1ca42) +- perf: switch terminal to WebGL2 GPU-accelerated renderer (84b0a21) +- docs: add macOS quarantine removal note to READMEs (9341044) + +## [v0.6.19] - 2026-06-21 + +- perf: move outline fisheye animation to CSS variables (cebf4b8) +- perf: stabilize outline index during streaming (38648e0) +- fix: hide dialog close button on mobile (60016c0) +- fix: refactor Android status bar to top-chrome self-extend (e4735da) +- feat(chat): render user messages as markdown (5f9f2d3) +- fix: make Android status bar fully transparent (7d03f6c) +- fix: stabilize mobile pager depth transform (8117762) +- feat: add mobile pager depth transform (d7c9b98) +- fix: improve mobile pager accessibility (0493f6c) +- fix: polish mobile pager depth (9a1a890) +- fix: round mobile pager separators (2a3bb8c) +- fix: harden mobile pager state sync (cb7a161) +- feat: add mobile snap panel pager (39cb451) + +## [v0.6.18] - 2026-06-19 + +- chore: upgrade ajv to v8 for build compatibility (2c5830f) +- fix: macOS 拖拽文件上传两次的问题 (977844f) +- fix: 高亮当前对话位置的 tick 在日间模式下不可见 (c919d1c) +- fix(macos): 兼容拖放坐标判断 (f4ae1e9) +- fix(macos): clear fullscreen state on window destroy (37972e9) +- fix(macos): 退出全屏后重新对齐红绿灯位置 (1a6c403) +- fix: undo 后输入框字体模糊的问题 (726387e) +- fix(macos): 红黄绿按钮与自定义标题栏按钮垂直对齐 (a942272) +- fix: macOS 从 Finder 拖放文件到窗口不生效 (71ed010) +- fix(chat): integrate pinned session UI and lifecycle (9b1b7a9) +- feat: add conversation pinning to sidebar (d6b7500) +- fix(chat): preserve first message on new sessions (38e51c3) +- fix(chat): keep outline highlight toggle scoped (a1e9ddb) +- fix(chat): stabilize outline current highlight (68b7748) +- fix(terminal): omit undefined restore size options (c0a6038) +- fix(chat): show hidden project directories (9da3bee) +- feat: add territory-based highlight to outline index (ec10ea4) +- fix: consolidate drawer bottom safe-area padding (8c01459) + +## [v0.6.17] - 2026-06-07 + +- fix: default changes panel to last turn scope (0c341e9) +- fix: read metadata.output for real-time bash streaming during execution (5da7b1f) +- fix(settings): apply mobile viewport height to config editor (d442e66) +- fix(titlebar): preserve decorum window controls (1892621) +- fix: remove touchAction:none from FileTreeItem button that broke mobile scrolling (a619d72) +- fix: replace 92vh with calc(var(--app-height) * 0.92) in mobile SettingsDialog to fix address bar hiding issue (f3558fc) + +## [v0.6.16] - 2026-06-07 + +- fix(startup): cancel stale local endpoint requests (9c789e5) + +## [v0.6.15] - 2026-06-06 + +- fix(settings): validate opencode server health (0c19e18) +- fix(startup): refresh after local service URL changes (991a856) + +## [v0.6.14] - 2026-06-06 + +- fix(pwa): clear stale iOS keyboard inset (f5eba98) +- fix(chat): lock mobile sidebar swipe direction (7e1df88) +- fix: apply detected local service URL before boot (e0c00bb) +- fix(settings): improve config editor schema handling (b5a8978) +- fix(session): reset missing routed sessions (46a35d0) +- fix(chat): preserve streaming page height measurements (1c97439) +- fix: detect local opencode service URL (e628ac7) +- feat(settings): add visual opencode config editor (7965d06) +- feat: auto-detect opencode service binary (b3b32bd) +- refactor: align api types with sdk (ad5fb7b) + +## [v0.6.13] - 2026-06-05 + +- chore: update opencode sdk (4a4fe17) +- fix: render Windows path markdown links (d895fa6) +- fix: improve Linux terminal font fallback (7158d87) +- fix: make markdown code blocks selectable (cd453ad) + +## [v0.6.12] - 2026-06-02 + +- refactor: harden internal drag interactions (ccb0280) +- feat: support desktop file path drops (aa177a3) +- fix: keep mermaid touch controls off desktop (346423a) +- fix: refine markdown touch controls (6dfa4b9) + +## [v0.6.11] - 2026-05-31 + +- fix: clarify auto approval behavior (e16e7aa) +- fix: preserve whitespace in split word diff (d0b10dd) +- fix: highlight streaming code blocks (9198a4d) +- fix: improve markdown rendering (2a75c1b) +- perf(chat): stabilize virtualized scroll premeasurement (046019d) +- fix: refine chat page virtualization (5854671) +- chore: bump version to 0.6.11-canary.1 (bd0ecd8) +- fix: stabilize chat scrolling with paged virtualization (a7bb14a) +- fix: terminal reverse video shows black box in light mode (b902645) +- fix: terminal ANSI white colors invisible in light mode (5a8a386) + +## [v0.6.11-canary.1] - 2026-05-25 (Pre-release) + +- fix: stabilize chat scrolling with paged virtualization (a7bb14a) + +## [v0.6.10] - 2026-05-17 + +- fix: refresh sidebar child session links (a49908a) +- ci: deploy pages from dev (bc50843) +- fix: route permission replies through session (75bd8ac) +- fix: avoid iOS toolbar bottom gap (d880fac) +- fix: keep provider enabled for visible models (2462adf) +- fix: show detailed completion time on hover (9ca1ab6) + +## [v0.6.9] - 2026-05-16 + +- fix(terminal): correct clipboard paste behavior (bf6f902) +- ci(docker): add image workflow guardrails (da80d45) +- fix(docker): avoid qemu frontend builds (2d4d00b) + +## [v0.6.8] - 2026-05-16 + +- fix(diff): keep split word diff text themed (53e1d9a) +- feat(docker): add optional host backend access (279ac15) +- fix(message): preserve aborted messages with content (86fd11f) + +## [v0.6.7] - 2026-05-15 + +- fix(input): prevent IME confirmation from sending messages (5f4e0bb) +- feat(terminal): add scoped clipboard keybindings (2f8a933) + +## [v0.6.6] - 2026-05-13 + +- fix(session): 修复 PR #90 的子目录加载竞态并理顺范围刷新语义 (5360b59) +- fix(session): 目录切换时合并活动会话刷新状态 (4aca25d) +- feat(file-explorer): 按目录恢复文件树展开状态 (e29498d) + +## [v0.6.5] - 2026-05-10 + +- ui: show bash working directory (48d5b28) +- fix: skip shiki fallback for unsupported languages (d4ea3be) + +## [v0.6.4] - 2026-05-04 + +- fix: keep delete change bars continuous (843d861) + +## [v0.6.3] - 2026-05-04 + +- ui: simplify tool diff result headers (a5889a5) +- build: reduce bundled asset size (3e4aaa2) +- fix: align wrapped diff empty textures (d4b187b) +- fix: smooth split session loading (c4f3bb7) + +## [v0.6.2] - 2026-05-04 + +- fix: smooth wrapped diff scrolling (e46d0e5) +- fix: center collapsed diff buttons on mobile (9c1c6ef) +- fix: keep empty diff buffer textures aligned (3b70696) + +## [v0.6.1] - 2026-05-03 + +- fix: align readonly code gutters with diffs (b9a15d3) +- fix: polish diff gutter and separators (2568070) +- fix: keep chat bottom spacing comfortable (bc42873) +- fix: tighten chat surface bottom spacing (f47dedb) +- fix: PWA safe-area double padding on iOS (0b4fa33) + +## [v0.6.0] - 2026-05-03 + +- test: update diff viewer mock for shared data (13d7c04) +- fix: stabilize preview tab hover width (f2d0564) +- feat: improve file and change previews (7ae977d) +- fix: restore scrolling for inline code previews (09d3e96) +- perf: share diff data with fullscreen viewer (e3ceef8) +- perf: hoist diff viewer data across view modes (4467730) +- fix: improve readonly code selection contrast (4955f56) +- fix: strengthen empty diff buffer texture (849445f) +- fix: add Pierre-style empty diff buffers (16eec59) +- fix: adapt line number gutter width (d37f112) +- fix: center diff separator icons (db857dc) +- fix: port Pierre diff separator styling (789dcf2) +- fix: add Pierre-style diff separator expansion (1ce0d3a) +- fix: refine handwritten diff collapsed separators (ab9c8a6) +- fix: mask readonly code gutters (0a41e2a) +- refactor: extract readonly codemirror view (0b81235) +- fix: refine code search responsive layout (fc12a33) +- fix: anchor code preview search panel (00244e3) +- fix: polish code preview search panel (cd4e8d1) +- fix: smooth syntax theme switching (1f079e1) +- fix: use complete github shiki themes (2fc89fb) +- fix: align shiki theme with github syntax colors (e4c171a) +- feat: add adaptive shiki theme (6cb720e) +- feat: use codemirror for code previews (487cea3) +- fix: optimize sidebar transition rendering (06fe9a2) +- fix: limit message offscreen unmounting to resize (3986e5f) + +## [v0.5.20] - 2026-05-01 + +- fix: preserve git subdirectory projects (e9646b9) + +## [v0.5.19] - 2026-04-29 + +- fix: tighten sidebar search spacing (8b76cb2) +- fix: align desktop titlebar and dialog layout (f5800ba) +- fix: align dialog overlay styling (fe3a3cf) +- fix: unify right panel tab styling (ca6d697) +- fix: align command palette styling (dc67f38) +- fix: polish sidebar popover styling (da633ea) +- fix: preserve route session during refresh (9d3f0a0) +- fix: prevent terminal reconnect loops on global switch (5d0aca2) + +## [v0.5.18] - 2026-04-29 + +- fix: tighten pane focus and interaction regressions (c800e44) +- fix: prevent route sync from overwriting focused panes (fa44f13) + +## [v0.5.17] - 2026-04-28 + +- fix: keep session actions tied to visible focus (0e16597) +- ci: update GitHub artifact actions (d9e49c6) + +## [v0.5.16] - 2026-04-28 + +- fix: hide session actions after mouse selection (1d6b4be) +- ci: upgrade GitHub Actions Node runtimes (ff10d7b) + +## [v0.5.15] - 2026-04-28 + +- fix: restore model selector typography scaling (121b762) + +## [v0.5.14] - 2026-04-28 + +- fix: close the remaining review regressions (58a03dc) +- fix: clear lint regressions and selector focus artifacts (1b85707) +- fix: preserve injected desktop titlebar controls (7e60acf) +- fix: reveal session row actions on keyboard focus (a568535) +- fix: ignore hidden controls during menu dismissal (2db0cd0) +- fix: normalize menu focus target detection (c981584) +- fix: harden menu close paths against hidden targets (c1ac600) +- fix: make portal menus inert only after close (f82b285) +- fix: route tab exits through real control order (ce3ad2d) +- fix: keep selector focus stable during keyboard actions (6fbefae) +- fix: finish keyboard flows for selection popups (110cb06) +- fix: complete keyboard navigation for selection menus (ce247ee) +- fix: stabilize stacked dialog and toolbar menu focus (3d3c542) +- fix: align keyboard paths across dialogs and selectors (8f7b01f) +- fix: tighten follow-up focus and visibility behavior (7618593) +- fix: close the remaining interaction accessibility gaps (f634223) +- fix: polish remaining high-traffic control semantics (5c88ad0) +- fix: resolve remaining session and dialog correctness issues (7bde507) +- fix: tighten menu and diff accessibility (399471d) +- fix: finish button semantics in project controls (c05d5c9) +- fix: improve semantic buttons in settings and sidebar (3a1b5bf) +- fix: harden session updates and menu interactions (1d673df) +- fix: update context usage immediately after compaction (df01835) + +## [v0.5.13] - 2026-04-27 + +- fix: restore drag and drop in secondary desktop windows (36d489e) + +## [v0.5.12] - 2026-04-27 + +- fix: create the Android main window explicitly (1c20510) + +## [v0.5.11] - 2026-04-26 + +- fix: defer desktop windows until the first frame is ready (1b1e186) +- fix: remove titlebar separator that misaligns with sidebar border (ed14752) +- fix: prevent overlay sidebars from being obscured by desktop titlebar (8b20d80) +- feat: add titlebar actions — back/forward, open project, settings, new window (65abc23) +- fix: keep fullscreen viewers below desktop chrome (40e1ef4) + +## [v0.5.10] - 2026-04-26 + +- feat: add manual terminal labels and restore state (b42a90d) +- fix: align terminal PTY handling with upstream (093f2f2) +- fix: avoid macOS traffic light overlap in fullscreen headers (a5a3554) + +## [v0.5.9] - 2026-04-23 + +- fix: harden live tool timing updates (d6c963a) +- feat: resolve issue #74 running tool durations (54490f3) +- feat: store calibrated server time locally (f35c4ab) +- feat: handle server.connected event timestamps (0e77075) +- refactor: extract shared ticking clock hook (1b0c626) + +## [v0.5.8] - 2026-04-21 + +- fix: polish sidebar session and notification layout (51b22f2) +- refactor: modularize settings backup snapshots (a9d59c6) +- fix: reset project dialog state on reopen (b344278) +- feat: add settings backup import and export (93ad2ab) +- fix: rebalance obsidian surface contrast (0837734) +- fix: separate system notification settings from sound config (ccc80e4) +- feat: add separate system notification controls (3236d14) +- feat: add Dracula theme preset (7623377) +- fix: persist model selection across session restore (d8eb318) +- fix: shrink launcher icon mark further for masked shells (4f3e99f) +- fix: reduce app icon mark scale and drop custom Android splash (2552342) +- fix: remove unsupported Android splash attr from canary build (21ea11a) +- fix: shrink Android app icon foreground and add native splash theme (8325bfc) +- feat: redesign app icons for desktop and Android (527d423) + +## [v0.5.7] - 2026-04-19 + +- fix: anchor toasts to the content area beneath desktop titlebar (fe15280) + +## [v0.5.6] - 2026-04-19 + +- chore: sync Tauri app icons from web opencode.svg (f792218) +- polish: slim desktop titlebar down to minimal app chrome (375e8d1) +- feat: add platform-aware desktop titlebar foundation (43fa2da) +- fix: prevent model selector from jumping to wrong provider on split (5b33360) + +## [v0.5.5] - 2026-04-18 + +- fix: remove redundant *Single i18n keys — let i18next handle count=1 natively (9385ef8) +- style: match pane drop highlight radius to pane shell (rounded-lg) (f8acac2) +- feat: enable drag-to-split on active session list items (ac20b7b) + +## [v0.5.4] - 2026-04-18 + +- polish: pane drop overlay — drop text labels, harden edge cases (2be4c13) +- perf: keep pane drop overlay state out of ChatPane re-render path (17025ad) +- feat: drag sessions onto chat pane to split or replace (e413864) +- fix: add keyboard shortcuts to question dialogs (send keybinding to submit, Escape to skip) (dc83e0e) +- fix: narrow shell tool detection to exact 'sh' match across all matchers (f534454) +- Update zh-CN usage stats labels to English (074878e) +- fix: distinguish file writes in tool summary (2f25e93) +- fix: refine message tool summary wording (1d41542) +- fix: guard session error without sessionID (2166183) + +## [v0.5.3] - 2026-04-17 + +- style: use distinct icons for settings tabs (9b2adb1) +- fix: sanitize exported CSS snippet filenames safely (139725c) +- fix: adapt token usage ring track to all themes (75f2ad4) +- refactor: derive theme previews from preset tokens (720c426) +- feat: add reusable CSS overrides for themes (eba9d1d) +- refactor: split settings into agent and workspace tabs (b38ac6b) +- feat: add configurable message completion timestamps (2f01c56) +- feat: add model visibility settings (731efdd) +- style: improve glass effect - thicker base, blur(22px), saturate(200%) (bd27ce1) +- feat(themes): 新增Sakura、Ocean、Obsidian三款预设主题 (bfa449c) +- feat(settings): 添加自定义CSS模板管理功能 (79db9f4) + +## [v0.5.2] - 2026-04-15 + +- test: fix streamed event mock typing (92627ee) +- fix: preserve UTF-8 text in streamed markdown updates (8d89ce4) +- feat: add release update checks with about entry (closes #33) (e29a842) +- fix: preserve files and changes panel state across tab switches (closes #63) (3d57dc9) +- fix: allow pin toggle on selected model in desktop model selector (96e0ab2) +- fix: remove mobile 16px font-size override so input matches message stream (9e1eb6e) + +## [v0.5.1] - 2026-04-13 + +- feat: show agent and model name in step finish info (closes #61) (7299319) +- fix: restore line-height lost during font system migration (3d83f3b) +- fix: split diff view losing syntax highlighting when word diff is active (9f281ec) +- feat: tune default font sizes to match opencode official UI proportions (29c6f76) +- feat: unified typography system with CSS variables and per-axis font scale sliders (4533b15) + +## [v0.5.0] - 2026-04-13 + +- feat: add keep-screen-awake toggle in appearance settings (Wake Lock API) (3a6a13c) +- fix: update inline code test to match simplified style (no border/bg) (edc4beb) +- fix: simplify inline code style and add persistent underline to links (7f2cd73) +- fix: allow share URL to scroll horizontally on mobile (d049ce0) +- fix: ensure bottom padding in PWA standalone for devices without Home Indicator (eb8682c) +- fix: use replaceState on mobile to prevent session history stacking (ceed83d) + +## [v0.4.9] - 2026-04-11 + +- fix: prevent model restoration from overriding user selection during streaming (0c799e3) +- fix: return 404 for /api on frontend-only image (b07a30e) +- fix: align UI hook dependencies with live state (63a2b87) +- refactor: remove dead UI cleanup leftovers (5b82694) +- fix: remove unused resolveAlias function in shiki module (81d5fdb) +- perf: lazy-load shiki languages and optimize Tauri release profile (1be34e0) +- fix: PTY WebSocket auth fails behind reverse proxy (4bbb3c5) +- fix: prevent session fetch storm on SSE reconnect in SessionContext (ae26e6c) +- refactor: unify SSE and PTY into a single transparent bridge (73e638f) +- fix: use tungstenite message variants for PTY bridge (6210294) +- fix: bridge Tauri mobile PTY through native client (e33bde5) + +## [v0.4.8] - 2026-04-10 + +- feat: queue follow-up messages behind active turns (90756e3) +- fix: GPT apply_patch diff not rendering and error messages invisible in chat (6d6f81d) +- fix: align prompt history cursor navigation (4dd2bf4) +- fix: tighten session alerts and mobile code copy (8e02c0a) + +## [v0.4.7] - 2026-04-09 + +- fix: update command test to mock sdk instead of removed http module (f7521bc) +- fix: patch sdk migration review findings (e45b2b5) +- refactor: collapse remaining sdk helper types (c9d2a74) +- refactor: align remaining api models with sdk (f9c9272) +- refactor: align user message model fields with sdk (e427ccd) +- refactor: align event types with sdk (826cd49) +- refactor: tighten message part guards (b83098f) +- refactor: align tool part types with sdk (cecab44) +- refactor: align event payload adapters with sdk (d9ffe50) +- refactor: tighten sdk adapters and message conversions (a5aa326) +- refactor: align config and skill types with sdk (4eed10c) +- refactor: trim remaining sdk type wrappers (bfa0bde) +- refactor: collapse API types onto sdk definitions (e5e7305) +- refactor: replace API type wrappers with sdk aliases (89d42ac) +- fix: finish sdk migration cleanup (a90d0aa) +- fix: align API layer with official opencode sdk (ae4308c) +- fix: eliminate UI flicker, merge duplicate effects, avoid object mutation (a6e4cc1) +- fix: stabilize git workspace recents and worktree actions (abd0ee5) + +## [v0.4.6] - 2026-04-07 + +- fix: keep active child sessions visible across projects (9d8a725) +- fix: polish folder recents load more control (cbb1d59) +- fix: fade changes stats as one line (f05f8af) +- fix: compact changes panel header (231904f) + +## [v0.4.5] - 2026-04-06 + +- fix: refine changes menu spacing (ad48921) +- feat: sync file explorer status with change modes (b655d50) +- fix: align undo state with visible messages (752cae6) +- fix: clear command drafts after dispatch (4851435) +- fix: show history compacted messages (cc7d980) +- fix: simplify changes panel mode switch (ceb721b) +- feat: add git and branch review modes (71a8c0d) +- feat: add git setup and current-turn session changes (769f3ab) + +## [v0.4.4] - 2026-04-06 + +- fix: keep edit mode checkboxes compact on mobile (3d7c342) +- feat: support shift-select in recents edit mode (6181f5f) +- style: refine edit mode selection visuals (c7ca524) +- feat: add batch edit mode for sidebar recents and rewrite folder drag-sort (a1326d5) +- fix: persist panel layout and terminal positions (e1aeea8) + +## [v0.4.3] - 2026-04-05 + +- fix: improve pane navigation and sidebar drag affordances (a3dd889) +- fix: keep input focus after sending (161e7b8) +- fix: render bash tool commands inline (b6a6db2) +- fix: remove sidebar footer divider (d6e59b9) +- fix: prefer pointer outline interaction on hybrid devices (a20ebc0) + +## [v0.4.2] - 2026-04-03 + +- fix: switch folder recents to the clicked directory (d579407) +- fix: support sticky ctrl+alt combos in mobile terminal keyboard (4d9c5b3) +- fix: use tauri plugin-opener for external links in terminal and MCP auth (9b80bf3) +- fix: add background tint and equal spacing to plain code block copy button (102b5d4) +- fix: prevent global mode from being overridden by pane directory sync (b7a665c) + +## [v0.4.1] - 2026-04-03 + +- fix: disable split-pane entry points on small touch screens (3c1a607) + +## [v0.4.0] - 2026-04-03 + +- fix: align request dialogs with the input dock width (59b5962) +- perf: fix memo-defeating patterns in message rendering pipeline (f678d36) +- fix: stabilize ChatPane tree structure across fullscreen toggle (79b28f1) +- fix: preserve DOM across pane fullscreen toggle and hide split button in fullscreen (13c7f5e) +- fix: keep split resizing off the render path (750dd50) +- fix: normalize panel PTY restoration and dedupe terminal tabs (d1b4565) +- feat: add pane fullscreen mode and refine split header actions (cf959e0) +- fix: remove split container transition side effects (5332f61) +- fix: stabilize split-pane header interactions (6631e01) +- refactor: streamline split-pane chrome and transitions (cdb4a8b) +- fix: avoid first-frame tool expansion flicker on session switch (a71dea2) +- refactor: finish pane-first cleanup and auto-approve wiring (8af9e57) +- fix: unify router state and focused-pane directory sync (2cbb177) +- refactor: remove legacy focused-session compatibility layer (e4d0616) +- refactor: unify chat shell around focused pane state (e45dd17) +- fix: sidebar selectedSessionId follows focused pane in split mode (a82abaf) +- fix: isolate per-pane state — fullAutoMode, agent selection, session eviction, clearSession (18bc9af) +- fix: prevent duplicate SSE subscriptions in split-pane mode (e15ad08) +- feat: add split-pane UI with full-parity ChatPane, SplitContainer, PaneHeader, SplitToolbar (3af6b14) +- refactor: parameterize useChatSession for multi-instance support (1b37d8d) +- refactor: make session infrastructure multi-instance ready (e0ce47d) +- fix: hide assistant fork action when no text can be copied (f734462) +- fix: keep composer action blur out of transform layers (6eb3bf0) + +## [v0.3.8] - 2026-03-31 + +- feat: add fullscreen button to file preview and changes diff preview (9e4ca2d) +- feat: enable fork from assistant messages to preserve AI replies (f2a0079) +- refactor: unify floating component shadows to a consistent two-tier system (shadow-sm / shadow-lg) (1cf526e) +- chore: upgrade dependencies (vite 8, i18next 26, lucide-react 1.x, etc.) (41498bf) +- fix: adjust ModelSelector padding so scrollbar doesn't overlap list content (e7d6e4f) +- refactor: unify ModelSelector into a single component for both PC and mobile (5b81638) +- fix: sync session title to messageStore on SSE update for real-time header refresh (b74bb6a) +- fix: align mobile header toggle spacing (1052826) + +## [v0.3.7] - 2026-03-28 + +- fix: remove double border on attachment meta when no content preview (d29a2f4) +- fix: hide floating actions (undo/redo/permission) during todo panel swap (a3a7231) +- fix: apply glass effect to mobile collapsed capsule (2075818) +- fix: fallback fetch after send to prevent missing user message on SSE drop (e2138df) +- fix: skip overlay scrollbar on elements with no-scrollbar class (506cf24) +- feat: child sessions displayed under parent in sidebar with toggle for always-show (97ce7d8) +- feat: add diff toggle for folder recents (a1ed95e) +- fix: wire compact model selector to global shortcut (d14fb42) + +## [v0.3.6] - 2026-03-28 + +- test: add chatViewport mock to InputBox and InputToolbar tests (7eb80ac) +- fix: remove unused let binding in overlayScrollbar (ab9eaba) +- feat: add horizontal overlay scrollbar support (ad8d21d) +- refactor: rewrite outline index with visual config, focus-based interaction and entry windowing (18bccf9) +- fix: align compact model selector trigger (480dd04) +- refactor: centralize chat viewport state (8512fcb) +- fix: improve coarse pointer support in desktop UI (4cfc8e5) + +## [v0.3.5] - 2026-03-27 + +- fix: use previous stable tag for release changelog (b00cff7) +- fix: load chat header title from session detail (0b8443c) +- fix: stabilize chat history loading scroll behavior (87190c6) +- fix: animate todo panel swap without layout jank (ca8a672) +- feat: add frosted glass toggle in appearance settings (662070c) +- fix: apply overlay scrollbar to textarea, hide all native scrollbars (5dce51f) +- fix: frosted glass not rendering in Tauri production build & overlay scrollbar positioning (96e72c1) +- feat: replace native scrollbars with global overlay scrollbar system (da09781) +- refactor: simplify @ and / menus with cleaner layout and unified style (f5bddec) +- refactor: redesign ModelSelector for glass aesthetic (33a9dd2) +- refactor: unify frosted glass system with CSS utility classes (0e459f9) +- style: introduce frosted glass effect to all floating surfaces (ae10178) +- fix: remove extra padding from message toggles (4eb916a) + +## [v0.3.4] - 2026-03-24 + +- fix: align markdown copy buttons with header text (80e59ce) +- fix: render markdown images without streamdown wrapper controls (650851f) +- fix: remove code size-based rendering limits (42171e7) +- fix: remove contain-intrinsic-size that caused scroll jank (7942144) + +## [v0.3.3] - 2026-03-24 + +- fix: resolve TypeScript errors in MarkdownRenderer for release (1abcdf1) +- fix: exclude task tool from compact inline permission mode (5f39aff) +- style: remove left accent line from TaskRenderer, restore badge status colors (1cb6044) +- style: refine TaskRenderer visual hierarchy (263b74b) +- fix: show tool description from input while running, not just after completion (b65d16e) +- fix: reduce excessive right padding in expanded reasoning content (885668d) +- fix: panel dropdown menu hover overflow — use inset padding with rounded items (02653bf) +- fix: table copy button pinned outside scroll, mobile always-visible copy buttons (2cdef17) +- style: unify tool output header height to h-8 (32px) (c566b1a) +- style: unify message flow border-radius to a tighter 3-tier system (4a8a89f) +- refactor: redesign markdown code blocks, tables, and inline code styles (11f6222) + +## [v0.3.2] - 2026-03-24 + +- fix: align fullscreen diff test mocks with typed children (fdf1a74) +- fix: restore release validation after fullscreen refactor (ef2d755) +- fix: adjust outline index spacing for visual balance (fb98097) +- refactor: unify fullscreen components into generic FullscreenViewer (9e6d8ed) +- refactor: redesign settings UI with section-based layout and cleaner primitives (478351a) + +## [v0.3.1] - 2026-03-23 + +- fix: unify chevron arrow direction - collapsed points right, expanded points down (3557e5a) +- feat: compact inline permission - hide duplicate content when tool body already renders (63d571b) +- refactor: redesign descriptive steps summary - merge categories, per-category errors, truncation (870f490) +- fix: move diff stats next to title and remove exit code from descriptive steps (74e69c0) +- fix: deferred permission unmount and multi-select question answer parsing (a8dc9bd) +- fix: auto-expand readable tools that finish instantly in immersive mode (06dd4d4) + +## [v0.3.0] - 2026-03-22 + +- fix: lint warnings, error tool diff stats, descriptive steps partial error coloring (ea53836) +- feat: add diff stats summary to descriptive steps, fix write tool diff display (ce29807) +- fix: immersive mode keeps non-readable tool groups collapsed even during execution (be61de2) +- fix: resolve lint errors and remove unused eslint-disable directives (6037d19) +- feat: add immersive mode with smart tool expand/collapse (0d4252d) +- fix: skip QuestionRenderer when user dismissed or error (2029584) +- feat: add QuestionRenderer with InlineQuestion-style read-only answered view (207f0c6) +- fix: allow long bash commands to wrap in terminal view (8a7ddfd) +- simplify: BashRenderer remove buttons, click command to copy, inline exit code (72186b5) +- refactor: BashRenderer with fixed bottom bar, exit code, fullscreen, mobile-friendly buttons (cb7bb70) +- fix: restore height limit and fullscreen button in compact mode (8f8689d) +- feat: add BashRenderer with terminal style, Shiki highlighting, ANSI color support (856d74d) +- feat: descriptive steps default collapsed, show output status on tool row (b25a0f2) +- refactor: unify InlinePermission with tool output style, remove unused inline variant (ac510d7) +- feat: add compact tool output mode (hide input, no collapse, no height limit) (1036133) +- feat: add descriptive tool steps mode (d7e153f) +- refactor: remove ambient tool mode and make inline requests opt-in (06034a2) + +## [v0.2.10] - 2026-03-21 + +- feat: restore forked prompts in the composer (0792cf6) +- fix: keep folder recents aligned with live updates (a0c8899) +- fix: improve long duration formatting (62e6d94) + +## [v0.2.9] - 2026-03-19 + +- fix: preserve custom audio when switching to builtin sounds (57119da) +- fix: resolve all eslint warnings across codebase (4b31da3) +- feat: add notification sound system with per-event configuration (b6019e2) +- perf: defer offscreen chat message rendering (52d8ba8) +- refactor: unify file and changes preview panels (9f315d8) + +## [v0.2.8] - 2026-03-18 + +- fix: wrap panel tab label case blocks (d353b80) +- feat: expand right panel resize range (3e67623) +- feat: add tabbed session changes workspaces (ace0259) +- feat: add tabbed file preview workspaces (ffad629) +- fix: stabilize mobile terminal toolbar layout (1eb23b7) +- feat: refine mobile terminal extra keys behavior (6e04254) +- feat: add mobile extra keys toolbar for terminal (Termux-style) (2b185ac) +- feat: show collapsed folder activity status (0bd1378) +- feat: mark completed sessions as unread in recents (90cf8c3) + +## [v0.2.7] - 2026-03-18 + +- feat: add markdown reasoning display mode (20fa476) +- fix: avoid action overlap in folder recents on mobile (83b40b9) +- feat: animate folder recents expansion (1b8ea13) +- fix: preserve folder recents expansion across tab switches (058c289) +- fix: limit streaming layout animation to bottom-follow mode (95c6aca) +- fix: restore reasoning thinking shimmer transition (5c631c8) +- feat: refine reasoning markdown presentation (71a012a) +- fix(chat): extract formatDuration to shared formatUtils (c405092) +- fix(chat): preserve aborted turn durations (13633fb) +- feat: add diff gutter style setting (markers vs change bars) (4c5e7b8) +- fix(i18n): improve permission dialog labels for request/rule clarity (e3730d5) +- refactor: replace react-markdown with Streamdown for streaming-optimized markdown rendering (fcf3307) +- refactor: extract useResponsiveMaxHeight hook for shared viewport-aware sizing (1993eb8) +- fix: make ContentBlock maxHeight responsive to viewport size (b584071) +- fix: resolve drag-to-reorder race condition causing stale closures (b08a763) + +## [v0.2.6] - 2026-03-17 + +- refactor: redesign folder recents with drag-to-reorder and compact session items (88b4139) +- fix: resolve bugs introduced by Python-to-Rust router migration (335b82e) +- refactor: migrate gateway router from Python to Rust (290087f) +- Feature: Add ability for router to read config from environment variables (bddc46b) +- refactor: create new Rust project opencodeui-router (dd96377) +- Update image previews in README.md (be84586) +- fix: 服务器编辑/删除按钮始终可见 (0037a17) + +## [v0.2.5] - 2026-03-16 + +- fix: 修复胶囊按钮和弹窗 header 图标与文字对齐 (a65e34e) +- fix: 工具 icon 光晕不再被父容器裁切 (c946f19) +- fix: 移动端设置 tab 选中态被 overflow 裁切 (027fcf9) +- refactor: 设置界面优化 — 修复双滚动条、服务器编辑/删除确认 (ec75fad) +- fix(i18n): 保留开发者工具常见英文术语不翻译 (f4a6022) +- fix(i18n): 修正中文翻译质量 (f2bd269) +- feat: add full i18n support with react-i18next (en + zh-CN) (bcd9850) + +## [v0.2.4] - 2026-03-16 + +- fix: 会话级 Full Auto 恢复原有行为 — 只在当前所在页面的 session 生效,切走后不再自动放行 (e98bd4e) +- fix: Full Auto 全局模式在 SSE 事件层拦截,确保非当前会话的权限请求也能自动放行 (1046837) +- feat: Full Auto 三态模式 — 单击循环 off/会话级(黄)/全局(红),会话级只放行当前会话,全局放行所有,纯内存刷新即清 (db95fc7) +- fix: 去掉 steps header 入场动画,保持与其他元素一致不做特殊处理 (1e725a5) +- fix: 虚拟滚动横向滚动条修复 — 去掉 probe 元素改用 scrollWidth 历史最大值追踪,SplitDiffView proxy scrollbar 加 gutter 占位对齐 (beee631) + +## [v0.2.3] - 2026-03-15 + +- fix: probe 最长行选取改用 monoDisplayWidth 估算渲染宽度,CJK/全角字符按双倍计,修复含中文注释时横向滚动不到位 (d1fb35a) +- Revert "fix: probe 元素去掉 overflow:hidden 修复横向滚动不到位 — hidden 在两个方向截断内容导致 scrollWidth 偏小" (0e5353f) +- fix: probe 元素去掉 overflow:hidden 修复横向滚动不到位 — hidden 在两个方向截断内容导致 scrollWidth 偏小 (c9788fc) +- fix: lint warnings — ref 写入移入 useEffect,CodePreview 提取 tokens 避免 render 期间读 ref,修复 CodePreview 测试,清理多余依赖和 eslint-disable (96955f7) +- fix: 消除胶囊⇄输入框切换闪烁 — FloatingActions 改为同一 DOM 切换定位避免 remount,胶囊去掉入场动画和防抖回归纯 UI (bc3cc0c) +- fix: 移动端胶囊⇄输入框过渡优化 — 胶囊退场不延迟避免与输入框重叠闪烁,收起方向加 120ms 防抖消除滚动边界抖动 (4060e83) +- refactor: 统一动画体系 — UndoStatus 去自带动画改由 PresenceItem 控制,CollapsedCapsule 从 CSS animate-in 换成 usePresence,PresenceItem 加 shrink-0 防挤压,清理 CSS 死代码 (5106ac0) +- fix: probe 元素精确撑开 scrollWidth 修复横向滚动不到位,去掉 backdrop-blur (0f3b764) +- Revert "fix: 代码预览/diff 横向滚动重构 — CodePreview/UnifiedDiffView 改原生滚动+sticky gutter,SplitDiffView 用 probe 元素精确撑宽,去掉 backdrop-blur" (9d12d72) +- fix: 代码预览/diff 横向滚动重构 — CodePreview/UnifiedDiffView 改原生滚动+sticky gutter,SplitDiffView 用 probe 元素精确撑宽,去掉 backdrop-blur (9e37646) +- feat: usePresence hook + 浮动按钮/权限框/提问框入场退场动画 — 命令式 animate() 零额外 bundle (fb8c59f) +- fix: 消息流底部间距增大,为浮动按钮预留空间 (7f32fa2) +- fix: 删除 Output 的 Running... 文字,Input/Output spinner 统一无文字对齐 (e8c409e) +- fix: 单工具调用始终 compact 布局,消除流结束时的缩进跳变 — SmoothHeight 平滑过渡 + steps header 入场动画 (031502f) +- fix: 代码预览和 diff 行号背景色统一 — gutter 去掉硬编码背景,继承父容器颜色 (9ef8223) + +## [v0.2.2] - 2026-03-15 + +- ci: 恢复 codegen-units=1 减小产物体积,Rust cache 按平台隔离,精简工作流 (5018e3b) +- chore: 清理 suppressAutoScroll 死代码 (d47085e) +- fix: loadMore 不跳变 — 去掉 inner wrapper,消息反序直接作为 flex 子元素,prepend 时临时禁用 content-visibility (657e7a4) +- refactor: 用 column-reverse 替代 ResizeObserver 实现原生 stick-to-bottom (591a8eb) +- refactor: DiffView 复用 DiffViewer 组件,删除 150 行重复 diff 渲染代码 (e78d987) + +## [v0.2.1] - 2026-03-14 + +- refactor: 用 ResizeObserver 替代 RAF 轮询实现流式自动滚动 (10d9e6a) +- fix: 空消息不参与可见列表,消除 abort 时的滚动跳变 (9dbaa4d) + +## [v0.2.0] - 2026-03-14 + +- feat: RECENTS 列表标记活跃 session 状态 (closes #25) (98ecce9) +- fix: motion animate() 类型歧义 — 改用 motion/mini 单签名 API 解决 tsc -b TS2769 (7927bcf) +- fix: 移除 ContentBlock loading skeleton 骨架条,避免输出短于占位时的负增长跳变 (f6db986) +- feat: 消息入场生长动画 — 用户和助手消息统一从 height 0 平滑展开 (ce055fb) +- fix: SmoothHeight 激活时锁定 outer 高度,修复动画不触发的问题 (e5f6cf5) +- feat: 命令式 animate() 动画方案 — 高性能 + 零 React 组件开销 (4481aba) +- fix: 修复流结束后闪烁回弹问题 (192944e) +- fix: 修复流式文本不实时渲染的问题,移除 useSmoothStream (005b6bf) +- refactor: 连续助手消息分组渲染,共享容器浑然一体 (cfadad0) +- Revert "fix: 复制按钮始终占位,避免 text 到达时布局跳变" (1af197a) +- fix: 移除未使用的 hasMoreHistory 解构变量,修复 TS6133 编译错误 (cf9419b) +- fix: 复制按钮始终占位,避免 text 到达时布局跳变 (d51e7a8) +- fix: 移除 'Beginning of conversation' 常驻提示,仅保留加载中 spinner (11484c6) + +## [v0.1.18] - 2026-03-13 + +- ci: 加速 release 编译 — Rust 多线程 codegen + Android 双架构并行 (2398c54) +- fix: FloatingActions 高度抖动、滚动按钮误显示 (466f4ea) +- refactor: gutter/content 分离架构 + 水平滚动独立化 + FullscreenViewer 确定高度 (e327e59) +- perf: streaming 渲染地基优化 — rAF 滚动、delta 批量化、布局稳定性 (f689910) + +## [v0.1.17] - 2026-03-11 + +- fix: update CodePreview test mock to use useSyntaxHighlightRef (a10075f) +- fix: move history loading indicator below top spacing so it's visible (4cfc71c) +- fix: remove MAX_HISTORY_MESSAGES cap and restore loading UI (43d2273) +- perf: reduce sidebar resize lag with CSS containment and DOM-only sidebar drag (63bc0c5) +- fix: enable virtual scrolling in CodePreview by adding height constraint (43c9e3a) +- fix: mobile input collapses when tapping FloatingActions buttons (a777334) +- refactor: simplify loadMore pagination and remove prependedCount (8614df4) +- refactor: rewrite messageStore and ChatArea, remove IndexedDB cache layer (eecdeaf) +- refactor: remove loading spinners/skeletons and reduce scroll-related re-renders (eb7ebff) + +## [v0.1.16] - 2026-03-11 + +- fix: scroll jitter after streaming ends caused by content-visibility height mismatch (765052a) +- fix: stop auto-scroll jitter when user scrolls slightly during streaming (171a62e) + +## [v0.1.15] - 2026-03-10 + +- fix: default revertSteps to 0 in FloatingActions (400fbb4) +- chore: bump version to 0.1.15 (3f57df2) +- fix: slow scroll during streaming causes jitter by pulling user back to bottom (860683e) +- fix: isFocused stuck after toolbar button click prevents capsule collapse (aaaa727) +- refactor: extract FloatingActions and CollapsedCapsule components from InputBox (273bd98) +- refactor: extract useInputHistory hook from InputBox (d920da9) +- refactor: extract useAttachmentRail hook from InputBox (2150d82) +- refactor: extract useMobileCollapse hook from InputBox (c5fd39b) +- fix: mobile input capsule state not resetting on session switch + blur collapsing on toolbar interaction (474938e) + +## [v0.1.14] - 2026-03-10 + +- fix: session switch scroll + eliminate all content flicker during load (d823799) +- fix: infinite history loading loop + stabilize observer + cleaner scroll-to-bottom (fe28777) +- fix: smooth shimmer gradient + disable browser scroll restoration + multi-frame scroll-to-bottom (2144260) +- fix: shimmer highlight sweep + remove unused visibleMessageIds state (ae767ca) +- fix: outline click retract, prepend scroll preservation, cross-message merge continuation (e3ca11a) +- fix: desktop label click navigation + mobile passive touch events in OutlineIndex (f35f758) +- refactor: rewrite OutlineIndex with clean fisheye engine (1057c1c) +- refactor: replace react-virtuoso with native scroll + content-visibility (2dc550a) +- fix: correct shimmer animation direction (left-to-right) and use linear timing (16bc419) +- fix: endsWithTool skips empty reasoning/text so cross-message tool merging works correctly (fdedbcd) +- style: replace thinking breath-bar with shimmer text animation in italic mode (1bed06b) + +## [v0.1.13] - 2026-03-10 + +- fix: SSE reconnect race conditions and stale timeout constant (7614d53) +- test: add unit tests for HTTP module and Tauri environment detection (79b77af) +- chore: eliminate all 8 lint warnings (0 errors, 0 warnings) (b312bb6) +- fix: URL-encode query string values in buildQueryString (f6a434e) +- fix: add AbortController-based timeout to HTTP request() (7962278) +- fix: pending permission/question cache supports multiple requests per session (b421172) +- fix: SSE parser now handles CRLF line endings and multi-line data correctly (d4553d5) +- chore: remove dead storage key exports (WIDE_MODE, THEME_MODE, SIDEBAR_WIDTH, MODEL_VARIANT_PREFIX) (2901c99) +- refactor: replace bare console.error with unified error handlers across 14 files (7a0c86c) +- refactor: move theme/wideMode into themeStore, eliminate prop drilling through Sidebar and SettingsDialog (9c8b2d7) +- refactor: split messageStore into store, types, and React hooks layers (caef036) +- refactor: extract App.tsx hooks (useViewportHeight, useCancelHint, useCloseServiceDialog) (2dd70e3) +- refactor: split SidePanel into focused sidebar components (673db54) +- refactor: extract InputBox utility functions to input/inputUtils.ts (a1c0eee) +- refactor: split SettingsDialog into focused component files (72f5ed4) +- refactor: consolidate duplicated formatting functions into utils/formatUtils.ts (a981c89) +- refactor: deduplicate diff content extraction (fad37d4) +- refactor: extract ModalShell to unify modal overlay infrastructure (ae9f96e) +- chore: remove unnecessary eslint-disable in logger.ts (118740c) +- chore: clean up dependencies (0c50ac0) +- refactor: add dev-only logger, replace bare console.log with logger.log (91fc303) +- refactor: remove dead code files (test-shiki.ts, editorUtils.ts, toolUtils.ts) (3a3f37a) + +## [v0.1.12] - 2026-03-09 + +- fix: preserve changelog ordering during release prep (039b906) +- chore: ignore generated tauri assets in lint (071e2db) +- fix: keep slash command composer responsive (42cdcf6) +- fix: restore cross-platform Tauri app handling (6b84717) +- perf: replace std default hasher with rapidhash (c6d5bdb) +- refactor: reorganize the file structure of Tauri backend (fa5edf6) +- perf: refactor OpenDirectoryState with papaya HashMap to reduce lock contention (6efd2a0) +- perf: optimize ServiceState.child_pid with AtomicU32 to reduce Mutex contention (a42df0b) +- perf: optimize SseState implementation - replace Mutex+Hashmap with papaya library to reduce contention and improve performance (f586a84) + +## [v0.1.11] - 2026-03-08 + +- feat: add an optional folder-style Recent view while preserving the original session row details and per-folder ordering controls +- feat: aggregate Active sessions across all saved projects instead of limiting the list to the currently selected directory + +## [v0.1.10] - 2026-03-08 + +- fix: harden session message sync and failed sends (b788dce) +- chore: add validated release preparation flow (010ffbe) +- docs: consolidate v0.1.9 release notes (79113da) + +## [v0.1.9] - 2026-03-08 + +- fix: restore message attachment expand animation (2975fe3) +- fix: streamline composer attachment rail interactions (99db58a) +- fix: constrain expanded attachments and preserve composer blank lines (dd2d7ba) +- fix: harden composer attachment rail scrolling (b2bac29) + +## [v0.1.7] - 2026-03-08 + +- fix: truncate tool description overflow in tool call row (3782c67) +- fix: tighten mobile model menu and attachment width (60b34a2) +- fix: preserve utf-8 across tauri stream chunks (1dcb15a) + +## [v0.1.6] - 2026-03-07 + +- fix: restore tauri mobile file attachments (ffe3398) + +## [v0.1.5] - 2026-03-07 + +- chore: keep tauri config formatted on release (48f6045) +- fix: sync settings version with app release (b815d18) +- chore: format release workflow (aef533b) +- ci: add build validation workflow (491a544) +- other: add "zed/\*" as ignored file (8f32b7d) + +## [v0.1.4] - 2026-03-07 + +- fix: split frontend and api slash commands (bdb2e33) +- fix: support clipboard fallback in insecure contexts (edf4dd0) +- fix: align slash command descriptions (9d84a78) + +## [v0.1.3] - 2026-03-07 + +- chore: restore release workflow formatting (73a41a4) +- perf: split code preview from file explorer (b94bfc5) +- perf: lazy load optional panels and split vendor chunks (9e0f7d6) +- chore: add test baseline and clean lint debt (0d5f175) +- chore: establish lint and formatting baseline (762786d) +- fix: shorten input footer disclaimer copy (d691b7b) +- chore: automate lockfile updates in release script (e678349) + +## [v0.1.2] - 2026-03-07 + +- fix: scope active session state to the current directory (2503c6b) + +## [v0.1.1] - 2026-03-07 + +Patch release focused on chat input polish, session list consistency, and smoother permission handling. + +### Fixes + +- Restored collapsed input dock bottom spacing +- Kept the session list in sync across directory filters and live updates +- Returned gracefully to a new chat after deleting the currently open session +- Aligned the todo popover with the input dock for desktop and mobile +- Removed extra polling from permission/question flows and synced reply state immediately + +### Improvements + +- Preloaded `@` root listing and `/` command data when entering a session to reduce first-open lag + +## [v0.1.0] - 2026-03-05 + +First stable release of OpenCodeUI. + +### Features + +- Drag-and-drop file attachment support (desktop & mobile) +- Material file icons for file/folder display +- File @mention from explorer sidebar +- Context breakdown visualization in sidebar +- Live retry status display with expand/collapse +- Attachment detail viewer with copy/save functionality +- Capability-based file attachment upload + +### Fixes + +- Aligned capsule thinking chevron with italic/tool toggle arrows +- Stabilized Tauri desktop file drag-and-drop handling +- Fixed multiple task windows rendering the latest child session +- Eliminated scroll jank from high-frequency re-renders +- Fixed mobile overflow in project and diff headers +- Fixed sidebar notification/session meta row overflow +- Fixed attachment pill truncation and compact tool layout + +### Improvements + +- Migrated all icons to lucide-react +- Unified message part spacing and alignment +- Added Docker support with material icons build step diff --git a/README.md b/README.md index 28f25c2a..b133bd60 100644 --- a/README.md +++ b/README.md @@ -1,108 +1,393 @@ # OpenCodeUI -一个为 [OpenCode](https://github.com/opencode-ai/opencode) 打造的第三方 Web 前端界面。 +中文 | [English](./README_EN.md) -**本项目完全由 AI 辅助编程(Vibe Coding)完成**——从第一行代码到最终发布,所有功能均通过与 AI 对话驱动开发,累计 162 次提交。 +一个为 [OpenCode](https://github.com/anomalyco/opencode) 打造的第三方 Web 前端界面。 + +**本项目完全由 AI 辅助编程(Vibe Coding)完成**——从第一行代码到最终发布,所有功能均通过与 AI 对话驱动开发。 > **免责声明**:本项目仅供学习交流使用,不对因使用本项目导致的任何问题承担责任。项目处于早期阶段,可能存在 bug 和不稳定之处。 +## 预览 + +image +image +image ## 特性 - **完整的 Chat 界面** — 消息流、Markdown 渲染、代码高亮(Shiki) - **内置终端** — 基于 xterm.js 的 Web 终端,支持 WebGL 渲染 - **文件浏览与 Diff** — 查看工作区文件、多文件 diff 对比 - **主题系统** — 3 套内置主题(Eucalyptus / Claude / Breeze),支持明暗模式切换和自定义 CSS -- **PWA 支持** — 可安装为桌面/移动端应用,支持离线缓存 +- **PWA 支持** — 可安装为桌面/移动端应用 - **移动端适配** — 安全区域、触摸优化、响应式布局 - **浏览器通知** — AI 回复完成时推送通知 - **@ 提及与 / 斜杠命令** — 对话中快速引用文件和执行命令 - **自定义快捷键** — 可配置的键位绑定 - **Docker 部署** — 前后端分离容器化,开箱即用 -- **View Transitions** — 主题切换时的圆形揭幕动画 +- **桌面应用** — 基于 Tauri 的原生客户端(macOS / Linux / Windows) +- **动态端口路由** — 容器内开发服务自动发现,生成预览链接 ## 技术栈 -| 类别 | 技术 | -|------|------| -| 框架 | React 19 + TypeScript | -| 构建 | Vite 7 | -| 样式 | Tailwind CSS v4 | -| 代码高亮 | Shiki | -| 终端 | xterm.js (WebGL) | -| Markdown | react-markdown + remark-gfm | -| 虚拟列表 | react-virtuoso | -| 部署 | Docker (Caddy + Ubuntu) | +| 类别 | 技术 | +| -------- | ------------------------------ | +| 框架 | React 19 + TypeScript | +| 构建 | Vite 7 | +| 样式 | Tailwind CSS v4 | +| 代码高亮 | Shiki | +| 终端 | xterm.js (WebGL) | +| Markdown | react-markdown + remark-gfm | +| 桌面 | Tauri 2 | +| 部署 | Docker (Caddy + Python Router) | -## 字体 +## 快速体验 -- **UI 正文**:Inter(系统回退:system-ui, sans-serif) -- **衬线**:Georgia -- **代码/终端**:JetBrains Mono(项目内置 woff2),回退 Fira Code +无需部署,在本地启动 OpenCode 后端后直接访问托管版前端: -## 设计说明 +```bash +opencode serve --cors "https://lehhair.github.io" +``` + +然后打开 https://lehhair.github.io/OpenCodeUI/ + +## Docker 部署(纯前端) + +适用于已有 `opencode serve` 在运行的场景,只需一个前端 UI 容器连接到现有后端。 + +```bash +git clone https://github.com/lehhair/OpenCodeUI.git +cd OpenCodeUI + +# 启动(默认连接宿主机的 opencode serve :4096) +docker compose -f docker-compose.standalone.yml up -d +``` + +访问 `http://localhost:3000`。 + +**连接远程后端:** -本项目的部分 UI 风格参考了 [Claude](https://claude.ai) 的界面设计。 +```bash +BACKEND_URL=your-server.com:4096 PORT=8080 docker compose -f docker-compose.standalone.yml up -d +``` + +| 环境变量 | 默认值 | 说明 | +| ------------- | --------------------------- | ----------------------------------- | +| `BACKEND_URL` | `host.docker.internal:4096` | opencode serve 地址(不含协议前缀) | +| `PORT` | `3000` | 前端监听端口 | + +## Docker 部署 + +### 架构与端口 + +部署包含三个服务,由 Gateway 统一对外: + +| 服务 | 端口 | 说明 | +| -------- | ---------------------- | ------------------------------ | +| Gateway | 6658(`GATEWAY_PORT`) | 统一入口,反代所有请求 | +| Gateway | 6659(`PREVIEW_PORT`) | 开发服务预览专用 | +| Frontend | 3000(内部) | 静态前端 | +| Backend | 4096(内部) | OpenCode API | +| Router | 7070(内部) | 动态端口路由(内置于 Gateway) | + +### Gateway 路由规则 -## 快速开始 +端口 `6658` 上的请求按以下规则转发: -### 前提 +| 路径 | 转发目标 | 说明 | +| ------------ | -------------- | ---------------------- | +| `/api/*` | Backend :4096 | OpenCode API,支持 SSE | +| `/routes` | Router :7070 | 动态路由管理面板 | +| `/preview/*` | Router :7070 | 预览端口切换 API | +| 其他 | Frontend :3000 | 前端静态资源 | -需要一个运行中的 [OpenCode](https://github.com/opencode-ai/opencode) 后端(`opencode serve`)。 +端口 `6659` 用于访问容器内开发服务,Router 自动扫描 `3000-9999` 端口,通过 `/p/{token}/` 路径生成预览链接。 -### 本地开发 +### 部署步骤(无需 clone 项目) + +默认部署使用 GitHub Container Registry 上的预构建镜像,只需要下载 Docker 配置文件,不需要 clone 整个项目源码。 + +```bash +mkdir OpenCodeUI && cd OpenCodeUI + +# 下载默认运行配置 +curl -fsSLO https://raw.githubusercontent.com/lehhair/OpenCodeUI/main/docker-compose.yml +curl -fsSLO https://raw.githubusercontent.com/lehhair/OpenCodeUI/main/.env.example + +# 复制并编辑环境变量,至少填写一个 LLM API Key +cp .env.example .env + +# 启动 +docker compose up -d +``` + +访问 `http://localhost:6658`。 + +如果你的环境没有 `curl`,也可以直接从 GitHub 下载 `docker-compose.yml` 和 `.env.example` 放到同一个目录。 + +### 开启宿主机 Docker 能力(可选) + +默认部署不会给后端容器开放宿主机 Docker daemon,也不会挂载宿主机根目录。这样更接近旧版本行为,老用户升级不会突然获得额外权限。 + +如果你需要在 OpenCode 后端容器里构建 Docker 镜像、执行 `docker compose`,或访问宿主机路径,再额外下载并启用 `docker-compose.host.yml`: + +```bash +curl -fsSLO https://raw.githubusercontent.com/lehhair/OpenCodeUI/main/docker-compose.host.yml +docker compose -f docker-compose.yml -f docker-compose.host.yml up -d +``` + +该 override 会给 `backend` 增加: + +- `privileged: true` +- `/var/run/docker.sock:/var/run/docker.sock` +- `/:/host` + +只在明确需要这些能力时开启。公网部署时务必设置 `OPENCODE_SERVER_PASSWORD`。 + +### 从源码本地构建(可选) + +只有需要修改镜像内容、调试 Dockerfile,或不想使用预构建镜像时,才需要 clone 完整项目。 ```bash -# 克隆 git clone https://github.com/lehhair/OpenCodeUI.git cd OpenCodeUI -# 安装依赖 -npm install +# 复制并编辑环境变量,至少填写一个 LLM API Key +cp .env.example .env + +# 构建并启动全部服务 +docker compose -f docker-compose.yml -f docker-compose.build.yml up -d --build +``` + +如果本地构建后端并且需要宿主机 Docker 能力: + +```bash +docker compose -f docker-compose.yml -f docker-compose.build.yml -f docker-compose.host.yml up -d --build +``` + +### 环境持久化(简化版) + +默认后端只保留一个核心持久化卷:`opencode-home`(挂载到 `/root`)。 + +后端入口脚本会在启动时自动校验并补齐 `opencode` / `mise`,并在首次启动时写入 npm / pip 镜像配置。语言运行时不预装,按需用 `mise` 安装到持久化的 `/root`。 + +- OpenCode 配置与会话缓存 +- npm / cargo / pip 等用户态缓存 +- 通过 `mise` 安装的 Node / Python 多版本运行时 + +容器重建后,上述内容都会保留,不需要再拆成多个小卷。 + +首次进入后端容器可直接安装并固化运行时版本: + +```bash +docker compose exec backend mise use -g node@22 python@3.12 +docker compose exec backend node -v +docker compose exec backend python -V +``` -# 启动开发服务器 +`gateway` 仍保留单独卷 `opencode-router-data`,用于存放动态路由状态。 + +宿主机 Docker 能力由 `docker-compose.host.yml` 单独开启。不开启时,后端容器内虽然带 Docker CLI,但无法访问宿主机 Docker daemon。 + +### 环境变量 + +编辑 `.env` 文件,关键配置: + +```env +# LLM API Key(至少填一个) +ANTHROPIC_API_KEY= +OPENAI_API_KEY= + +# 端口 +GATEWAY_PORT=6658 +PREVIEW_PORT=6659 + +# 工作目录(挂载到容器 /workspace) +WORKSPACE=./workspace + +# 公网部署务必设置 +OPENCODE_SERVER_USERNAME=opencode +OPENCODE_SERVER_PASSWORD=your-strong-password + +# 路由服务 +ROUTER_SCAN_INTERVAL=5 +ROUTER_PORT_RANGE=3000-9999 +ROUTER_EXCLUDE_PORTS=4096 + +# Backend 工具链镜像/运行时源(可选) +NPM_CONFIG_REGISTRY=https://registry.npmmirror.com +PIP_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple +``` + +### 反向代理 + +Docker 默认监听 `127.0.0.1`,公网部署需在前面加反向代理。 + +**Nginx:** + +```nginx +server { + listen 443 ssl; + server_name opencode.example.com; + + ssl_certificate /path/to/cert.pem; + ssl_certificate_key /path/to/key.pem; + + location / { + proxy_pass http://127.0.0.1:6658; + proxy_http_version 1.1; + + # SSE(必须) + proxy_set_header Connection ''; + proxy_buffering off; + proxy_cache off; + + # WebSocket + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 86400s; + } +} + +# 预览(可选,建议绑独立域名) +server { + listen 443 ssl; + server_name preview.example.com; + + ssl_certificate /path/to/cert.pem; + ssl_certificate_key /path/to/key.pem; + + location / { + proxy_pass http://127.0.0.1:6659; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_read_timeout 86400s; + } +} +``` + +**Caddy:** + +```caddyfile +opencode.example.com { + reverse_proxy 127.0.0.1:6658 { + flush_interval -1 + } +} + +preview.example.com { + reverse_proxy 127.0.0.1:6659 +} +``` + +> **重要**:SSE 要求禁用缓冲。Nginx 需 `proxy_buffering off`,Caddy 需 `flush_interval -1`。 + +## 本地开发 + +需要一个运行中的 [OpenCode](https://github.com/anomalyco/opencode) 后端。 + +```bash +opencode serve + +# 另一个终端 +git clone https://github.com/lehhair/OpenCodeUI.git +cd OpenCodeUI +npm install npm run dev ``` -### Docker 部署 +Vite 启动在 `http://localhost:5173`,`/api` 自动代理到 `http://127.0.0.1:4096`。 + +### 提交前校验 + +提交 PR 前,建议先在本地跑一遍和 CI 相同的校验: ```bash -# 复制环境变量并填写 API Key -cp .env.example .env +npm run validate +``` -# 启动前后端容器 -docker compose up -d +这条命令会顺序执行 TypeScript 检查、ESLint、单元测试和生产构建。 + +如果你习惯 `type-check` 这个命名,也可以使用: + +```bash +npm run type-check ``` -前端默认端口 `3000`,后端 API 端口 `4096`。详见 `.env.example`。 +GitHub Actions 的 `Build Validation` workflow 会在 PR 和 `main` 分支 push 时运行同一套校验。 + +### 发版准备 + +正式发版时,优先使用下面这条命令,它会先跑完整校验,再执行版本号和 changelog 更新: + +```bash +npm run release:prepare -- 0.2.0 +``` + +命令完成后,再按提示执行 `git commit`、`git tag` 和 `git push`。 + +## 桌面应用 + +从 [Releases](https://github.com/lehhair/OpenCodeUI/releases) 下载安装包,或本地构建: + +```bash +npm install +npm run tauri build +``` + +> **macOS 用户注意**:安装后如果打开提示"无法验证开发者"而移入废纸篓,请在终端执行以下命令移除隔离属性: +> +> ```bash +> xattr -d com.apple.quarantine /Applications/OpenCode.app +> ``` ## 项目结构 ``` src/ -├── api/ # API 请求封装 -├── assets/ # 静态资源 -├── components/ # 通用组件(Terminal、CodeBlock、DiffView 等) -├── contexts/ # React Context -├── features/ # 业务功能模块 -│ ├── chat/ # 聊天界面(输入框、侧栏、消息区) -│ ├── message/ # 消息渲染(Markdown、工具调用) -│ ├── sessions/ # 会话管理 -│ ├── settings/ # 设置面板 -│ ├── attachment/ # 附件处理 -│ ├── mention/ # @ 提及 -│ └── slash-command/ # 斜杠命令 -├── hooks/ # 自定义 Hooks -├── store/ # 状态管理(主题、快捷键等) -├── themes/ # 主题预设定义 -├── types/ # TypeScript 类型 -├── utils/ # 工具函数 -└── workers/ # Web Workers +├── api/ # API 请求封装 +├── components/ # 通用组件(Terminal、DiffView 等) +├── features/ # 业务模块 +│ ├── chat/ # 聊天界面 +│ ├── message/ # 消息渲染 +│ ├── sessions/ # 会话管理 +│ ├── settings/ # 设置面板 +│ ├── mention/ # @ 提及 +│ └── slash-command/ # 斜杠命令 +├── hooks/ # 自定义 Hooks +├── store/ # 状态管理 +├── themes/ # 主题预设 +└── utils/ # 工具函数 + +src-tauri/ # Tauri 桌面应用(Rust) +docker/ # Docker 配置(Gateway / Frontend / Backend) ``` +## 设计说明 + +部分 UI 风格参考了 [Claude](https://claude.ai) 的界面设计。 + ## 许可证 -本项目基于 [GPL-3.0](./LICENSE) 协议开源。 +[GPL-3.0](./LICENSE) + +## Star History + + + + + + Star History Chart + + --- -*本项目由 Vibe Coding 驱动开发,如果你也对 AI 辅助编程感兴趣,欢迎交流。* +_本项目由 Vibe Coding 驱动开发,如果你也对 AI 辅助编程感兴趣,欢迎交流。_ diff --git a/README_EN.md b/README_EN.md new file mode 100644 index 00000000..4c09b2bd --- /dev/null +++ b/README_EN.md @@ -0,0 +1,342 @@ +# OpenCodeUI + +[中文](./README.md) | English + +A third-party Web frontend for [OpenCode](https://github.com/anomalyco/opencode). + +**This project is entirely built with AI-assisted programming (Vibe Coding)** — from the first line of code to the final release, all features were developed through conversations with AI. + +> **Disclaimer**: This project is for learning and communication purposes only. We are not responsible for any issues arising from the use of this project. The project is in its early stages and may contain bugs and instabilities. + +## Preview + +image +image + +## Features + +- **Full Chat Interface** — Message streaming, Markdown rendering, code highlighting (Shiki) +- **Built-in Terminal** — Web terminal based on xterm.js with WebGL rendering +- **File Browsing & Diff** — Browse workspace files, multi-file diff comparison +- **Theme System** — 3 built-in themes (Eucalyptus / Claude / Breeze), light/dark mode toggle and custom CSS +- **PWA Support** — Installable as a desktop/mobile app +- **Mobile Friendly** — Safe area handling, touch optimization, responsive layout +- **Browser Notifications** — Push notifications when AI replies are complete +- **@ Mentions & / Slash Commands** — Quickly reference files and execute commands in conversations +- **Custom Shortcuts** — Configurable key bindings +- **Docker Deployment** — Containerized frontend and backend separation, ready to use out of the box +- **Desktop App** — Native client based on Tauri (macOS / Linux / Windows) +- **Dynamic Port Routing** — Auto-discovery of dev services inside containers, generates preview links + +## Tech Stack + +| Category | Technology | +| ----------------- | ------------------------------ | +| Framework | React 19 + TypeScript | +| Build | Vite 7 | +| Styling | Tailwind CSS v4 | +| Code Highlighting | Shiki | +| Terminal | xterm.js (WebGL) | +| Markdown | react-markdown + remark-gfm | +| Desktop | Tauri 2 | +| Deployment | Docker (Caddy + Python Router) | + +## Quick Start + +No deployment needed — after starting the OpenCode backend locally, access the hosted frontend directly: + +```bash +opencode serve --cors "https://lehhair.github.io" +``` + +Then open https://lehhair.github.io/OpenCodeUI/ + +## Docker Deployment (Frontend Only) + +For scenarios where `opencode serve` is already running, you only need a frontend UI container to connect to the existing backend. + +```bash +git clone https://github.com/lehhair/OpenCodeUI.git +cd OpenCodeUI + +# Start (connects to host's opencode serve :4096 by default) +docker compose -f docker-compose.standalone.yml up -d +``` + +Visit `http://localhost:3000`. + +**Connect to a remote backend:** + +```bash +BACKEND_URL=your-server.com:4096 PORT=8080 docker compose -f docker-compose.standalone.yml up -d +``` + +| Environment Variable | Default | Description | +| -------------------- | --------------------------- | ------------------------------------------------ | +| `BACKEND_URL` | `host.docker.internal:4096` | opencode serve address (without protocol prefix) | +| `PORT` | `3000` | Frontend listening port | + +## Docker Deployment + +### Architecture & Ports + +The deployment consists of three services, unified through the Gateway: + +| Service | Port | Description | +| -------- | --------------------- | --------------------------------------------------- | +| Gateway | 6658 (`GATEWAY_PORT`) | Unified entry point, reverse proxy for all requests | +| Gateway | 6659 (`PREVIEW_PORT`) | Dev service preview | +| Frontend | 3000 (internal) | Static frontend | +| Backend | 4096 (internal) | OpenCode API | +| Router | 7070 (internal) | Dynamic port routing (built into Gateway) | + +### Gateway Routing Rules + +Requests on port `6658` are forwarded according to these rules: + +| Path | Target | Description | +| ------------ | -------------- | ------------------------------ | +| `/api/*` | Backend :4096 | OpenCode API, supports SSE | +| `/routes` | Router :7070 | Dynamic route management panel | +| `/preview/*` | Router :7070 | Preview port switching API | +| Other | Frontend :3000 | Frontend static assets | + +Port `6659` is used to access dev services inside the container. The Router automatically scans ports `3000-9999` and generates preview links via the `/p/{token}/` path. + +### Deployment Steps + +```bash +git clone https://github.com/lehhair/OpenCodeUI.git +cd OpenCodeUI + +# Copy and edit environment variables, fill in at least one LLM API Key +cp .env.example .env + +# Start +docker compose up -d +``` + +Visit `http://localhost:6658`. + +### Environment Persistence (Simplified) + +The backend now retains only one core persistent volume: `opencode-home` (mounted at `/root`). + +The backend entry script automatically verifies and supplements `opencode` / `mise` on startup to prevent toolchain loss after container rebuilds. + +- OpenCode configuration and session cache +- npm / cargo / pip and other user-space caches +- Node / Python multi-version runtimes installed via `mise` + +All of the above will be preserved after container rebuilds — no need to split into multiple small volumes. + +When upgrading from older versions, the original `opencode-data/opencode-config/opencode-cache/opencode-npm/opencode-cargo/opencode-local/opencode-opt` volumes become orphaned and can be manually cleaned up after confirming data has been migrated. + +First time entering the backend container, you can install and persist runtime versions directly: + +```bash +docker compose exec backend mise use -g node@22 python@3.12 +docker compose exec backend node -v +docker compose exec backend python -V +``` + +The `gateway` still retains a separate volume `opencode-router-data` for storing dynamic routing state. + +### Environment Variables + +Edit the `.env` file with the key configuration: + +```env +# LLM API Key (fill in at least one) +ANTHROPIC_API_KEY= +OPENAI_API_KEY= + +# Ports +GATEWAY_PORT=6658 +PREVIEW_PORT=6659 + +# Working directory (mounted to /workspace in the container) +WORKSPACE=./workspace + +# Must be set for public deployment +OPENCODE_SERVER_USERNAME=opencode +OPENCODE_SERVER_PASSWORD=your-strong-password + +# Router service +ROUTER_SCAN_INTERVAL=5 +ROUTER_PORT_RANGE=3000-9999 +ROUTER_EXCLUDE_PORTS=4096 +``` + +### Reverse Proxy + +Docker listens on `127.0.0.1` by default; public deployment requires a reverse proxy in front. + +**Nginx:** + +```nginx +server { + listen 443 ssl; + server_name opencode.example.com; + + ssl_certificate /path/to/cert.pem; + ssl_certificate_key /path/to/key.pem; + + location / { + proxy_pass http://127.0.0.1:6658; + proxy_http_version 1.1; + + # SSE (required) + proxy_set_header Connection ''; + proxy_buffering off; + proxy_cache off; + + # WebSocket + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 86400s; + } +} + +# Preview (optional, recommended to use a separate domain) +server { + listen 443 ssl; + server_name preview.example.com; + + ssl_certificate /path/to/cert.pem; + ssl_certificate_key /path/to/key.pem; + + location / { + proxy_pass http://127.0.0.1:6659; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_read_timeout 86400s; + } +} +``` + +**Caddy:** + +```caddyfile +opencode.example.com { + reverse_proxy 127.0.0.1:6658 { + flush_interval -1 + } +} + +preview.example.com { + reverse_proxy 127.0.0.1:6659 +} +``` + +> **Important**: SSE requires buffering to be disabled. Nginx needs `proxy_buffering off`, Caddy needs `flush_interval -1`. + +## Local Development + +Requires a running [OpenCode](https://github.com/anomalyco/opencode) backend. + +```bash +opencode serve + +# In another terminal +git clone https://github.com/lehhair/OpenCodeUI.git +cd OpenCodeUI +npm install +npm run dev +``` + +Vite starts at `http://localhost:5173`, `/api` is automatically proxied to `http://127.0.0.1:4096`. + +### Pre-PR Validation + +Before opening a PR, run the same validation steps locally that CI uses: + +```bash +npm run validate +``` + +This command runs TypeScript validation, ESLint, unit tests, and a production build in sequence. + +If you prefer the hyphenated name, this alias is also available: + +```bash +npm run type-check +``` + +GitHub Actions runs the same checks in the `Build Validation` workflow for every PR and every push to `main`. + +### Release Preparation + +For a real release, prefer the command below. It runs the full validation suite first, then updates versions and the changelog: + +```bash +npm run release:prepare -- 0.2.0 +``` + +After it finishes, follow the printed `git commit`, `git tag`, and `git push` steps. + +## Desktop App + +Download the installer from [Releases](https://github.com/lehhair/OpenCodeUI/releases), or build locally: + +```bash +npm install +npm run tauri build +``` + +> **macOS users**: If the app shows "cannot be opened because the developer cannot be verified" and gets moved to Trash, run this command in Terminal to remove the quarantine attribute: +> +> ```bash +> xattr -d com.apple.quarantine /Applications/OpenCode.app +> ``` + +## Project Structure + +``` +src/ +├── api/ # API request wrappers +├── components/ # Common components (Terminal, DiffView, etc.) +├── features/ # Business modules +│ ├── chat/ # Chat interface +│ ├── message/ # Message rendering +│ ├── sessions/ # Session management +│ ├── settings/ # Settings panel +│ ├── mention/ # @ mentions +│ └── slash-command/ # Slash commands +├── hooks/ # Custom Hooks +├── store/ # State management +├── themes/ # Theme presets +└── utils/ # Utility functions + +src-tauri/ # Tauri desktop app (Rust) +docker/ # Docker config (Gateway / Frontend / Backend) +``` + +## Design Notes + +Some UI styles are inspired by the [Claude](https://claude.ai) interface design. + +## License + +[GPL-3.0](./LICENSE) + +## Star History + + + + + + Star History Chart + + + +--- + +_This project is driven by Vibe Coding. If you're also interested in AI-assisted programming, feel free to connect._ diff --git a/assets/app-icons/app-icon-android-background.svg b/assets/app-icons/app-icon-android-background.svg new file mode 100644 index 00000000..d3a58c14 --- /dev/null +++ b/assets/app-icons/app-icon-android-background.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/app-icons/app-icon-android-foreground.svg b/assets/app-icons/app-icon-android-foreground.svg new file mode 100644 index 00000000..ad21ada0 --- /dev/null +++ b/assets/app-icons/app-icon-android-foreground.svg @@ -0,0 +1,19 @@ + + + + diff --git a/assets/app-icons/app-icon.svg b/assets/app-icons/app-icon.svg new file mode 100644 index 00000000..e0f3fdf6 --- /dev/null +++ b/assets/app-icons/app-icon.svg @@ -0,0 +1,20 @@ + + + + + diff --git a/docker-compose.build.yml b/docker-compose.build.yml new file mode 100644 index 00000000..2ffdc096 --- /dev/null +++ b/docker-compose.build.yml @@ -0,0 +1,31 @@ +# ============================================ +# 本地构建 override(gateway/frontend/backend) +# ============================================ +# 用法: docker compose -f docker-compose.yml -f docker-compose.build.yml up -d --build + +services: + gateway: + image: opencodeui-gateway:local + build: + context: . + dockerfile: docker/Dockerfile.gateway + + frontend: + image: opencodeui-frontend:local + build: + context: . + dockerfile: docker/Dockerfile.frontend + + backend: + image: opencodeui-backend:local + build: + context: . + dockerfile: docker/Dockerfile.backend + args: + USE_CHINA_MIRROR: ${USE_CHINA_MIRROR:-1} + APT_MIRROR: ${APT_MIRROR:-http://mirrors.tuna.tsinghua.edu.cn/debian} + APT_SECURITY_MIRROR: ${APT_SECURITY_MIRROR:-http://mirrors.tuna.tsinghua.edu.cn/debian-security} + NPM_CONFIG_REGISTRY: ${NPM_CONFIG_REGISTRY:-https://registry.npmmirror.com} + NODEJS_ORG_MIRROR: ${NODEJS_ORG_MIRROR:-https://npmmirror.com/mirrors/node} + MISE_INSTALL_URL: ${MISE_INSTALL_URL:-https://mise.run} + OPENCODE_INSTALL_URL: ${OPENCODE_INSTALL_URL:-https://opencode.ai/install} diff --git a/docker-compose.host.yml b/docker-compose.host.yml new file mode 100644 index 00000000..12f6cbf7 --- /dev/null +++ b/docker-compose.host.yml @@ -0,0 +1,15 @@ +# ============================================ +# 宿主机能力 override(可选) +# ============================================ +# 用法: +# docker compose -f docker-compose.yml -f docker-compose.host.yml up -d +# +# 开启后 backend 可以访问宿主机 Docker daemon 和宿主机文件系统。 +# 只在确实需要容器内构建/管理 Docker、访问宿主机路径时使用。 + +services: + backend: + privileged: true + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - /:/host diff --git a/docker-compose.standalone.yml b/docker-compose.standalone.yml new file mode 100644 index 00000000..16af6d2f --- /dev/null +++ b/docker-compose.standalone.yml @@ -0,0 +1,25 @@ +# ============================================ +# OpenCode WebUI - 纯前端独立部署 +# ============================================ +# 适用于已有 opencode serve 的场景,只需前端 UI +# +# 使用方法: +# docker compose -f docker-compose.standalone.yml up -d +# +# 环境变量: +# BACKEND_URL - opencode serve 地址(默认 host.docker.internal:4096) +# PORT - 前端监听端口(默认 3000) + +services: + frontend: + image: ghcr.io/lehhair/opencodeui-frontend:latest + container_name: opencodeui + restart: unless-stopped + ports: + - '${PORT:-3000}:3000' + command: ['caddy', 'run', '--config', '/etc/caddy/Caddyfile.standalone'] + environment: + - BACKEND_URL=${BACKEND_URL:-host.docker.internal:4096} + # Linux 需要 host.docker.internal 支持 + extra_hosts: + - 'host.docker.internal:host-gateway' diff --git a/docker-compose.yml b/docker-compose.yml index d83c8de4..4286d1c1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,43 +3,68 @@ # ============================================ # # 架构: -# frontend (Caddy) :3000 -> 静态文件 -# backend (Ubuntu) :4096 -> opencode serve -# -# 两个独立容器,各自暴露端口给宿主机 nginx 反代 +# gateway (Caddy) :6658 -> 统一入口(反代 + 端口路由) +# :6659 -> 独立预览端口(前端开发服务专用) +# frontend (Caddy) :3000 -> 静态文件 (GitHub Actions 预构建镜像) +# backend (opencode):4096 -> opencode serve (GitHub Actions 预构建镜像) # # 使用方法: # cp .env.example .env # 填写 API Key # docker compose up -d # 启动 +# +# 本地构建全部服务 (可选): +# docker compose -f docker-compose.yml -f docker-compose.build.yml up -d --build services: - # ---- 前端:静态文件 ---- + # ---- 统一入口:网关 ---- + gateway: + image: ghcr.io/lehhair/opencodeui-gateway:latest + container_name: opencode-gateway + restart: unless-stopped + ports: + - '127.0.0.1:${GATEWAY_PORT:-6658}:6658' + - '127.0.0.1:${PREVIEW_PORT:-6659}:6659' + volumes: + - opencode-router-data:/data + - /var/run/docker.sock:/var/run/docker.sock:ro + depends_on: + - frontend + - backend + environment: + - TARGET_CONTAINER=opencode-backend + - ROUTER_STATE_FILE=/data/routes.json + - ROUTER_SCAN_INTERVAL=${ROUTER_SCAN_INTERVAL:-5} + - ROUTER_TOKEN_LENGTH=${ROUTER_TOKEN_LENGTH:-12} + - ROUTER_PORT_RANGE=${ROUTER_PORT_RANGE:-3000-9999} + - ROUTER_EXCLUDE_PORTS=${ROUTER_EXCLUDE_PORTS:-4096} + - PUBLIC_BASE_URL=${PUBLIC_BASE_URL:-} + - PREVIEW_DOMAIN=${PREVIEW_DOMAIN:-} + - ROUTER_USERNAME=${OPENCODE_SERVER_USERNAME:-opencode} + - ROUTER_PASSWORD=${OPENCODE_SERVER_PASSWORD:-} + networks: + - opencode-net + + # ---- 前端:预构建镜像 ---- frontend: - build: - context: . - dockerfile: docker/Dockerfile.frontend + image: ghcr.io/lehhair/opencodeui-frontend:latest container_name: opencode-frontend restart: unless-stopped - ports: - - "127.0.0.1:${FRONTEND_PORT:-3000}:3000" + networks: + - opencode-net - # ---- 后端:opencode serve ---- + # ---- 后端:预构建镜像 ---- backend: - build: - context: . - dockerfile: docker/Dockerfile.backend + image: ghcr.io/lehhair/opencodeui-backend:latest container_name: opencode-backend restart: unless-stopped - ports: - - "127.0.0.1:${API_PORT:-4096}:4096" deploy: resources: limits: memory: 4G volumes: - ${WORKSPACE:-./workspace}:/workspace - - opencode-data:/root/.local/share/opencode - - opencode-config:/root/.config/opencode + - opencode-home:/root + working_dir: /workspace environment: - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:-} - OPENAI_API_KEY=${OPENAI_API_KEY:-} @@ -50,8 +75,26 @@ services: - XAI_API_KEY=${XAI_API_KEY:-} - OPENCODE_SERVER_PASSWORD=${OPENCODE_SERVER_PASSWORD:-} - OPENCODE_SERVER_USERNAME=${OPENCODE_SERVER_USERNAME:-opencode} - - WORKSPACE=/workspace + - DOCKER_API_VERSION=${DOCKER_API_VERSION:-1.40} + - NPM_CONFIG_REGISTRY=${NPM_CONFIG_REGISTRY:-https://registry.npmmirror.com} + - NODEJS_ORG_MIRROR=${NODEJS_ORG_MIRROR:-https://npmmirror.com/mirrors/node} + - PIP_INDEX_URL=${PIP_INDEX_URL:-https://pypi.tuna.tsinghua.edu.cn/simple} + - PIP_TRUSTED_HOST=${PIP_TRUSTED_HOST:-pypi.tuna.tsinghua.edu.cn} + - LANG=C.UTF-8 + - LC_ALL=C.UTF-8 + - TERM=xterm-256color + - COLORTERM=truecolor + - MISE_DATA_DIR=/root/.local/share/mise + - MISE_CONFIG_DIR=/root/.config/mise + - MISE_STATE_DIR=/root/.local/state/mise + - PATH=/root/.local/bin:/root/.cargo/bin:/root/.local/share/mise/shims:/root/.local/share/mise/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin + networks: + - opencode-net volumes: - opencode-data: - opencode-config: + opencode-home: + opencode-router-data: + +networks: + opencode-net: + name: opencode-net diff --git a/docker/Caddyfile b/docker/Caddyfile index 12c24b21..d6cc8726 100644 --- a/docker/Caddyfile +++ b/docker/Caddyfile @@ -2,11 +2,18 @@ # 纯静态文件服务 :3000 { - root * /srv - file_server + @api path /api /api/* + handle @api { + respond "API is not served by the frontend-only image" 404 + } - # SPA 路由 - 所有非文件请求返回 index.html - try_files {path} /index.html + handle { + root * /srv + file_server + + # SPA 路由 - 所有非文件请求返回 index.html + try_files {path} /index.html + } encode gzip } diff --git a/docker/Caddyfile.gateway b/docker/Caddyfile.gateway new file mode 100644 index 00000000..a73ee0ff --- /dev/null +++ b/docker/Caddyfile.gateway @@ -0,0 +1,36 @@ +{ + auto_https off +} + +# ---- 主入口 :6658 — OpenCode 应用 ---- +:6658 { + # API(SSE + WebSocket 自动处理) + handle_path /api/* { + reverse_proxy opencode-backend:4096 { + flush_interval -1 + } + } + + # Routes UI + Preview API(供面板切换预览端口) + @router path /routes /preview/* + handle @router { + reverse_proxy 127.0.0.1:7070 + } + + # 前端(兜底) + handle { + reverse_proxy opencode-frontend:3000 + } +} + +# ---- 开发服务入口 :6659 — 绑独立域名 ---- +# 路径穿透 /p/{token}/ + 独占预览(兜底) +:6659 { + # 动态端口路由(由 router 自动生成) + import /etc/caddy/routes.conf + + # 独占预览(兜底:未匹配路径穿透的请求走 preview 端口) + handle { + import /etc/caddy/preview.conf + } +} diff --git a/docker/Caddyfile.standalone b/docker/Caddyfile.standalone new file mode 100644 index 00000000..2627b9d5 --- /dev/null +++ b/docker/Caddyfile.standalone @@ -0,0 +1,21 @@ +# OpenCode WebUI - 纯前端独立部署 +# 静态文件 + API 反代到外部 opencode serve + +:3000 { + # API 反代(strip /api 前缀,SSE 需要 flush_interval -1) + handle_path /api/* { + reverse_proxy {$BACKEND_URL:host.docker.internal:4096} { + header_up Host {upstream_hostport} + flush_interval -1 + } + } + + # 前端静态文件 + handle { + root * /srv + file_server + try_files {path} /index.html + } + + encode gzip +} diff --git a/docker/Dockerfile.backend b/docker/Dockerfile.backend index 77ea63d3..f7a838cf 100644 --- a/docker/Dockerfile.backend +++ b/docker/Dockerfile.backend @@ -1,34 +1,114 @@ # ============================================ # OpenCode WebUI - Backend # ============================================ -# Ubuntu + opencode serve +# Debian slim + opencode serve + agent tooling -FROM ubuntu:24.04 +FROM docker:29-cli AS docker-cli -ENV DEBIAN_FRONTEND=noninteractive +FROM debian:bookworm-slim -# 基础依赖 +ARG USE_CHINA_MIRROR=0 +ARG APT_MIRROR=http://mirrors.tuna.tsinghua.edu.cn/debian +ARG APT_SECURITY_MIRROR=http://mirrors.tuna.tsinghua.edu.cn/debian-security +ARG NPM_CONFIG_REGISTRY=https://registry.npmmirror.com +ARG NODEJS_ORG_MIRROR=https://npmmirror.com/mirrors/node +ARG MISE_INSTALL_URL=https://mise.run +ARG OPENCODE_INSTALL_URL=https://opencode.ai/install + +ENV DEBIAN_FRONTEND=noninteractive \ + LANG=C.UTF-8 \ + LC_ALL=C.UTF-8 \ + TERM=xterm-256color \ + COLORTERM=truecolor \ + EDITOR=vim \ + NPM_CONFIG_REGISTRY=${NPM_CONFIG_REGISTRY} \ + NODEJS_ORG_MIRROR=${NODEJS_ORG_MIRROR} + +RUN if [ "${USE_CHINA_MIRROR}" = "1" ] && [ -f /etc/apt/sources.list.d/debian.sources ]; then \ + sed -i \ + -e "s|http://deb.debian.org/debian|${APT_MIRROR}|g" \ + -e "s|http://security.debian.org/debian-security|${APT_SECURITY_MIRROR}|g" \ + /etc/apt/sources.list.d/debian.sources; \ + fi + +# Agent 常用基础环境。语言运行时交给 mise,安装到持久化的 /root。 RUN apt-get update && apt-get install -y --no-install-recommends \ + bash \ ca-certificates \ curl \ + dnsutils \ + fd-find \ + file \ + fonts-dejavu-core \ + fonts-liberation \ + fonts-noto-cjk \ + fonts-noto-color-emoji \ + g++ \ git \ + git-lfs \ + gnupg \ + jq \ + less \ + make \ + nano \ + openssh-client \ + patch \ + procps \ + python3 \ + python3-venv \ + ripgrep \ + rsync \ + tini \ + tzdata \ unzip \ + vim-tiny \ + wget \ + xz-utils \ + zip \ && rm -rf /var/lib/apt/lists/* -# 安装 opencode -RUN curl -fsSL https://opencode.ai/install | bash \ - && OPENCODE_BIN=$(find /root -name opencode -type f 2>/dev/null | head -1) \ - && if [ -n "$OPENCODE_BIN" ]; then ln -sf "$OPENCODE_BIN" /usr/local/bin/opencode; fi \ +# Docker CLI + Compose/Buildx plugin,只复制客户端,不安装 dockerd/containerd/runc。 +RUN mkdir -p /usr/local/libexec/docker/cli-plugins +COPY --from=docker-cli /usr/local/bin/docker /usr/local/bin/docker +COPY --from=docker-cli /usr/local/libexec/docker/cli-plugins/docker-compose /usr/local/libexec/docker/cli-plugins/docker-compose +COPY --from=docker-cli /usr/local/libexec/docker/cli-plugins/docker-buildx /usr/local/libexec/docker/cli-plugins/docker-buildx + +# Debian 的 fd 二进制叫 fdfind,补一个常见命令名。 +RUN ln -sf /usr/bin/fdfind /usr/local/bin/fd + +# 安装 mise。GitHub Actions 等 CI 环境会在构建时下载,运行时安装的工具仍落在持久化 /root。 +RUN curl -fsSL "${MISE_INSTALL_URL}" | sh \ + && MISE_BIN="$(find /root -name mise -type f 2>/dev/null | head -1)" \ + && if [ -z "${MISE_BIN}" ]; then echo "mise binary not found" >&2; exit 1; fi \ + && cp "${MISE_BIN}" /usr/local/bin/mise \ + && chmod +x /usr/local/bin/mise \ + && mise --version + +# 安装 opencode。 +RUN curl -fsSL "${OPENCODE_INSTALL_URL}" | bash \ + && OPENCODE_BIN="$(find /root -name opencode -type f 2>/dev/null | head -1)" \ + && if [ -z "${OPENCODE_BIN}" ]; then echo "opencode binary not found" >&2; exit 1; fi \ + && cp "${OPENCODE_BIN}" /usr/local/bin/opencode \ + && chmod +x /usr/local/bin/opencode \ && opencode --version # 工作目录 RUN mkdir -p /workspace WORKDIR /workspace -ENV OPENCODE_DISABLE_AUTOUPDATE=true \ +ENV MISE_DATA_DIR=/root/.local/share/mise \ + MISE_CONFIG_DIR=/root/.config/mise \ + MISE_STATE_DIR=/root/.local/state/mise \ + OPENCODE_DISABLE_AUTOUPDATE=true \ OPENCODE_DISABLE_TERMINAL_TITLE=true \ WORKSPACE=/workspace +ENV PATH="/root/.local/bin:/root/.cargo/bin:/root/.local/share/mise/shims:/root/.local/share/mise/bin:${PATH}" + +COPY ./docker/backend-entrypoint.sh /usr/local/bin/backend-entrypoint.sh +RUN chmod +x /usr/local/bin/backend-entrypoint.sh + EXPOSE 4096 +ENTRYPOINT ["tini", "--", "/usr/local/bin/backend-entrypoint.sh"] CMD ["opencode", "serve", "--port", "4096", "--hostname", "0.0.0.0"] diff --git a/docker/Dockerfile.frontend b/docker/Dockerfile.frontend index 3676274b..f932e5b3 100644 --- a/docker/Dockerfile.frontend +++ b/docker/Dockerfile.frontend @@ -4,12 +4,32 @@ # 纯前端静态文件 + Caddy 反向代理 API # ---- Stage 1: 构建前端 ---- -FROM node:22-alpine AS builder +# 前端产物是静态文件,和目标 CPU 架构无关。 +# 多架构镜像构建时让 Node builder 始终跑在构建机原生平台,避免 arm64 通过 QEMU 重跑 npm/vite 构建。 +FROM --platform=$BUILDPLATFORM node:22-alpine AS builder + +ARG NPM_REGISTRY=https://registry.npmjs.org/ +ARG NPM_REGISTRY_FALLBACK=https://registry.npmmirror.com/ WORKDIR /app COPY package.json package-lock.json ./ -RUN npm ci --ignore-scripts +RUN set -eux; \ + npm config set registry "${NPM_REGISTRY}"; \ + npm config set fetch-retries 5; \ + npm config set fetch-retry-factor 2; \ + npm config set fetch-retry-mintimeout 10000; \ + npm config set fetch-retry-maxtimeout 120000; \ + npm config set audit false; \ + npm config set fund false; \ + if npm ci --ignore-scripts; then \ + exit 0; \ + fi; \ + echo "npm ci failed on ${NPM_REGISTRY}, retrying with ${NPM_REGISTRY_FALLBACK}"; \ + npm cache clean --force; \ + npm config set registry "${NPM_REGISTRY_FALLBACK}"; \ + npm ci --ignore-scripts COPY . . +RUN node scripts/copy-material-icons.mjs ENV VITE_API_BASE_URL=/api RUN npm run build @@ -19,8 +39,9 @@ FROM caddy:2-alpine # 前端静态文件 COPY --from=builder /app/dist /srv -# Caddy 配置 +# Caddy 配置(默认 + standalone) COPY docker/Caddyfile /etc/caddy/Caddyfile +COPY docker/Caddyfile.standalone /etc/caddy/Caddyfile.standalone EXPOSE 3000 diff --git a/docker/Dockerfile.gateway b/docker/Dockerfile.gateway new file mode 100644 index 00000000..29595064 --- /dev/null +++ b/docker/Dockerfile.gateway @@ -0,0 +1,27 @@ +FROM rust:1.94-alpine AS router-builder + +RUN apk add --no-cache musl-dev + +WORKDIR /build + +COPY src-router/Cargo.toml src-router/Cargo.lock ./ +COPY src-router/src ./src + +RUN cargo build --release --locked + +FROM caddy:2-alpine + +RUN apk add --no-cache docker-cli + +WORKDIR /app + +COPY --from=router-builder /build/target/release/opencodeui-router /usr/local/bin/opencodeui-router +COPY docker/entrypoint-gateway.sh /app/entrypoint.sh +RUN chmod +x /app/entrypoint.sh +COPY docker/Caddyfile.gateway /etc/caddy/Caddyfile + +RUN touch /etc/caddy/routes.conf && touch /etc/caddy/preview.conf + +EXPOSE 6658 6659 + +CMD ["/app/entrypoint.sh"] diff --git a/docker/backend-entrypoint.sh b/docker/backend-entrypoint.sh new file mode 100644 index 00000000..b17f9e0c --- /dev/null +++ b/docker/backend-entrypoint.sh @@ -0,0 +1,86 @@ +#!/usr/bin/env bash +set -eu + +ensure_mise() { + if command -v mise >/dev/null 2>&1; then + return + fi + + if [ -x /root/.local/bin/mise ]; then + rm -f /usr/local/bin/mise + cp /root/.local/bin/mise /usr/local/bin/mise + chmod +x /usr/local/bin/mise + return + fi + + curl -fsSL "${MISE_INSTALL_URL:-https://mise.run}" | sh + + MISE_BIN="$(find /root -name mise -type f 2>/dev/null | head -1)" + if [ -z "${MISE_BIN}" ]; then + echo "mise install failed: binary not found" >&2 + exit 1 + fi + + rm -f /usr/local/bin/mise + cp "${MISE_BIN}" /usr/local/bin/mise + chmod +x /usr/local/bin/mise +} + +ensure_opencode() { + if opencode --version >/dev/null 2>&1; then + return + fi + + OPENCODE_BIN="$(find /root -name opencode -type f 2>/dev/null | head -1)" + if [ -n "${OPENCODE_BIN}" ]; then + rm -f /usr/local/bin/opencode + cp "${OPENCODE_BIN}" /usr/local/bin/opencode + chmod +x /usr/local/bin/opencode + if opencode --version >/dev/null 2>&1; then + return + fi + fi + + curl -fsSL "${OPENCODE_INSTALL_URL:-https://opencode.ai/install}" | bash + + OPENCODE_BIN="$(find /root -name opencode -type f 2>/dev/null | head -1)" + if [ -z "${OPENCODE_BIN}" ]; then + echo "opencode install failed: binary not found" >&2 + exit 1 + fi + + rm -f /usr/local/bin/opencode + cp "${OPENCODE_BIN}" /usr/local/bin/opencode + chmod +x /usr/local/bin/opencode +} + +ensure_package_mirrors() { + mkdir -p /root/.config/pip + + if [ ! -f /root/.npmrc ]; then + cat > /root/.npmrc < /root/.config/pip/pip.conf </dev/null + wait "$ROUTER_PID" "$CADDY_PID" 2>/dev/null + exit 1 +} + +trap cleanup TERM INT + +opencodeui-router & +ROUTER_PID=$! + +caddy run --config /etc/caddy/Caddyfile & +CADDY_PID=$! + +# Wait for either process to exit, then tear down the other. +# `wait -n` is not available in busybox sh, so poll both. +while kill -0 "$ROUTER_PID" 2>/dev/null && kill -0 "$CADDY_PID" 2>/dev/null; do + wait -n "$ROUTER_PID" "$CADDY_PID" 2>/dev/null || sleep 1 +done + +echo "One of the processes exited, shutting down..." +cleanup diff --git a/eslint.config.js b/eslint.config.js index 5e6b472f..0a0062f5 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,4 +1,5 @@ import js from '@eslint/js' +import eslintConfigPrettier from 'eslint-config-prettier' import globals from 'globals' import reactHooks from 'eslint-plugin-react-hooks' import reactRefresh from 'eslint-plugin-react-refresh' @@ -6,7 +7,7 @@ import tseslint from 'typescript-eslint' import { defineConfig, globalIgnores } from 'eslint/config' export default defineConfig([ - globalIgnores(['dist']), + globalIgnores(['dist', 'node_modules', 'public/material-icons', 'src-tauri/target/**']), { files: ['**/*.{ts,tsx}'], extends: [ @@ -14,10 +15,31 @@ export default defineConfig([ tseslint.configs.recommended, reactHooks.configs.flat.recommended, reactRefresh.configs.vite, + eslintConfigPrettier, ], languageOptions: { ecmaVersion: 2020, globals: globals.browser, }, + rules: { + '@typescript-eslint/ban-ts-comment': 'warn', + '@typescript-eslint/no-explicit-any': 'warn', + '@typescript-eslint/no-unused-vars': [ + 'warn', + { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + caughtErrorsIgnorePattern: '^_', + }, + ], + 'prefer-const': 'warn', + 'react-hooks/exhaustive-deps': 'warn', + 'react-hooks/immutability': 'warn', + 'react-hooks/preserve-manual-memoization': 'off', + 'react-hooks/refs': 'warn', + 'react-hooks/set-state-in-effect': 'warn', + 'react-hooks/use-memo': 'warn', + 'react-refresh/only-export-components': 'warn', + }, }, ]) diff --git a/index.html b/index.html index b14bd5d4..d3fa4c60 100644 --- a/index.html +++ b/index.html @@ -3,13 +3,16 @@ - + OpenCode - + - + @@ -17,6 +20,8 @@ + +
diff --git a/openapi_doc.json b/openapi_doc.json index b37b3079..091c0f78 100644 --- a/openapi_doc.json +++ b/openapi_doc.json @@ -1 +1,5378 @@ -{"openapi":"3.1.1","info":{"title":"opencode","description":"opencode api","version":"0.0.3"},"paths":{"/global/health":{"get":{"operationId":"global.health","summary":"Get health","description":"Get health information about the OpenCode server.","responses":{"200":{"description":"Health information","content":{"application/json":{"schema":{"type":"object","properties":{"healthy":{"type":"boolean","const":true},"version":{"type":"string"}},"required":["healthy","version"]}}}}}}},"/global/event":{"get":{"operationId":"global.event","summary":"Get global events","description":"Subscribe to global events from the OpenCode system using server-sent events.","responses":{"200":{"description":"Event stream","content":{"text/event-stream":{"schema":{"$ref":"#/components/schemas/GlobalEvent"}}}}}}},"/global/config":{"get":{"operationId":"global.config.get","summary":"Get global configuration","description":"Retrieve the current global OpenCode configuration settings and preferences.","responses":{"200":{"description":"Get global config info","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Config"}}}}}},"patch":{"operationId":"global.config.update","summary":"Update global configuration","description":"Update global OpenCode configuration settings and preferences.","responses":{"200":{"description":"Successfully updated global config","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Config"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BadRequestError"}}}}},"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Config"}}}}}},"/global/dispose":{"post":{"operationId":"global.dispose","summary":"Dispose instance","description":"Clean up and dispose all OpenCode instances, releasing all resources.","responses":{"200":{"description":"Global disposed","content":{"application/json":{"schema":{"type":"boolean"}}}}}}},"/auth/{providerID}":{"put":{"operationId":"auth.set","summary":"Set auth credentials","description":"Set authentication credentials","responses":{"200":{"description":"Successfully set authentication credentials","content":{"application/json":{"schema":{"type":"boolean"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BadRequestError"}}}}},"parameters":[{"in":"path","name":"providerID","schema":{"type":"string"},"required":true}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Auth"}}}}},"delete":{"operationId":"auth.remove","summary":"Remove auth credentials","description":"Remove authentication credentials","responses":{"200":{"description":"Successfully removed authentication credentials","content":{"application/json":{"schema":{"type":"boolean"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BadRequestError"}}}}},"parameters":[{"in":"path","name":"providerID","schema":{"type":"string"},"required":true}]}},"/project":{"get":{"operationId":"project.list","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}}],"summary":"List all projects","description":"Get a list of projects that have been opened with OpenCode.","responses":{"200":{"description":"List of projects","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Project"}}}}}}}},"/project/current":{"get":{"operationId":"project.current","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}}],"summary":"Get current project","description":"Retrieve the currently active project that OpenCode is working with.","responses":{"200":{"description":"Current project information","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Project"}}}}}}},"/project/{projectID}":{"patch":{"operationId":"project.update","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}},{"in":"path","name":"projectID","schema":{"type":"string"},"required":true}],"summary":"Update project","description":"Update project properties such as name, icon, and commands.","responses":{"200":{"description":"Updated project information","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Project"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BadRequestError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NotFoundError"}}}}},"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"icon":{"type":"object","properties":{"url":{"type":"string"},"override":{"type":"string"},"color":{"type":"string"}}},"commands":{"type":"object","properties":{"start":{"description":"Startup script to run when creating a new workspace (worktree)","type":"string"}}}}}}}}}},"/pty":{"get":{"operationId":"pty.list","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}}],"summary":"List PTY sessions","description":"Get a list of all active pseudo-terminal (PTY) sessions managed by OpenCode.","responses":{"200":{"description":"List of sessions","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Pty"}}}}}}},"post":{"operationId":"pty.create","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}}],"summary":"Create PTY session","description":"Create a new pseudo-terminal (PTY) session for running shell commands and processes.","responses":{"200":{"description":"Created session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Pty"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BadRequestError"}}}}},"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"command":{"type":"string"},"args":{"type":"array","items":{"type":"string"}},"cwd":{"type":"string"},"title":{"type":"string"},"env":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}}}}}}}}},"/pty/{ptyID}":{"get":{"operationId":"pty.get","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}},{"in":"path","name":"ptyID","schema":{"type":"string"},"required":true}],"summary":"Get PTY session","description":"Retrieve detailed information about a specific pseudo-terminal (PTY) session.","responses":{"200":{"description":"Session info","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Pty"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NotFoundError"}}}}}},"put":{"operationId":"pty.update","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}},{"in":"path","name":"ptyID","schema":{"type":"string"},"required":true}],"summary":"Update PTY session","description":"Update properties of an existing pseudo-terminal (PTY) session.","responses":{"200":{"description":"Updated session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Pty"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BadRequestError"}}}}},"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string"},"size":{"type":"object","properties":{"rows":{"type":"number"},"cols":{"type":"number"}},"required":["rows","cols"]}}}}}}},"delete":{"operationId":"pty.remove","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}},{"in":"path","name":"ptyID","schema":{"type":"string"},"required":true}],"summary":"Remove PTY session","description":"Remove and terminate a specific pseudo-terminal (PTY) session.","responses":{"200":{"description":"Session removed","content":{"application/json":{"schema":{"type":"boolean"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NotFoundError"}}}}}}},"/pty/{ptyID}/connect":{"get":{"operationId":"pty.connect","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}},{"in":"path","name":"ptyID","schema":{"type":"string"},"required":true}],"summary":"Connect to PTY session","description":"Establish a WebSocket connection to interact with a pseudo-terminal (PTY) session in real-time.","responses":{"200":{"description":"Connected session","content":{"application/json":{"schema":{"type":"boolean"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NotFoundError"}}}}}}},"/config":{"get":{"operationId":"config.get","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}}],"summary":"Get configuration","description":"Retrieve the current OpenCode configuration settings and preferences.","responses":{"200":{"description":"Get config info","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Config"}}}}}},"patch":{"operationId":"config.update","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}}],"summary":"Update configuration","description":"Update OpenCode configuration settings and preferences.","responses":{"200":{"description":"Successfully updated config","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Config"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BadRequestError"}}}}},"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Config"}}}}}},"/config/providers":{"get":{"operationId":"config.providers","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}}],"summary":"List config providers","description":"Get a list of all configured AI providers and their default models.","responses":{"200":{"description":"List of providers","content":{"application/json":{"schema":{"type":"object","properties":{"providers":{"type":"array","items":{"$ref":"#/components/schemas/Provider"}},"default":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}}},"required":["providers","default"]}}}}}}},"/experimental/tool/ids":{"get":{"operationId":"tool.ids","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}}],"summary":"List tool IDs","description":"Get a list of all available tool IDs, including both built-in tools and dynamically registered tools.","responses":{"200":{"description":"Tool IDs","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ToolIDs"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BadRequestError"}}}}}}},"/experimental/tool":{"get":{"operationId":"tool.list","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}},{"in":"query","name":"provider","schema":{"type":"string"},"required":true},{"in":"query","name":"model","schema":{"type":"string"},"required":true}],"summary":"List tools","description":"Get a list of available tools with their JSON schema parameters for a specific provider and model combination.","responses":{"200":{"description":"Tools","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ToolList"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BadRequestError"}}}}}}},"/experimental/worktree":{"post":{"operationId":"worktree.create","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}}],"summary":"Create worktree","description":"Create a new git worktree for the current project and run any configured startup scripts.","responses":{"200":{"description":"Worktree created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Worktree"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BadRequestError"}}}}},"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorktreeCreateInput"}}}}},"get":{"operationId":"worktree.list","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}}],"summary":"List worktrees","description":"List all sandbox worktrees for the current project.","responses":{"200":{"description":"List of worktree directories","content":{"application/json":{"schema":{"type":"array","items":{"type":"string"}}}}}}},"delete":{"operationId":"worktree.remove","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}}],"summary":"Remove worktree","description":"Remove a git worktree and delete its branch.","responses":{"200":{"description":"Worktree removed","content":{"application/json":{"schema":{"type":"boolean"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BadRequestError"}}}}},"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorktreeRemoveInput"}}}}}},"/experimental/worktree/reset":{"post":{"operationId":"worktree.reset","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}}],"summary":"Reset worktree","description":"Reset a worktree branch to the primary default branch.","responses":{"200":{"description":"Worktree reset","content":{"application/json":{"schema":{"type":"boolean"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BadRequestError"}}}}},"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorktreeResetInput"}}}}}},"/experimental/resource":{"get":{"operationId":"experimental.resource.list","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}}],"summary":"Get MCP resources","description":"Get all available MCP resources from connected servers. Optionally filter by name.","responses":{"200":{"description":"MCP resources","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"$ref":"#/components/schemas/McpResource"}}}}}}}},"/session":{"get":{"operationId":"session.list","parameters":[{"in":"query","name":"directory","schema":{"type":"string"},"description":"Filter sessions by project directory"},{"in":"query","name":"roots","schema":{"type":"boolean"},"description":"Only return root sessions (no parentID)"},{"in":"query","name":"start","schema":{"type":"number"},"description":"Filter sessions updated on or after this timestamp (milliseconds since epoch)"},{"in":"query","name":"search","schema":{"type":"string"},"description":"Filter sessions by title (case-insensitive)"},{"in":"query","name":"limit","schema":{"type":"number"},"description":"Maximum number of sessions to return"}],"summary":"List sessions","description":"Get a list of all OpenCode sessions, sorted by most recently updated.","responses":{"200":{"description":"List of sessions","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Session"}}}}}}},"post":{"operationId":"session.create","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}}],"summary":"Create session","description":"Create a new OpenCode session for interacting with AI assistants and managing conversations.","responses":{"200":{"description":"Successfully created session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Session"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BadRequestError"}}}}},"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"parentID":{"type":"string","pattern":"^ses.*"},"title":{"type":"string"},"permission":{"$ref":"#/components/schemas/PermissionRuleset"}}}}}}}},"/session/status":{"get":{"operationId":"session.status","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}}],"summary":"Get session status","description":"Retrieve the current status of all sessions, including active, idle, and completed states.","responses":{"200":{"description":"Get session status","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"$ref":"#/components/schemas/SessionStatus"}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BadRequestError"}}}}}}},"/session/{sessionID}":{"get":{"operationId":"session.get","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}},{"in":"path","name":"sessionID","schema":{"type":"string","pattern":"^ses.*"},"required":true}],"summary":"Get session","description":"Retrieve detailed information about a specific OpenCode session.","tags":["Session"],"responses":{"200":{"description":"Get session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Session"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BadRequestError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NotFoundError"}}}}}},"delete":{"operationId":"session.delete","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}},{"in":"path","name":"sessionID","schema":{"type":"string","pattern":"^ses.*"},"required":true}],"summary":"Delete session","description":"Delete a session and permanently remove all associated data, including messages and history.","responses":{"200":{"description":"Successfully deleted session","content":{"application/json":{"schema":{"type":"boolean"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BadRequestError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NotFoundError"}}}}}},"patch":{"operationId":"session.update","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}},{"in":"path","name":"sessionID","schema":{"type":"string"},"required":true}],"summary":"Update session","description":"Update properties of an existing session, such as title or other metadata.","responses":{"200":{"description":"Successfully updated session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Session"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BadRequestError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NotFoundError"}}}}},"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string"},"time":{"type":"object","properties":{"archived":{"type":"number"}}}}}}}}}},"/session/{sessionID}/children":{"get":{"operationId":"session.children","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}},{"in":"path","name":"sessionID","schema":{"type":"string","pattern":"^ses.*"},"required":true}],"summary":"Get session children","tags":["Session"],"description":"Retrieve all child sessions that were forked from the specified parent session.","responses":{"200":{"description":"List of children","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Session"}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BadRequestError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NotFoundError"}}}}}}},"/session/{sessionID}/todo":{"get":{"operationId":"session.todo","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}},{"in":"path","name":"sessionID","schema":{"type":"string"},"required":true,"description":"Session ID"}],"summary":"Get session todos","description":"Retrieve the todo list associated with a specific session, showing tasks and action items.","responses":{"200":{"description":"Todo list","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Todo"}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BadRequestError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NotFoundError"}}}}}}},"/session/{sessionID}/init":{"post":{"operationId":"session.init","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}},{"in":"path","name":"sessionID","schema":{"type":"string"},"required":true,"description":"Session ID"}],"summary":"Initialize session","description":"Analyze the current application and create an AGENTS.md file with project-specific agent configurations.","responses":{"200":{"description":"200","content":{"application/json":{"schema":{"type":"boolean"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BadRequestError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NotFoundError"}}}}},"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"modelID":{"type":"string"},"providerID":{"type":"string"},"messageID":{"type":"string","pattern":"^msg.*"}},"required":["modelID","providerID","messageID"]}}}}}},"/session/{sessionID}/fork":{"post":{"operationId":"session.fork","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}},{"in":"path","name":"sessionID","schema":{"type":"string","pattern":"^ses.*"},"required":true}],"summary":"Fork session","description":"Create a new session by forking an existing session at a specific message point.","responses":{"200":{"description":"200","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Session"}}}}},"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"messageID":{"type":"string","pattern":"^msg.*"}}}}}}}},"/session/{sessionID}/abort":{"post":{"operationId":"session.abort","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}},{"in":"path","name":"sessionID","schema":{"type":"string"},"required":true}],"summary":"Abort session","description":"Abort an active session and stop any ongoing AI processing or command execution.","responses":{"200":{"description":"Aborted session","content":{"application/json":{"schema":{"type":"boolean"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BadRequestError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NotFoundError"}}}}}}},"/session/{sessionID}/share":{"post":{"operationId":"session.share","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}},{"in":"path","name":"sessionID","schema":{"type":"string"},"required":true}],"summary":"Share session","description":"Create a shareable link for a session, allowing others to view the conversation.","responses":{"200":{"description":"Successfully shared session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Session"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BadRequestError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NotFoundError"}}}}}},"delete":{"operationId":"session.unshare","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}},{"in":"path","name":"sessionID","schema":{"type":"string","pattern":"^ses.*"},"required":true}],"summary":"Unshare session","description":"Remove the shareable link for a session, making it private again.","responses":{"200":{"description":"Successfully unshared session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Session"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BadRequestError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NotFoundError"}}}}}}},"/session/{sessionID}/diff":{"get":{"operationId":"session.diff","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}},{"in":"path","name":"sessionID","schema":{"type":"string","pattern":"^ses.*"},"required":true},{"in":"query","name":"messageID","schema":{"type":"string","pattern":"^msg.*"}}],"summary":"Get message diff","description":"Get the file changes (diff) that resulted from a specific user message in the session.","responses":{"200":{"description":"Successfully retrieved diff","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/FileDiff"}}}}}}}},"/session/{sessionID}/summarize":{"post":{"operationId":"session.summarize","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}},{"in":"path","name":"sessionID","schema":{"type":"string"},"required":true,"description":"Session ID"}],"summary":"Summarize session","description":"Generate a concise summary of the session using AI compaction to preserve key information.","responses":{"200":{"description":"Summarized session","content":{"application/json":{"schema":{"type":"boolean"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BadRequestError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NotFoundError"}}}}},"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"providerID":{"type":"string"},"modelID":{"type":"string"},"auto":{"default":false,"type":"boolean"}},"required":["providerID","modelID"]}}}}}},"/session/{sessionID}/message":{"get":{"operationId":"session.messages","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}},{"in":"path","name":"sessionID","schema":{"type":"string"},"required":true,"description":"Session ID"},{"in":"query","name":"limit","schema":{"type":"number"}}],"summary":"Get session messages","description":"Retrieve all messages in a session, including user prompts and AI responses.","responses":{"200":{"description":"List of messages","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"info":{"$ref":"#/components/schemas/Message"},"parts":{"type":"array","items":{"$ref":"#/components/schemas/Part"}}},"required":["info","parts"]}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BadRequestError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NotFoundError"}}}}}},"post":{"operationId":"session.prompt","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}},{"in":"path","name":"sessionID","schema":{"type":"string"},"required":true,"description":"Session ID"}],"summary":"Send message","description":"Create and send a new message to a session, streaming the AI response.","responses":{"200":{"description":"Created message","content":{"application/json":{"schema":{"type":"object","properties":{"info":{"$ref":"#/components/schemas/AssistantMessage"},"parts":{"type":"array","items":{"$ref":"#/components/schemas/Part"}}},"required":["info","parts"]}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BadRequestError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NotFoundError"}}}}},"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"messageID":{"type":"string","pattern":"^msg.*"},"model":{"type":"object","properties":{"providerID":{"type":"string"},"modelID":{"type":"string"}},"required":["providerID","modelID"]},"agent":{"type":"string"},"noReply":{"type":"boolean"},"tools":{"description":"@deprecated tools and permissions have been merged, you can set permissions on the session itself now","type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"boolean"}},"system":{"type":"string"},"variant":{"type":"string"},"parts":{"type":"array","items":{"anyOf":[{"$ref":"#/components/schemas/TextPartInput"},{"$ref":"#/components/schemas/FilePartInput"},{"$ref":"#/components/schemas/AgentPartInput"},{"$ref":"#/components/schemas/SubtaskPartInput"}]}}},"required":["parts"]}}}}}},"/session/{sessionID}/message/{messageID}":{"get":{"operationId":"session.message","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}},{"in":"path","name":"sessionID","schema":{"type":"string"},"required":true,"description":"Session ID"},{"in":"path","name":"messageID","schema":{"type":"string"},"required":true,"description":"Message ID"}],"summary":"Get message","description":"Retrieve a specific message from a session by its message ID.","responses":{"200":{"description":"Message","content":{"application/json":{"schema":{"type":"object","properties":{"info":{"$ref":"#/components/schemas/Message"},"parts":{"type":"array","items":{"$ref":"#/components/schemas/Part"}}},"required":["info","parts"]}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BadRequestError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NotFoundError"}}}}}}},"/session/{sessionID}/message/{messageID}/part/{partID}":{"delete":{"operationId":"part.delete","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}},{"in":"path","name":"sessionID","schema":{"type":"string"},"required":true,"description":"Session ID"},{"in":"path","name":"messageID","schema":{"type":"string"},"required":true,"description":"Message ID"},{"in":"path","name":"partID","schema":{"type":"string"},"required":true,"description":"Part ID"}],"description":"Delete a part from a message","responses":{"200":{"description":"Successfully deleted part","content":{"application/json":{"schema":{"type":"boolean"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BadRequestError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NotFoundError"}}}}}},"patch":{"operationId":"part.update","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}},{"in":"path","name":"sessionID","schema":{"type":"string"},"required":true,"description":"Session ID"},{"in":"path","name":"messageID","schema":{"type":"string"},"required":true,"description":"Message ID"},{"in":"path","name":"partID","schema":{"type":"string"},"required":true,"description":"Part ID"}],"description":"Update a part in a message","responses":{"200":{"description":"Successfully updated part","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Part"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BadRequestError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NotFoundError"}}}}},"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Part"}}}}}},"/session/{sessionID}/prompt_async":{"post":{"operationId":"session.prompt_async","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}},{"in":"path","name":"sessionID","schema":{"type":"string"},"required":true,"description":"Session ID"}],"summary":"Send async message","description":"Create and send a new message to a session asynchronously, starting the session if needed and returning immediately.","responses":{"204":{"description":"Prompt accepted"},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BadRequestError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NotFoundError"}}}}},"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"messageID":{"type":"string","pattern":"^msg.*"},"model":{"type":"object","properties":{"providerID":{"type":"string"},"modelID":{"type":"string"}},"required":["providerID","modelID"]},"agent":{"type":"string"},"noReply":{"type":"boolean"},"tools":{"description":"@deprecated tools and permissions have been merged, you can set permissions on the session itself now","type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"boolean"}},"system":{"type":"string"},"variant":{"type":"string"},"parts":{"type":"array","items":{"anyOf":[{"$ref":"#/components/schemas/TextPartInput"},{"$ref":"#/components/schemas/FilePartInput"},{"$ref":"#/components/schemas/AgentPartInput"},{"$ref":"#/components/schemas/SubtaskPartInput"}]}}},"required":["parts"]}}}}}},"/session/{sessionID}/command":{"post":{"operationId":"session.command","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}},{"in":"path","name":"sessionID","schema":{"type":"string"},"required":true,"description":"Session ID"}],"summary":"Send command","description":"Send a new command to a session for execution by the AI assistant.","responses":{"200":{"description":"Created message","content":{"application/json":{"schema":{"type":"object","properties":{"info":{"$ref":"#/components/schemas/AssistantMessage"},"parts":{"type":"array","items":{"$ref":"#/components/schemas/Part"}}},"required":["info","parts"]}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BadRequestError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NotFoundError"}}}}},"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"messageID":{"type":"string","pattern":"^msg.*"},"agent":{"type":"string"},"model":{"type":"string"},"arguments":{"type":"string"},"command":{"type":"string"},"variant":{"type":"string"},"parts":{"type":"array","items":{"anyOf":[{"type":"object","properties":{"id":{"type":"string"},"type":{"type":"string","const":"file"},"mime":{"type":"string"},"filename":{"type":"string"},"url":{"type":"string"},"source":{"$ref":"#/components/schemas/FilePartSource"}},"required":["type","mime","url"]}]}}},"required":["arguments","command"]}}}}}},"/session/{sessionID}/shell":{"post":{"operationId":"session.shell","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}},{"in":"path","name":"sessionID","schema":{"type":"string"},"required":true,"description":"Session ID"}],"summary":"Run shell command","description":"Execute a shell command within the session context and return the AI's response.","responses":{"200":{"description":"Created message","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssistantMessage"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BadRequestError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NotFoundError"}}}}},"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"agent":{"type":"string"},"model":{"type":"object","properties":{"providerID":{"type":"string"},"modelID":{"type":"string"}},"required":["providerID","modelID"]},"command":{"type":"string"}},"required":["agent","command"]}}}}}},"/session/{sessionID}/revert":{"post":{"operationId":"session.revert","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}},{"in":"path","name":"sessionID","schema":{"type":"string"},"required":true}],"summary":"Revert message","description":"Revert a specific message in a session, undoing its effects and restoring the previous state.","responses":{"200":{"description":"Updated session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Session"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BadRequestError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NotFoundError"}}}}},"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"messageID":{"type":"string","pattern":"^msg.*"},"partID":{"type":"string","pattern":"^prt.*"}},"required":["messageID"]}}}}}},"/session/{sessionID}/unrevert":{"post":{"operationId":"session.unrevert","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}},{"in":"path","name":"sessionID","schema":{"type":"string"},"required":true}],"summary":"Restore reverted messages","description":"Restore all previously reverted messages in a session.","responses":{"200":{"description":"Updated session","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Session"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BadRequestError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NotFoundError"}}}}}}},"/session/{sessionID}/permissions/{permissionID}":{"post":{"operationId":"permission.respond","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}},{"in":"path","name":"sessionID","schema":{"type":"string"},"required":true},{"in":"path","name":"permissionID","schema":{"type":"string"},"required":true}],"summary":"Respond to permission","deprecated":true,"description":"Approve or deny a permission request from the AI assistant.","responses":{"200":{"description":"Permission processed successfully","content":{"application/json":{"schema":{"type":"boolean"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BadRequestError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NotFoundError"}}}}},"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"response":{"type":"string","enum":["once","always","reject"]}},"required":["response"]}}}}}},"/permission/{requestID}/reply":{"post":{"operationId":"permission.reply","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}},{"in":"path","name":"requestID","schema":{"type":"string"},"required":true}],"summary":"Respond to permission request","description":"Approve or deny a permission request from the AI assistant.","responses":{"200":{"description":"Permission processed successfully","content":{"application/json":{"schema":{"type":"boolean"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BadRequestError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NotFoundError"}}}}},"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"reply":{"type":"string","enum":["once","always","reject"]},"message":{"type":"string"}},"required":["reply"]}}}}}},"/permission":{"get":{"operationId":"permission.list","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}}],"summary":"List pending permissions","description":"Get all pending permission requests across all sessions.","responses":{"200":{"description":"List of pending permissions","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/PermissionRequest"}}}}}}}},"/question":{"get":{"operationId":"question.list","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}}],"summary":"List pending questions","description":"Get all pending question requests across all sessions.","responses":{"200":{"description":"List of pending questions","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/QuestionRequest"}}}}}}}},"/question/{requestID}/reply":{"post":{"operationId":"question.reply","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}},{"in":"path","name":"requestID","schema":{"type":"string"},"required":true}],"summary":"Reply to question request","description":"Provide answers to a question request from the AI assistant.","responses":{"200":{"description":"Question answered successfully","content":{"application/json":{"schema":{"type":"boolean"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BadRequestError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NotFoundError"}}}}},"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"answers":{"description":"User answers in order of questions (each answer is an array of selected labels)","type":"array","items":{"$ref":"#/components/schemas/QuestionAnswer"}}},"required":["answers"]}}}}}},"/question/{requestID}/reject":{"post":{"operationId":"question.reject","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}},{"in":"path","name":"requestID","schema":{"type":"string"},"required":true}],"summary":"Reject question request","description":"Reject a question request from the AI assistant.","responses":{"200":{"description":"Question rejected successfully","content":{"application/json":{"schema":{"type":"boolean"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BadRequestError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NotFoundError"}}}}}}},"/provider":{"get":{"operationId":"provider.list","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}}],"summary":"List providers","description":"Get a list of all available AI providers, including both available and connected ones.","responses":{"200":{"description":"List of providers","content":{"application/json":{"schema":{"type":"object","properties":{"all":{"type":"array","items":{"type":"object","properties":{"api":{"type":"string"},"name":{"type":"string"},"env":{"type":"array","items":{"type":"string"}},"id":{"type":"string"},"npm":{"type":"string"},"models":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"family":{"type":"string"},"release_date":{"type":"string"},"attachment":{"type":"boolean"},"reasoning":{"type":"boolean"},"temperature":{"type":"boolean"},"tool_call":{"type":"boolean"},"interleaved":{"anyOf":[{"type":"boolean","const":true},{"type":"object","properties":{"field":{"type":"string","enum":["reasoning_content","reasoning_details"]}},"required":["field"],"additionalProperties":false}]},"cost":{"type":"object","properties":{"input":{"type":"number"},"output":{"type":"number"},"cache_read":{"type":"number"},"cache_write":{"type":"number"},"context_over_200k":{"type":"object","properties":{"input":{"type":"number"},"output":{"type":"number"},"cache_read":{"type":"number"},"cache_write":{"type":"number"}},"required":["input","output"]}},"required":["input","output"]},"limit":{"type":"object","properties":{"context":{"type":"number"},"input":{"type":"number"},"output":{"type":"number"}},"required":["context","output"]},"modalities":{"type":"object","properties":{"input":{"type":"array","items":{"type":"string","enum":["text","audio","image","video","pdf"]}},"output":{"type":"array","items":{"type":"string","enum":["text","audio","image","video","pdf"]}}},"required":["input","output"]},"experimental":{"type":"boolean"},"status":{"type":"string","enum":["alpha","beta","deprecated"]},"options":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},"headers":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}},"provider":{"type":"object","properties":{"npm":{"type":"string"}},"required":["npm"]},"variants":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}},"required":["id","name","release_date","attachment","reasoning","temperature","tool_call","limit","options"]}}},"required":["name","env","id","models"]}},"default":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}},"connected":{"type":"array","items":{"type":"string"}}},"required":["all","default","connected"]}}}}}}},"/provider/auth":{"get":{"operationId":"provider.auth","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}}],"summary":"Get provider auth methods","description":"Retrieve available authentication methods for all AI providers.","responses":{"200":{"description":"Provider auth methods","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"array","items":{"$ref":"#/components/schemas/ProviderAuthMethod"}}}}}}}}},"/provider/{providerID}/oauth/authorize":{"post":{"operationId":"provider.oauth.authorize","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}},{"in":"path","name":"providerID","schema":{"type":"string"},"required":true,"description":"Provider ID"}],"summary":"OAuth authorize","description":"Initiate OAuth authorization for a specific AI provider to get an authorization URL.","responses":{"200":{"description":"Authorization URL and method","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProviderAuthAuthorization"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BadRequestError"}}}}},"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"method":{"description":"Auth method index","type":"number"}},"required":["method"]}}}}}},"/provider/{providerID}/oauth/callback":{"post":{"operationId":"provider.oauth.callback","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}},{"in":"path","name":"providerID","schema":{"type":"string"},"required":true,"description":"Provider ID"}],"summary":"OAuth callback","description":"Handle the OAuth callback from a provider after user authorization.","responses":{"200":{"description":"OAuth callback processed successfully","content":{"application/json":{"schema":{"type":"boolean"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BadRequestError"}}}}},"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"method":{"description":"Auth method index","type":"number"},"code":{"description":"OAuth authorization code","type":"string"}},"required":["method"]}}}}}},"/find":{"get":{"operationId":"find.text","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}},{"in":"query","name":"pattern","schema":{"type":"string"},"required":true}],"summary":"Find text","description":"Search for text patterns across files in the project using ripgrep.","responses":{"200":{"description":"Matches","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"path":{"type":"object","properties":{"text":{"type":"string"}},"required":["text"]},"lines":{"type":"object","properties":{"text":{"type":"string"}},"required":["text"]},"line_number":{"type":"number"},"absolute_offset":{"type":"number"},"submatches":{"type":"array","items":{"type":"object","properties":{"match":{"type":"object","properties":{"text":{"type":"string"}},"required":["text"]},"start":{"type":"number"},"end":{"type":"number"}},"required":["match","start","end"]}}},"required":["path","lines","line_number","absolute_offset","submatches"]}}}}}}}},"/find/file":{"get":{"operationId":"find.files","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}},{"in":"query","name":"query","schema":{"type":"string"},"required":true},{"in":"query","name":"dirs","schema":{"type":"string","enum":["true","false"]}},{"in":"query","name":"type","schema":{"type":"string","enum":["file","directory"]}},{"in":"query","name":"limit","schema":{"type":"integer","minimum":1,"maximum":200}}],"summary":"Find files","description":"Search for files or directories by name or pattern in the project directory.","responses":{"200":{"description":"File paths","content":{"application/json":{"schema":{"type":"array","items":{"type":"string"}}}}}}}},"/find/symbol":{"get":{"operationId":"find.symbols","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}},{"in":"query","name":"query","schema":{"type":"string"},"required":true}],"summary":"Find symbols","description":"Search for workspace symbols like functions, classes, and variables using LSP.","responses":{"200":{"description":"Symbols","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Symbol"}}}}}}}},"/file":{"get":{"operationId":"file.list","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}},{"in":"query","name":"path","schema":{"type":"string"},"required":true}],"summary":"List files","description":"List files and directories in a specified path.","responses":{"200":{"description":"Files and directories","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/FileNode"}}}}}}}},"/file/content":{"get":{"operationId":"file.read","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}},{"in":"query","name":"path","schema":{"type":"string"},"required":true}],"summary":"Read file","description":"Read the content of a specified file.","responses":{"200":{"description":"File content","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FileContent"}}}}}}},"/file/status":{"get":{"operationId":"file.status","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}}],"summary":"Get file status","description":"Get the git status of all files in the project.","responses":{"200":{"description":"File status","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/File"}}}}}}}},"/mcp":{"get":{"operationId":"mcp.status","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}}],"summary":"Get MCP status","description":"Get the status of all Model Context Protocol (MCP) servers.","responses":{"200":{"description":"MCP server status","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"$ref":"#/components/schemas/MCPStatus"}}}}}}},"post":{"operationId":"mcp.add","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}}],"summary":"Add MCP server","description":"Dynamically add a new Model Context Protocol (MCP) server to the system.","responses":{"200":{"description":"MCP server added successfully","content":{"application/json":{"schema":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"$ref":"#/components/schemas/MCPStatus"}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BadRequestError"}}}}},"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"config":{"anyOf":[{"$ref":"#/components/schemas/McpLocalConfig"},{"$ref":"#/components/schemas/McpRemoteConfig"}]}},"required":["name","config"]}}}}}},"/mcp/{name}/auth":{"post":{"operationId":"mcp.auth.start","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}},{"schema":{"type":"string"},"in":"path","name":"name","required":true}],"summary":"Start MCP OAuth","description":"Start OAuth authentication flow for a Model Context Protocol (MCP) server.","responses":{"200":{"description":"OAuth flow started","content":{"application/json":{"schema":{"type":"object","properties":{"authorizationUrl":{"description":"URL to open in browser for authorization","type":"string"}},"required":["authorizationUrl"]}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BadRequestError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NotFoundError"}}}}}},"delete":{"operationId":"mcp.auth.remove","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}},{"schema":{"type":"string"},"in":"path","name":"name","required":true}],"summary":"Remove MCP OAuth","description":"Remove OAuth credentials for an MCP server","responses":{"200":{"description":"OAuth credentials removed","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","const":true}},"required":["success"]}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NotFoundError"}}}}}}},"/mcp/{name}/auth/callback":{"post":{"operationId":"mcp.auth.callback","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}},{"schema":{"type":"string"},"in":"path","name":"name","required":true}],"summary":"Complete MCP OAuth","description":"Complete OAuth authentication for a Model Context Protocol (MCP) server using the authorization code.","responses":{"200":{"description":"OAuth authentication completed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MCPStatus"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BadRequestError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NotFoundError"}}}}},"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"code":{"description":"Authorization code from OAuth callback","type":"string"}},"required":["code"]}}}}}},"/mcp/{name}/auth/authenticate":{"post":{"operationId":"mcp.auth.authenticate","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}},{"schema":{"type":"string"},"in":"path","name":"name","required":true}],"summary":"Authenticate MCP OAuth","description":"Start OAuth flow and wait for callback (opens browser)","responses":{"200":{"description":"OAuth authentication completed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MCPStatus"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BadRequestError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NotFoundError"}}}}}}},"/mcp/{name}/connect":{"post":{"operationId":"mcp.connect","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}},{"in":"path","name":"name","schema":{"type":"string"},"required":true}],"description":"Connect an MCP server","responses":{"200":{"description":"MCP server connected successfully","content":{"application/json":{"schema":{"type":"boolean"}}}}}}},"/mcp/{name}/disconnect":{"post":{"operationId":"mcp.disconnect","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}},{"in":"path","name":"name","schema":{"type":"string"},"required":true}],"description":"Disconnect an MCP server","responses":{"200":{"description":"MCP server disconnected successfully","content":{"application/json":{"schema":{"type":"boolean"}}}}}}},"/tui/append-prompt":{"post":{"operationId":"tui.appendPrompt","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}}],"summary":"Append TUI prompt","description":"Append prompt to the TUI","responses":{"200":{"description":"Prompt processed successfully","content":{"application/json":{"schema":{"type":"boolean"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BadRequestError"}}}}},"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"text":{"type":"string"}},"required":["text"]}}}}}},"/tui/open-help":{"post":{"operationId":"tui.openHelp","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}}],"summary":"Open help dialog","description":"Open the help dialog in the TUI to display user assistance information.","responses":{"200":{"description":"Help dialog opened successfully","content":{"application/json":{"schema":{"type":"boolean"}}}}}}},"/tui/open-sessions":{"post":{"operationId":"tui.openSessions","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}}],"summary":"Open sessions dialog","description":"Open the session dialog","responses":{"200":{"description":"Session dialog opened successfully","content":{"application/json":{"schema":{"type":"boolean"}}}}}}},"/tui/open-themes":{"post":{"operationId":"tui.openThemes","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}}],"summary":"Open themes dialog","description":"Open the theme dialog","responses":{"200":{"description":"Theme dialog opened successfully","content":{"application/json":{"schema":{"type":"boolean"}}}}}}},"/tui/open-models":{"post":{"operationId":"tui.openModels","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}}],"summary":"Open models dialog","description":"Open the model dialog","responses":{"200":{"description":"Model dialog opened successfully","content":{"application/json":{"schema":{"type":"boolean"}}}}}}},"/tui/submit-prompt":{"post":{"operationId":"tui.submitPrompt","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}}],"summary":"Submit TUI prompt","description":"Submit the prompt","responses":{"200":{"description":"Prompt submitted successfully","content":{"application/json":{"schema":{"type":"boolean"}}}}}}},"/tui/clear-prompt":{"post":{"operationId":"tui.clearPrompt","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}}],"summary":"Clear TUI prompt","description":"Clear the prompt","responses":{"200":{"description":"Prompt cleared successfully","content":{"application/json":{"schema":{"type":"boolean"}}}}}}},"/tui/execute-command":{"post":{"operationId":"tui.executeCommand","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}}],"summary":"Execute TUI command","description":"Execute a TUI command (e.g. agent_cycle)","responses":{"200":{"description":"Command executed successfully","content":{"application/json":{"schema":{"type":"boolean"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BadRequestError"}}}}},"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"command":{"type":"string"}},"required":["command"]}}}}}},"/tui/show-toast":{"post":{"operationId":"tui.showToast","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}}],"summary":"Show TUI toast","description":"Show a toast notification in the TUI","responses":{"200":{"description":"Toast notification shown successfully","content":{"application/json":{"schema":{"type":"boolean"}}}}},"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string"},"message":{"type":"string"},"variant":{"type":"string","enum":["info","success","warning","error"]},"duration":{"description":"Duration in milliseconds","default":5000,"type":"number"}},"required":["message","variant"]}}}}}},"/tui/publish":{"post":{"operationId":"tui.publish","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}}],"summary":"Publish TUI event","description":"Publish a TUI event","responses":{"200":{"description":"Event published successfully","content":{"application/json":{"schema":{"type":"boolean"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BadRequestError"}}}}},"requestBody":{"content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/Event.tui.prompt.append"},{"$ref":"#/components/schemas/Event.tui.command.execute"},{"$ref":"#/components/schemas/Event.tui.toast.show"},{"$ref":"#/components/schemas/Event.tui.session.select"}]}}}}}},"/tui/select-session":{"post":{"operationId":"tui.selectSession","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}}],"summary":"Select session","description":"Navigate the TUI to display the specified session.","responses":{"200":{"description":"Session selected successfully","content":{"application/json":{"schema":{"type":"boolean"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BadRequestError"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NotFoundError"}}}}},"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"sessionID":{"description":"Session ID to navigate to","type":"string","pattern":"^ses"}},"required":["sessionID"]}}}}}},"/tui/control/next":{"get":{"operationId":"tui.control.next","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}}],"summary":"Get next TUI request","description":"Retrieve the next TUI (Terminal User Interface) request from the queue for processing.","responses":{"200":{"description":"Next TUI request","content":{"application/json":{"schema":{"type":"object","properties":{"path":{"type":"string"},"body":{}},"required":["path","body"]}}}}}}},"/tui/control/response":{"post":{"operationId":"tui.control.response","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}}],"summary":"Submit TUI response","description":"Submit a response to the TUI request queue to complete a pending request.","responses":{"200":{"description":"Response submitted successfully","content":{"application/json":{"schema":{"type":"boolean"}}}}},"requestBody":{"content":{"application/json":{"schema":{}}}}}},"/instance/dispose":{"post":{"operationId":"instance.dispose","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}}],"summary":"Dispose instance","description":"Clean up and dispose the current OpenCode instance, releasing all resources.","responses":{"200":{"description":"Instance disposed","content":{"application/json":{"schema":{"type":"boolean"}}}}}}},"/path":{"get":{"operationId":"path.get","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}}],"summary":"Get paths","description":"Retrieve the current working directory and related path information for the OpenCode instance.","responses":{"200":{"description":"Path","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Path"}}}}}}},"/vcs":{"get":{"operationId":"vcs.get","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}}],"summary":"Get VCS info","description":"Retrieve version control system (VCS) information for the current project, such as git branch.","responses":{"200":{"description":"VCS info","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VcsInfo"}}}}}}},"/command":{"get":{"operationId":"command.list","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}}],"summary":"List commands","description":"Get a list of all available commands in the OpenCode system.","responses":{"200":{"description":"List of commands","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Command"}}}}}}}},"/log":{"post":{"operationId":"app.log","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}}],"summary":"Write log","description":"Write a log entry to the server logs with specified level and metadata.","responses":{"200":{"description":"Log entry written successfully","content":{"application/json":{"schema":{"type":"boolean"}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BadRequestError"}}}}},"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"service":{"description":"Service name for the log entry","type":"string"},"level":{"description":"Log level","type":"string","enum":["debug","info","error","warn"]},"message":{"description":"Log message","type":"string"},"extra":{"description":"Additional metadata for the log entry","type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"required":["service","level","message"]}}}}}},"/agent":{"get":{"operationId":"app.agents","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}}],"summary":"List agents","description":"Get a list of all available AI agents in the OpenCode system.","responses":{"200":{"description":"List of agents","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Agent"}}}}}}}},"/skill":{"get":{"operationId":"app.skills","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}}],"summary":"List skills","description":"Get a list of all available skills in the OpenCode system.","responses":{"200":{"description":"List of skills","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"location":{"type":"string"},"content":{"type":"string"}},"required":["name","description","location","content"]}}}}}}}},"/lsp":{"get":{"operationId":"lsp.status","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}}],"summary":"Get LSP status","description":"Get LSP server status","responses":{"200":{"description":"LSP server status","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/LSPStatus"}}}}}}}},"/formatter":{"get":{"operationId":"formatter.status","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}}],"summary":"Get formatter status","description":"Get formatter status","responses":{"200":{"description":"Formatter status","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/FormatterStatus"}}}}}}}},"/event":{"get":{"operationId":"event.subscribe","parameters":[{"in":"query","name":"directory","schema":{"type":"string"}}],"summary":"Subscribe to events","description":"Get events","responses":{"200":{"description":"Event stream","content":{"text/event-stream":{"schema":{"$ref":"#/components/schemas/Event"}}}}}}}},"components":{"schemas":{"Event.server.connected":{"type":"object","properties":{"type":{"type":"string","const":"server.connected"},"properties":{"type":"object","properties":{}}},"required":["type","properties"]},"Event.global.disposed":{"type":"object","properties":{"type":{"type":"string","const":"global.disposed"},"properties":{"type":"object","properties":{}}},"required":["type","properties"]},"Event.tui.prompt.append":{"type":"object","properties":{"type":{"type":"string","const":"tui.prompt.append"},"properties":{"type":"object","properties":{"text":{"type":"string"}},"required":["text"]}},"required":["type","properties"]},"Event.tui.command.execute":{"type":"object","properties":{"type":{"type":"string","const":"tui.command.execute"},"properties":{"type":"object","properties":{"command":{"anyOf":[{"type":"string","enum":["session.list","session.new","session.share","session.interrupt","session.compact","session.page.up","session.page.down","session.line.up","session.line.down","session.half.page.up","session.half.page.down","session.first","session.last","prompt.clear","prompt.submit","agent.cycle"]},{"type":"string"}]}},"required":["command"]}},"required":["type","properties"]},"Event.tui.toast.show":{"type":"object","properties":{"type":{"type":"string","const":"tui.toast.show"},"properties":{"type":"object","properties":{"title":{"type":"string"},"message":{"type":"string"},"variant":{"type":"string","enum":["info","success","warning","error"]},"duration":{"description":"Duration in milliseconds","default":5000,"type":"number"}},"required":["message","variant"]}},"required":["type","properties"]},"Event.tui.session.select":{"type":"object","properties":{"type":{"type":"string","const":"tui.session.select"},"properties":{"type":"object","properties":{"sessionID":{"description":"Session ID to navigate to","type":"string","pattern":"^ses"}},"required":["sessionID"]}},"required":["type","properties"]},"Event.installation.updated":{"type":"object","properties":{"type":{"type":"string","const":"installation.updated"},"properties":{"type":"object","properties":{"version":{"type":"string"}},"required":["version"]}},"required":["type","properties"]},"Event.installation.update-available":{"type":"object","properties":{"type":{"type":"string","const":"installation.update-available"},"properties":{"type":"object","properties":{"version":{"type":"string"}},"required":["version"]}},"required":["type","properties"]},"Project":{"type":"object","properties":{"id":{"type":"string"},"worktree":{"type":"string"},"vcs":{"type":"string","const":"git"},"name":{"type":"string"},"icon":{"type":"object","properties":{"url":{"type":"string"},"override":{"type":"string"},"color":{"type":"string"}}},"commands":{"type":"object","properties":{"start":{"description":"Startup script to run when creating a new workspace (worktree)","type":"string"}}},"time":{"type":"object","properties":{"created":{"type":"number"},"updated":{"type":"number"},"initialized":{"type":"number"}},"required":["created","updated"]},"sandboxes":{"type":"array","items":{"type":"string"}}},"required":["id","worktree","time","sandboxes"]},"Event.project.updated":{"type":"object","properties":{"type":{"type":"string","const":"project.updated"},"properties":{"$ref":"#/components/schemas/Project"}},"required":["type","properties"]},"Event.server.instance.disposed":{"type":"object","properties":{"type":{"type":"string","const":"server.instance.disposed"},"properties":{"type":"object","properties":{"directory":{"type":"string"}},"required":["directory"]}},"required":["type","properties"]},"Event.file.edited":{"type":"object","properties":{"type":{"type":"string","const":"file.edited"},"properties":{"type":"object","properties":{"file":{"type":"string"}},"required":["file"]}},"required":["type","properties"]},"Event.worktree.ready":{"type":"object","properties":{"type":{"type":"string","const":"worktree.ready"},"properties":{"type":"object","properties":{"name":{"type":"string"},"branch":{"type":"string"}},"required":["name","branch"]}},"required":["type","properties"]},"Event.worktree.failed":{"type":"object","properties":{"type":{"type":"string","const":"worktree.failed"},"properties":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}},"required":["type","properties"]},"Event.lsp.client.diagnostics":{"type":"object","properties":{"type":{"type":"string","const":"lsp.client.diagnostics"},"properties":{"type":"object","properties":{"serverID":{"type":"string"},"path":{"type":"string"}},"required":["serverID","path"]}},"required":["type","properties"]},"PermissionRequest":{"type":"object","properties":{"id":{"type":"string","pattern":"^per.*"},"sessionID":{"type":"string","pattern":"^ses.*"},"permission":{"type":"string"},"patterns":{"type":"array","items":{"type":"string"}},"metadata":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},"always":{"type":"array","items":{"type":"string"}},"tool":{"type":"object","properties":{"messageID":{"type":"string"},"callID":{"type":"string"}},"required":["messageID","callID"]}},"required":["id","sessionID","permission","patterns","metadata","always"]},"Event.permission.asked":{"type":"object","properties":{"type":{"type":"string","const":"permission.asked"},"properties":{"$ref":"#/components/schemas/PermissionRequest"}},"required":["type","properties"]},"Event.permission.replied":{"type":"object","properties":{"type":{"type":"string","const":"permission.replied"},"properties":{"type":"object","properties":{"sessionID":{"type":"string"},"requestID":{"type":"string"},"reply":{"type":"string","enum":["once","always","reject"]}},"required":["sessionID","requestID","reply"]}},"required":["type","properties"]},"SessionStatus":{"anyOf":[{"type":"object","properties":{"type":{"type":"string","const":"idle"}},"required":["type"]},{"type":"object","properties":{"type":{"type":"string","const":"retry"},"attempt":{"type":"number"},"message":{"type":"string"},"next":{"type":"number"}},"required":["type","attempt","message","next"]},{"type":"object","properties":{"type":{"type":"string","const":"busy"}},"required":["type"]}]},"Event.session.status":{"type":"object","properties":{"type":{"type":"string","const":"session.status"},"properties":{"type":"object","properties":{"sessionID":{"type":"string"},"status":{"$ref":"#/components/schemas/SessionStatus"}},"required":["sessionID","status"]}},"required":["type","properties"]},"Event.session.idle":{"type":"object","properties":{"type":{"type":"string","const":"session.idle"},"properties":{"type":"object","properties":{"sessionID":{"type":"string"}},"required":["sessionID"]}},"required":["type","properties"]},"QuestionOption":{"type":"object","properties":{"label":{"description":"Display text (1-5 words, concise)","type":"string"},"description":{"description":"Explanation of choice","type":"string"}},"required":["label","description"]},"QuestionInfo":{"type":"object","properties":{"question":{"description":"Complete question","type":"string"},"header":{"description":"Very short label (max 30 chars)","type":"string"},"options":{"description":"Available choices","type":"array","items":{"$ref":"#/components/schemas/QuestionOption"}},"multiple":{"description":"Allow selecting multiple choices","type":"boolean"},"custom":{"description":"Allow typing a custom answer (default: true)","type":"boolean"}},"required":["question","header","options"]},"QuestionRequest":{"type":"object","properties":{"id":{"type":"string","pattern":"^que.*"},"sessionID":{"type":"string","pattern":"^ses.*"},"questions":{"description":"Questions to ask","type":"array","items":{"$ref":"#/components/schemas/QuestionInfo"}},"tool":{"type":"object","properties":{"messageID":{"type":"string"},"callID":{"type":"string"}},"required":["messageID","callID"]}},"required":["id","sessionID","questions"]},"Event.question.asked":{"type":"object","properties":{"type":{"type":"string","const":"question.asked"},"properties":{"$ref":"#/components/schemas/QuestionRequest"}},"required":["type","properties"]},"QuestionAnswer":{"type":"array","items":{"type":"string"}},"Event.question.replied":{"type":"object","properties":{"type":{"type":"string","const":"question.replied"},"properties":{"type":"object","properties":{"sessionID":{"type":"string"},"requestID":{"type":"string"},"answers":{"type":"array","items":{"$ref":"#/components/schemas/QuestionAnswer"}}},"required":["sessionID","requestID","answers"]}},"required":["type","properties"]},"Event.question.rejected":{"type":"object","properties":{"type":{"type":"string","const":"question.rejected"},"properties":{"type":"object","properties":{"sessionID":{"type":"string"},"requestID":{"type":"string"}},"required":["sessionID","requestID"]}},"required":["type","properties"]},"Todo":{"type":"object","properties":{"content":{"description":"Brief description of the task","type":"string"},"status":{"description":"Current status of the task: pending, in_progress, completed, cancelled","type":"string"},"priority":{"description":"Priority level of the task: high, medium, low","type":"string"},"id":{"description":"Unique identifier for the todo item","type":"string"}},"required":["content","status","priority","id"]},"Event.todo.updated":{"type":"object","properties":{"type":{"type":"string","const":"todo.updated"},"properties":{"type":"object","properties":{"sessionID":{"type":"string"},"todos":{"type":"array","items":{"$ref":"#/components/schemas/Todo"}}},"required":["sessionID","todos"]}},"required":["type","properties"]},"Pty":{"type":"object","properties":{"id":{"type":"string","pattern":"^pty.*"},"title":{"type":"string"},"command":{"type":"string"},"args":{"type":"array","items":{"type":"string"}},"cwd":{"type":"string"},"status":{"type":"string","enum":["running","exited"]},"pid":{"type":"number"}},"required":["id","title","command","args","cwd","status","pid"]},"Event.pty.created":{"type":"object","properties":{"type":{"type":"string","const":"pty.created"},"properties":{"type":"object","properties":{"info":{"$ref":"#/components/schemas/Pty"}},"required":["info"]}},"required":["type","properties"]},"Event.pty.updated":{"type":"object","properties":{"type":{"type":"string","const":"pty.updated"},"properties":{"type":"object","properties":{"info":{"$ref":"#/components/schemas/Pty"}},"required":["info"]}},"required":["type","properties"]},"Event.pty.exited":{"type":"object","properties":{"type":{"type":"string","const":"pty.exited"},"properties":{"type":"object","properties":{"id":{"type":"string","pattern":"^pty.*"},"exitCode":{"type":"number"}},"required":["id","exitCode"]}},"required":["type","properties"]},"Event.pty.deleted":{"type":"object","properties":{"type":{"type":"string","const":"pty.deleted"},"properties":{"type":"object","properties":{"id":{"type":"string","pattern":"^pty.*"}},"required":["id"]}},"required":["type","properties"]},"Event.file.watcher.updated":{"type":"object","properties":{"type":{"type":"string","const":"file.watcher.updated"},"properties":{"type":"object","properties":{"file":{"type":"string"},"event":{"anyOf":[{"type":"string","const":"add"},{"type":"string","const":"change"},{"type":"string","const":"unlink"}]}},"required":["file","event"]}},"required":["type","properties"]},"Event.mcp.tools.changed":{"type":"object","properties":{"type":{"type":"string","const":"mcp.tools.changed"},"properties":{"type":"object","properties":{"server":{"type":"string"}},"required":["server"]}},"required":["type","properties"]},"Event.mcp.browser.open.failed":{"type":"object","properties":{"type":{"type":"string","const":"mcp.browser.open.failed"},"properties":{"type":"object","properties":{"mcpName":{"type":"string"},"url":{"type":"string"}},"required":["mcpName","url"]}},"required":["type","properties"]},"Event.lsp.updated":{"type":"object","properties":{"type":{"type":"string","const":"lsp.updated"},"properties":{"type":"object","properties":{}}},"required":["type","properties"]},"Event.vcs.branch.updated":{"type":"object","properties":{"type":{"type":"string","const":"vcs.branch.updated"},"properties":{"type":"object","properties":{"branch":{"type":"string"}}}},"required":["type","properties"]},"Event.command.executed":{"type":"object","properties":{"type":{"type":"string","const":"command.executed"},"properties":{"type":"object","properties":{"name":{"type":"string"},"sessionID":{"type":"string","pattern":"^ses.*"},"arguments":{"type":"string"},"messageID":{"type":"string","pattern":"^msg.*"}},"required":["name","sessionID","arguments","messageID"]}},"required":["type","properties"]},"FileDiff":{"type":"object","properties":{"file":{"type":"string"},"before":{"type":"string"},"after":{"type":"string"},"additions":{"type":"number"},"deletions":{"type":"number"}},"required":["file","before","after","additions","deletions"]},"UserMessage":{"type":"object","properties":{"id":{"type":"string"},"sessionID":{"type":"string"},"role":{"type":"string","const":"user"},"time":{"type":"object","properties":{"created":{"type":"number"}},"required":["created"]},"summary":{"type":"object","properties":{"title":{"type":"string"},"body":{"type":"string"},"diffs":{"type":"array","items":{"$ref":"#/components/schemas/FileDiff"}}},"required":["diffs"]},"agent":{"type":"string"},"model":{"type":"object","properties":{"providerID":{"type":"string"},"modelID":{"type":"string"}},"required":["providerID","modelID"]},"system":{"type":"string"},"tools":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"boolean"}},"variant":{"type":"string"}},"required":["id","sessionID","role","time","agent","model"]},"ProviderAuthError":{"type":"object","properties":{"name":{"type":"string","const":"ProviderAuthError"},"data":{"type":"object","properties":{"providerID":{"type":"string"},"message":{"type":"string"}},"required":["providerID","message"]}},"required":["name","data"]},"UnknownError":{"type":"object","properties":{"name":{"type":"string","const":"UnknownError"},"data":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}},"required":["name","data"]},"MessageOutputLengthError":{"type":"object","properties":{"name":{"type":"string","const":"MessageOutputLengthError"},"data":{"type":"object","properties":{}}},"required":["name","data"]},"MessageAbortedError":{"type":"object","properties":{"name":{"type":"string","const":"MessageAbortedError"},"data":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}},"required":["name","data"]},"APIError":{"type":"object","properties":{"name":{"type":"string","const":"APIError"},"data":{"type":"object","properties":{"message":{"type":"string"},"statusCode":{"type":"number"},"isRetryable":{"type":"boolean"},"responseHeaders":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}},"responseBody":{"type":"string"},"metadata":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}}},"required":["message","isRetryable"]}},"required":["name","data"]},"AssistantMessage":{"type":"object","properties":{"id":{"type":"string"},"sessionID":{"type":"string"},"role":{"type":"string","const":"assistant"},"time":{"type":"object","properties":{"created":{"type":"number"},"completed":{"type":"number"}},"required":["created"]},"error":{"anyOf":[{"$ref":"#/components/schemas/ProviderAuthError"},{"$ref":"#/components/schemas/UnknownError"},{"$ref":"#/components/schemas/MessageOutputLengthError"},{"$ref":"#/components/schemas/MessageAbortedError"},{"$ref":"#/components/schemas/APIError"}]},"parentID":{"type":"string"},"modelID":{"type":"string"},"providerID":{"type":"string"},"mode":{"type":"string"},"agent":{"type":"string"},"path":{"type":"object","properties":{"cwd":{"type":"string"},"root":{"type":"string"}},"required":["cwd","root"]},"summary":{"type":"boolean"},"cost":{"type":"number"},"tokens":{"type":"object","properties":{"input":{"type":"number"},"output":{"type":"number"},"reasoning":{"type":"number"},"cache":{"type":"object","properties":{"read":{"type":"number"},"write":{"type":"number"}},"required":["read","write"]}},"required":["input","output","reasoning","cache"]},"finish":{"type":"string"}},"required":["id","sessionID","role","time","parentID","modelID","providerID","mode","agent","path","cost","tokens"]},"Message":{"anyOf":[{"$ref":"#/components/schemas/UserMessage"},{"$ref":"#/components/schemas/AssistantMessage"}]},"Event.message.updated":{"type":"object","properties":{"type":{"type":"string","const":"message.updated"},"properties":{"type":"object","properties":{"info":{"$ref":"#/components/schemas/Message"}},"required":["info"]}},"required":["type","properties"]},"Event.message.removed":{"type":"object","properties":{"type":{"type":"string","const":"message.removed"},"properties":{"type":"object","properties":{"sessionID":{"type":"string"},"messageID":{"type":"string"}},"required":["sessionID","messageID"]}},"required":["type","properties"]},"TextPart":{"type":"object","properties":{"id":{"type":"string"},"sessionID":{"type":"string"},"messageID":{"type":"string"},"type":{"type":"string","const":"text"},"text":{"type":"string"},"synthetic":{"type":"boolean"},"ignored":{"type":"boolean"},"time":{"type":"object","properties":{"start":{"type":"number"},"end":{"type":"number"}},"required":["start"]},"metadata":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"required":["id","sessionID","messageID","type","text"]},"SubtaskPart":{"type":"object","properties":{"id":{"type":"string"},"sessionID":{"type":"string"},"messageID":{"type":"string"},"type":{"type":"string","const":"subtask"},"prompt":{"type":"string"},"description":{"type":"string"},"agent":{"type":"string"},"model":{"type":"object","properties":{"providerID":{"type":"string"},"modelID":{"type":"string"}},"required":["providerID","modelID"]},"command":{"type":"string"}},"required":["id","sessionID","messageID","type","prompt","description","agent"]},"ReasoningPart":{"type":"object","properties":{"id":{"type":"string"},"sessionID":{"type":"string"},"messageID":{"type":"string"},"type":{"type":"string","const":"reasoning"},"text":{"type":"string"},"metadata":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},"time":{"type":"object","properties":{"start":{"type":"number"},"end":{"type":"number"}},"required":["start"]}},"required":["id","sessionID","messageID","type","text","time"]},"FilePartSourceText":{"type":"object","properties":{"value":{"type":"string"},"start":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"end":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"required":["value","start","end"]},"FileSource":{"type":"object","properties":{"text":{"$ref":"#/components/schemas/FilePartSourceText"},"type":{"type":"string","const":"file"},"path":{"type":"string"}},"required":["text","type","path"]},"Range":{"type":"object","properties":{"start":{"type":"object","properties":{"line":{"type":"number"},"character":{"type":"number"}},"required":["line","character"]},"end":{"type":"object","properties":{"line":{"type":"number"},"character":{"type":"number"}},"required":["line","character"]}},"required":["start","end"]},"SymbolSource":{"type":"object","properties":{"text":{"$ref":"#/components/schemas/FilePartSourceText"},"type":{"type":"string","const":"symbol"},"path":{"type":"string"},"range":{"$ref":"#/components/schemas/Range"},"name":{"type":"string"},"kind":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"required":["text","type","path","range","name","kind"]},"ResourceSource":{"type":"object","properties":{"text":{"$ref":"#/components/schemas/FilePartSourceText"},"type":{"type":"string","const":"resource"},"clientName":{"type":"string"},"uri":{"type":"string"}},"required":["text","type","clientName","uri"]},"FilePartSource":{"anyOf":[{"$ref":"#/components/schemas/FileSource"},{"$ref":"#/components/schemas/SymbolSource"},{"$ref":"#/components/schemas/ResourceSource"}]},"FilePart":{"type":"object","properties":{"id":{"type":"string"},"sessionID":{"type":"string"},"messageID":{"type":"string"},"type":{"type":"string","const":"file"},"mime":{"type":"string"},"filename":{"type":"string"},"url":{"type":"string"},"source":{"$ref":"#/components/schemas/FilePartSource"}},"required":["id","sessionID","messageID","type","mime","url"]},"ToolStatePending":{"type":"object","properties":{"status":{"type":"string","const":"pending"},"input":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},"raw":{"type":"string"}},"required":["status","input","raw"]},"ToolStateRunning":{"type":"object","properties":{"status":{"type":"string","const":"running"},"input":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},"title":{"type":"string"},"metadata":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},"time":{"type":"object","properties":{"start":{"type":"number"}},"required":["start"]}},"required":["status","input","time"]},"ToolStateCompleted":{"type":"object","properties":{"status":{"type":"string","const":"completed"},"input":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},"output":{"type":"string"},"title":{"type":"string"},"metadata":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},"time":{"type":"object","properties":{"start":{"type":"number"},"end":{"type":"number"},"compacted":{"type":"number"}},"required":["start","end"]},"attachments":{"type":"array","items":{"$ref":"#/components/schemas/FilePart"}}},"required":["status","input","output","title","metadata","time"]},"ToolStateError":{"type":"object","properties":{"status":{"type":"string","const":"error"},"input":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},"error":{"type":"string"},"metadata":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},"time":{"type":"object","properties":{"start":{"type":"number"},"end":{"type":"number"}},"required":["start","end"]}},"required":["status","input","error","time"]},"ToolState":{"anyOf":[{"$ref":"#/components/schemas/ToolStatePending"},{"$ref":"#/components/schemas/ToolStateRunning"},{"$ref":"#/components/schemas/ToolStateCompleted"},{"$ref":"#/components/schemas/ToolStateError"}]},"ToolPart":{"type":"object","properties":{"id":{"type":"string"},"sessionID":{"type":"string"},"messageID":{"type":"string"},"type":{"type":"string","const":"tool"},"callID":{"type":"string"},"tool":{"type":"string"},"state":{"$ref":"#/components/schemas/ToolState"},"metadata":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"required":["id","sessionID","messageID","type","callID","tool","state"]},"StepStartPart":{"type":"object","properties":{"id":{"type":"string"},"sessionID":{"type":"string"},"messageID":{"type":"string"},"type":{"type":"string","const":"step-start"},"snapshot":{"type":"string"}},"required":["id","sessionID","messageID","type"]},"StepFinishPart":{"type":"object","properties":{"id":{"type":"string"},"sessionID":{"type":"string"},"messageID":{"type":"string"},"type":{"type":"string","const":"step-finish"},"reason":{"type":"string"},"snapshot":{"type":"string"},"cost":{"type":"number"},"tokens":{"type":"object","properties":{"input":{"type":"number"},"output":{"type":"number"},"reasoning":{"type":"number"},"cache":{"type":"object","properties":{"read":{"type":"number"},"write":{"type":"number"}},"required":["read","write"]}},"required":["input","output","reasoning","cache"]}},"required":["id","sessionID","messageID","type","reason","cost","tokens"]},"SnapshotPart":{"type":"object","properties":{"id":{"type":"string"},"sessionID":{"type":"string"},"messageID":{"type":"string"},"type":{"type":"string","const":"snapshot"},"snapshot":{"type":"string"}},"required":["id","sessionID","messageID","type","snapshot"]},"PatchPart":{"type":"object","properties":{"id":{"type":"string"},"sessionID":{"type":"string"},"messageID":{"type":"string"},"type":{"type":"string","const":"patch"},"hash":{"type":"string"},"files":{"type":"array","items":{"type":"string"}}},"required":["id","sessionID","messageID","type","hash","files"]},"AgentPart":{"type":"object","properties":{"id":{"type":"string"},"sessionID":{"type":"string"},"messageID":{"type":"string"},"type":{"type":"string","const":"agent"},"name":{"type":"string"},"source":{"type":"object","properties":{"value":{"type":"string"},"start":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"end":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"required":["value","start","end"]}},"required":["id","sessionID","messageID","type","name"]},"RetryPart":{"type":"object","properties":{"id":{"type":"string"},"sessionID":{"type":"string"},"messageID":{"type":"string"},"type":{"type":"string","const":"retry"},"attempt":{"type":"number"},"error":{"$ref":"#/components/schemas/APIError"},"time":{"type":"object","properties":{"created":{"type":"number"}},"required":["created"]}},"required":["id","sessionID","messageID","type","attempt","error","time"]},"CompactionPart":{"type":"object","properties":{"id":{"type":"string"},"sessionID":{"type":"string"},"messageID":{"type":"string"},"type":{"type":"string","const":"compaction"},"auto":{"type":"boolean"}},"required":["id","sessionID","messageID","type","auto"]},"Part":{"anyOf":[{"$ref":"#/components/schemas/TextPart"},{"$ref":"#/components/schemas/SubtaskPart"},{"$ref":"#/components/schemas/ReasoningPart"},{"$ref":"#/components/schemas/FilePart"},{"$ref":"#/components/schemas/ToolPart"},{"$ref":"#/components/schemas/StepStartPart"},{"$ref":"#/components/schemas/StepFinishPart"},{"$ref":"#/components/schemas/SnapshotPart"},{"$ref":"#/components/schemas/PatchPart"},{"$ref":"#/components/schemas/AgentPart"},{"$ref":"#/components/schemas/RetryPart"},{"$ref":"#/components/schemas/CompactionPart"}]},"Event.message.part.updated":{"type":"object","properties":{"type":{"type":"string","const":"message.part.updated"},"properties":{"type":"object","properties":{"part":{"$ref":"#/components/schemas/Part"},"delta":{"type":"string"}},"required":["part"]}},"required":["type","properties"]},"Event.message.part.removed":{"type":"object","properties":{"type":{"type":"string","const":"message.part.removed"},"properties":{"type":"object","properties":{"sessionID":{"type":"string"},"messageID":{"type":"string"},"partID":{"type":"string"}},"required":["sessionID","messageID","partID"]}},"required":["type","properties"]},"Event.session.compacted":{"type":"object","properties":{"type":{"type":"string","const":"session.compacted"},"properties":{"type":"object","properties":{"sessionID":{"type":"string"}},"required":["sessionID"]}},"required":["type","properties"]},"PermissionAction":{"type":"string","enum":["allow","deny","ask"]},"PermissionRule":{"type":"object","properties":{"permission":{"type":"string"},"pattern":{"type":"string"},"action":{"$ref":"#/components/schemas/PermissionAction"}},"required":["permission","pattern","action"]},"PermissionRuleset":{"type":"array","items":{"$ref":"#/components/schemas/PermissionRule"}},"Session":{"type":"object","properties":{"id":{"type":"string","pattern":"^ses.*"},"slug":{"type":"string"},"projectID":{"type":"string"},"directory":{"type":"string"},"parentID":{"type":"string","pattern":"^ses.*"},"summary":{"type":"object","properties":{"additions":{"type":"number"},"deletions":{"type":"number"},"files":{"type":"number"},"diffs":{"type":"array","items":{"$ref":"#/components/schemas/FileDiff"}}},"required":["additions","deletions","files"]},"share":{"type":"object","properties":{"url":{"type":"string"}},"required":["url"]},"title":{"type":"string"},"version":{"type":"string"},"time":{"type":"object","properties":{"created":{"type":"number"},"updated":{"type":"number"},"compacting":{"type":"number"},"archived":{"type":"number"}},"required":["created","updated"]},"permission":{"$ref":"#/components/schemas/PermissionRuleset"},"revert":{"type":"object","properties":{"messageID":{"type":"string"},"partID":{"type":"string"},"snapshot":{"type":"string"},"diff":{"type":"string"}},"required":["messageID"]}},"required":["id","slug","projectID","directory","title","version","time"]},"Event.session.created":{"type":"object","properties":{"type":{"type":"string","const":"session.created"},"properties":{"type":"object","properties":{"info":{"$ref":"#/components/schemas/Session"}},"required":["info"]}},"required":["type","properties"]},"Event.session.updated":{"type":"object","properties":{"type":{"type":"string","const":"session.updated"},"properties":{"type":"object","properties":{"info":{"$ref":"#/components/schemas/Session"}},"required":["info"]}},"required":["type","properties"]},"Event.session.deleted":{"type":"object","properties":{"type":{"type":"string","const":"session.deleted"},"properties":{"type":"object","properties":{"info":{"$ref":"#/components/schemas/Session"}},"required":["info"]}},"required":["type","properties"]},"Event.session.diff":{"type":"object","properties":{"type":{"type":"string","const":"session.diff"},"properties":{"type":"object","properties":{"sessionID":{"type":"string"},"diff":{"type":"array","items":{"$ref":"#/components/schemas/FileDiff"}}},"required":["sessionID","diff"]}},"required":["type","properties"]},"Event.session.error":{"type":"object","properties":{"type":{"type":"string","const":"session.error"},"properties":{"type":"object","properties":{"sessionID":{"type":"string"},"error":{"anyOf":[{"$ref":"#/components/schemas/ProviderAuthError"},{"$ref":"#/components/schemas/UnknownError"},{"$ref":"#/components/schemas/MessageOutputLengthError"},{"$ref":"#/components/schemas/MessageAbortedError"},{"$ref":"#/components/schemas/APIError"}]}}}},"required":["type","properties"]},"Event":{"anyOf":[{"$ref":"#/components/schemas/Event.server.connected"},{"$ref":"#/components/schemas/Event.global.disposed"},{"$ref":"#/components/schemas/Event.tui.prompt.append"},{"$ref":"#/components/schemas/Event.tui.command.execute"},{"$ref":"#/components/schemas/Event.tui.toast.show"},{"$ref":"#/components/schemas/Event.tui.session.select"},{"$ref":"#/components/schemas/Event.installation.updated"},{"$ref":"#/components/schemas/Event.installation.update-available"},{"$ref":"#/components/schemas/Event.project.updated"},{"$ref":"#/components/schemas/Event.server.instance.disposed"},{"$ref":"#/components/schemas/Event.file.edited"},{"$ref":"#/components/schemas/Event.worktree.ready"},{"$ref":"#/components/schemas/Event.worktree.failed"},{"$ref":"#/components/schemas/Event.lsp.client.diagnostics"},{"$ref":"#/components/schemas/Event.permission.asked"},{"$ref":"#/components/schemas/Event.permission.replied"},{"$ref":"#/components/schemas/Event.session.status"},{"$ref":"#/components/schemas/Event.session.idle"},{"$ref":"#/components/schemas/Event.question.asked"},{"$ref":"#/components/schemas/Event.question.replied"},{"$ref":"#/components/schemas/Event.question.rejected"},{"$ref":"#/components/schemas/Event.todo.updated"},{"$ref":"#/components/schemas/Event.pty.created"},{"$ref":"#/components/schemas/Event.pty.updated"},{"$ref":"#/components/schemas/Event.pty.exited"},{"$ref":"#/components/schemas/Event.pty.deleted"},{"$ref":"#/components/schemas/Event.file.watcher.updated"},{"$ref":"#/components/schemas/Event.mcp.tools.changed"},{"$ref":"#/components/schemas/Event.mcp.browser.open.failed"},{"$ref":"#/components/schemas/Event.lsp.updated"},{"$ref":"#/components/schemas/Event.vcs.branch.updated"},{"$ref":"#/components/schemas/Event.command.executed"},{"$ref":"#/components/schemas/Event.message.updated"},{"$ref":"#/components/schemas/Event.message.removed"},{"$ref":"#/components/schemas/Event.message.part.updated"},{"$ref":"#/components/schemas/Event.message.part.removed"},{"$ref":"#/components/schemas/Event.session.compacted"},{"$ref":"#/components/schemas/Event.session.created"},{"$ref":"#/components/schemas/Event.session.updated"},{"$ref":"#/components/schemas/Event.session.deleted"},{"$ref":"#/components/schemas/Event.session.diff"},{"$ref":"#/components/schemas/Event.session.error"}]},"GlobalEvent":{"type":"object","properties":{"directory":{"type":"string"},"payload":{"$ref":"#/components/schemas/Event"}},"required":["directory","payload"]},"KeybindsConfig":{"description":"Custom keybind configurations","type":"object","properties":{"leader":{"description":"Leader key for keybind combinations","default":"ctrl+x","type":"string"},"app_exit":{"description":"Exit the application","default":"ctrl+c,ctrl+d,q","type":"string"},"editor_open":{"description":"Open external editor","default":"e","type":"string"},"theme_list":{"description":"List available themes","default":"t","type":"string"},"sidebar_toggle":{"description":"Toggle sidebar","default":"b","type":"string"},"scrollbar_toggle":{"description":"Toggle session scrollbar","default":"none","type":"string"},"username_toggle":{"description":"Toggle username visibility","default":"none","type":"string"},"status_view":{"description":"View status","default":"s","type":"string"},"session_export":{"description":"Export session to editor","default":"x","type":"string"},"session_new":{"description":"Create a new session","default":"n","type":"string"},"session_list":{"description":"List all sessions","default":"l","type":"string"},"session_timeline":{"description":"Show session timeline","default":"g","type":"string"},"session_fork":{"description":"Fork session from message","default":"none","type":"string"},"session_rename":{"description":"Rename session","default":"ctrl+r","type":"string"},"session_delete":{"description":"Delete session","default":"ctrl+d","type":"string"},"stash_delete":{"description":"Delete stash entry","default":"ctrl+d","type":"string"},"model_provider_list":{"description":"Open provider list from model dialog","default":"ctrl+a","type":"string"},"model_favorite_toggle":{"description":"Toggle model favorite status","default":"ctrl+f","type":"string"},"session_share":{"description":"Share current session","default":"none","type":"string"},"session_unshare":{"description":"Unshare current session","default":"none","type":"string"},"session_interrupt":{"description":"Interrupt current session","default":"escape","type":"string"},"session_compact":{"description":"Compact the session","default":"c","type":"string"},"messages_page_up":{"description":"Scroll messages up by one page","default":"pageup,ctrl+alt+b","type":"string"},"messages_page_down":{"description":"Scroll messages down by one page","default":"pagedown,ctrl+alt+f","type":"string"},"messages_line_up":{"description":"Scroll messages up by one line","default":"ctrl+alt+y","type":"string"},"messages_line_down":{"description":"Scroll messages down by one line","default":"ctrl+alt+e","type":"string"},"messages_half_page_up":{"description":"Scroll messages up by half page","default":"ctrl+alt+u","type":"string"},"messages_half_page_down":{"description":"Scroll messages down by half page","default":"ctrl+alt+d","type":"string"},"messages_first":{"description":"Navigate to first message","default":"ctrl+g,home","type":"string"},"messages_last":{"description":"Navigate to last message","default":"ctrl+alt+g,end","type":"string"},"messages_next":{"description":"Navigate to next message","default":"none","type":"string"},"messages_previous":{"description":"Navigate to previous message","default":"none","type":"string"},"messages_last_user":{"description":"Navigate to last user message","default":"none","type":"string"},"messages_copy":{"description":"Copy message","default":"y","type":"string"},"messages_undo":{"description":"Undo message","default":"u","type":"string"},"messages_redo":{"description":"Redo message","default":"r","type":"string"},"messages_toggle_conceal":{"description":"Toggle code block concealment in messages","default":"h","type":"string"},"tool_details":{"description":"Toggle tool details visibility","default":"none","type":"string"},"model_list":{"description":"List available models","default":"m","type":"string"},"model_cycle_recent":{"description":"Next recently used model","default":"f2","type":"string"},"model_cycle_recent_reverse":{"description":"Previous recently used model","default":"shift+f2","type":"string"},"model_cycle_favorite":{"description":"Next favorite model","default":"none","type":"string"},"model_cycle_favorite_reverse":{"description":"Previous favorite model","default":"none","type":"string"},"command_list":{"description":"List available commands","default":"ctrl+p","type":"string"},"agent_list":{"description":"List agents","default":"a","type":"string"},"agent_cycle":{"description":"Next agent","default":"tab","type":"string"},"agent_cycle_reverse":{"description":"Previous agent","default":"shift+tab","type":"string"},"variant_cycle":{"description":"Cycle model variants","default":"ctrl+t","type":"string"},"input_clear":{"description":"Clear input field","default":"ctrl+c","type":"string"},"input_paste":{"description":"Paste from clipboard","default":"ctrl+v","type":"string"},"input_submit":{"description":"Submit input","default":"return","type":"string"},"input_newline":{"description":"Insert newline in input","default":"shift+return,ctrl+return,alt+return,ctrl+j","type":"string"},"input_move_left":{"description":"Move cursor left in input","default":"left,ctrl+b","type":"string"},"input_move_right":{"description":"Move cursor right in input","default":"right,ctrl+f","type":"string"},"input_move_up":{"description":"Move cursor up in input","default":"up","type":"string"},"input_move_down":{"description":"Move cursor down in input","default":"down","type":"string"},"input_select_left":{"description":"Select left in input","default":"shift+left","type":"string"},"input_select_right":{"description":"Select right in input","default":"shift+right","type":"string"},"input_select_up":{"description":"Select up in input","default":"shift+up","type":"string"},"input_select_down":{"description":"Select down in input","default":"shift+down","type":"string"},"input_line_home":{"description":"Move to start of line in input","default":"ctrl+a","type":"string"},"input_line_end":{"description":"Move to end of line in input","default":"ctrl+e","type":"string"},"input_select_line_home":{"description":"Select to start of line in input","default":"ctrl+shift+a","type":"string"},"input_select_line_end":{"description":"Select to end of line in input","default":"ctrl+shift+e","type":"string"},"input_visual_line_home":{"description":"Move to start of visual line in input","default":"alt+a","type":"string"},"input_visual_line_end":{"description":"Move to end of visual line in input","default":"alt+e","type":"string"},"input_select_visual_line_home":{"description":"Select to start of visual line in input","default":"alt+shift+a","type":"string"},"input_select_visual_line_end":{"description":"Select to end of visual line in input","default":"alt+shift+e","type":"string"},"input_buffer_home":{"description":"Move to start of buffer in input","default":"home","type":"string"},"input_buffer_end":{"description":"Move to end of buffer in input","default":"end","type":"string"},"input_select_buffer_home":{"description":"Select to start of buffer in input","default":"shift+home","type":"string"},"input_select_buffer_end":{"description":"Select to end of buffer in input","default":"shift+end","type":"string"},"input_delete_line":{"description":"Delete line in input","default":"ctrl+shift+d","type":"string"},"input_delete_to_line_end":{"description":"Delete to end of line in input","default":"ctrl+k","type":"string"},"input_delete_to_line_start":{"description":"Delete to start of line in input","default":"ctrl+u","type":"string"},"input_backspace":{"description":"Backspace in input","default":"backspace,shift+backspace","type":"string"},"input_delete":{"description":"Delete character in input","default":"ctrl+d,delete,shift+delete","type":"string"},"input_undo":{"description":"Undo in input","default":"ctrl+-,super+z","type":"string"},"input_redo":{"description":"Redo in input","default":"ctrl+.,super+shift+z","type":"string"},"input_word_forward":{"description":"Move word forward in input","default":"alt+f,alt+right,ctrl+right","type":"string"},"input_word_backward":{"description":"Move word backward in input","default":"alt+b,alt+left,ctrl+left","type":"string"},"input_select_word_forward":{"description":"Select word forward in input","default":"alt+shift+f,alt+shift+right","type":"string"},"input_select_word_backward":{"description":"Select word backward in input","default":"alt+shift+b,alt+shift+left","type":"string"},"input_delete_word_forward":{"description":"Delete word forward in input","default":"alt+d,alt+delete,ctrl+delete","type":"string"},"input_delete_word_backward":{"description":"Delete word backward in input","default":"ctrl+w,ctrl+backspace,alt+backspace","type":"string"},"history_previous":{"description":"Previous history item","default":"up","type":"string"},"history_next":{"description":"Next history item","default":"down","type":"string"},"session_child_cycle":{"description":"Next child session","default":"right","type":"string"},"session_child_cycle_reverse":{"description":"Previous child session","default":"left","type":"string"},"session_parent":{"description":"Go to parent session","default":"up","type":"string"},"terminal_suspend":{"description":"Suspend terminal","default":"ctrl+z","type":"string"},"terminal_title_toggle":{"description":"Toggle terminal title","default":"none","type":"string"},"tips_toggle":{"description":"Toggle tips on home screen","default":"h","type":"string"}},"additionalProperties":false},"LogLevel":{"description":"Log level","type":"string","enum":["DEBUG","INFO","WARN","ERROR"]},"ServerConfig":{"description":"Server configuration for opencode serve and web commands","type":"object","properties":{"port":{"description":"Port to listen on","type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"hostname":{"description":"Hostname to listen on","type":"string"},"mdns":{"description":"Enable mDNS service discovery","type":"boolean"},"cors":{"description":"Additional domains to allow for CORS","type":"array","items":{"type":"string"}}},"additionalProperties":false},"PermissionActionConfig":{"type":"string","enum":["ask","allow","deny"]},"PermissionObjectConfig":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"$ref":"#/components/schemas/PermissionActionConfig"}},"PermissionRuleConfig":{"anyOf":[{"$ref":"#/components/schemas/PermissionActionConfig"},{"$ref":"#/components/schemas/PermissionObjectConfig"}]},"PermissionConfig":{"anyOf":[{"type":"object","properties":{"__originalKeys":{"type":"array","items":{"type":"string"}},"read":{"$ref":"#/components/schemas/PermissionRuleConfig"},"edit":{"$ref":"#/components/schemas/PermissionRuleConfig"},"glob":{"$ref":"#/components/schemas/PermissionRuleConfig"},"grep":{"$ref":"#/components/schemas/PermissionRuleConfig"},"list":{"$ref":"#/components/schemas/PermissionRuleConfig"},"bash":{"$ref":"#/components/schemas/PermissionRuleConfig"},"task":{"$ref":"#/components/schemas/PermissionRuleConfig"},"external_directory":{"$ref":"#/components/schemas/PermissionRuleConfig"},"todowrite":{"$ref":"#/components/schemas/PermissionActionConfig"},"todoread":{"$ref":"#/components/schemas/PermissionActionConfig"},"question":{"$ref":"#/components/schemas/PermissionActionConfig"},"webfetch":{"$ref":"#/components/schemas/PermissionActionConfig"},"websearch":{"$ref":"#/components/schemas/PermissionActionConfig"},"codesearch":{"$ref":"#/components/schemas/PermissionActionConfig"},"lsp":{"$ref":"#/components/schemas/PermissionRuleConfig"},"doom_loop":{"$ref":"#/components/schemas/PermissionActionConfig"},"skill":{"$ref":"#/components/schemas/PermissionRuleConfig"}},"additionalProperties":{"$ref":"#/components/schemas/PermissionRuleConfig"}},{"$ref":"#/components/schemas/PermissionActionConfig"}]},"AgentConfig":{"type":"object","properties":{"model":{"type":"string"},"temperature":{"type":"number"},"top_p":{"type":"number"},"prompt":{"type":"string"},"tools":{"description":"@deprecated Use 'permission' field instead","type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"boolean"}},"disable":{"type":"boolean"},"description":{"description":"Description of when to use the agent","type":"string"},"mode":{"type":"string","enum":["subagent","primary","all"]},"hidden":{"description":"Hide this subagent from the @ autocomplete menu (default: false, only applies to mode: subagent)","type":"boolean"},"options":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},"color":{"description":"Hex color code for the agent (e.g., #FF5733)","type":"string","pattern":"^#[0-9a-fA-F]{6}$"},"steps":{"description":"Maximum number of agentic iterations before forcing text-only response","type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"maxSteps":{"description":"@deprecated Use 'steps' field instead.","type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"permission":{"$ref":"#/components/schemas/PermissionConfig"}},"additionalProperties":{}},"ProviderConfig":{"type":"object","properties":{"api":{"type":"string"},"name":{"type":"string"},"env":{"type":"array","items":{"type":"string"}},"id":{"type":"string"},"npm":{"type":"string"},"models":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"family":{"type":"string"},"release_date":{"type":"string"},"attachment":{"type":"boolean"},"reasoning":{"type":"boolean"},"temperature":{"type":"boolean"},"tool_call":{"type":"boolean"},"interleaved":{"anyOf":[{"type":"boolean","const":true},{"type":"object","properties":{"field":{"type":"string","enum":["reasoning_content","reasoning_details"]}},"required":["field"],"additionalProperties":false}]},"cost":{"type":"object","properties":{"input":{"type":"number"},"output":{"type":"number"},"cache_read":{"type":"number"},"cache_write":{"type":"number"},"context_over_200k":{"type":"object","properties":{"input":{"type":"number"},"output":{"type":"number"},"cache_read":{"type":"number"},"cache_write":{"type":"number"}},"required":["input","output"]}},"required":["input","output"]},"limit":{"type":"object","properties":{"context":{"type":"number"},"input":{"type":"number"},"output":{"type":"number"}},"required":["context","output"]},"modalities":{"type":"object","properties":{"input":{"type":"array","items":{"type":"string","enum":["text","audio","image","video","pdf"]}},"output":{"type":"array","items":{"type":"string","enum":["text","audio","image","video","pdf"]}}},"required":["input","output"]},"experimental":{"type":"boolean"},"status":{"type":"string","enum":["alpha","beta","deprecated"]},"options":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},"headers":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}},"provider":{"type":"object","properties":{"npm":{"type":"string"}},"required":["npm"]},"variants":{"description":"Variant-specific configuration","type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"disabled":{"description":"Disable this variant for the model","type":"boolean"}},"additionalProperties":{}}}}}},"whitelist":{"type":"array","items":{"type":"string"}},"blacklist":{"type":"array","items":{"type":"string"}},"options":{"type":"object","properties":{"apiKey":{"type":"string"},"baseURL":{"type":"string"},"enterpriseUrl":{"description":"GitHub Enterprise URL for copilot authentication","type":"string"},"setCacheKey":{"description":"Enable promptCacheKey for this provider (default false)","type":"boolean"},"timeout":{"description":"Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout.","anyOf":[{"description":"Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout.","type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},{"description":"Disable timeout for this provider entirely.","type":"boolean","const":false}]}},"additionalProperties":{}}},"additionalProperties":false},"McpLocalConfig":{"type":"object","properties":{"type":{"description":"Type of MCP server connection","type":"string","const":"local"},"command":{"description":"Command and arguments to run the MCP server","type":"array","items":{"type":"string"}},"environment":{"description":"Environment variables to set when running the MCP server","type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}},"enabled":{"description":"Enable or disable the MCP server on startup","type":"boolean"},"timeout":{"description":"Timeout in ms for MCP server requests. Defaults to 5000 (5 seconds) if not specified.","type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"required":["type","command"],"additionalProperties":false},"McpOAuthConfig":{"type":"object","properties":{"clientId":{"description":"OAuth client ID. If not provided, dynamic client registration (RFC 7591) will be attempted.","type":"string"},"clientSecret":{"description":"OAuth client secret (if required by the authorization server)","type":"string"},"scope":{"description":"OAuth scopes to request during authorization","type":"string"}},"additionalProperties":false},"McpRemoteConfig":{"type":"object","properties":{"type":{"description":"Type of MCP server connection","type":"string","const":"remote"},"url":{"description":"URL of the remote MCP server","type":"string"},"enabled":{"description":"Enable or disable the MCP server on startup","type":"boolean"},"headers":{"description":"Headers to send with the request","type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}},"oauth":{"description":"OAuth authentication configuration for the MCP server. Set to false to disable OAuth auto-detection.","anyOf":[{"$ref":"#/components/schemas/McpOAuthConfig"},{"type":"boolean","const":false}]},"timeout":{"description":"Timeout in ms for MCP server requests. Defaults to 5000 (5 seconds) if not specified.","type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"required":["type","url"],"additionalProperties":false},"LayoutConfig":{"description":"@deprecated Always uses stretch layout.","type":"string","enum":["auto","stretch"]},"Config":{"type":"object","properties":{"$schema":{"description":"JSON schema reference for configuration validation","type":"string"},"theme":{"description":"Theme name to use for the interface","type":"string"},"keybinds":{"$ref":"#/components/schemas/KeybindsConfig"},"logLevel":{"$ref":"#/components/schemas/LogLevel"},"tui":{"description":"TUI specific settings","type":"object","properties":{"scroll_speed":{"description":"TUI scroll speed","type":"number","minimum":0.001},"scroll_acceleration":{"description":"Scroll acceleration settings","type":"object","properties":{"enabled":{"description":"Enable scroll acceleration","type":"boolean"}},"required":["enabled"]},"diff_style":{"description":"Control diff rendering style: 'auto' adapts to terminal width, 'stacked' always shows single column","type":"string","enum":["auto","stacked"]}}},"server":{"$ref":"#/components/schemas/ServerConfig"},"command":{"description":"Command configuration, see https://opencode.ai/docs/commands","type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"template":{"type":"string"},"description":{"type":"string"},"agent":{"type":"string"},"model":{"type":"string"},"subtask":{"type":"boolean"}},"required":["template"]}},"skills":{"description":"Additional skill folder paths","type":"object","properties":{"paths":{"description":"Additional paths to skill folders","type":"array","items":{"type":"string"}}}},"watcher":{"type":"object","properties":{"ignore":{"type":"array","items":{"type":"string"}}}},"plugin":{"type":"array","items":{"type":"string"}},"snapshot":{"type":"boolean"},"share":{"description":"Control sharing behavior:'manual' allows manual sharing via commands, 'auto' enables automatic sharing, 'disabled' disables all sharing","type":"string","enum":["manual","auto","disabled"]},"autoshare":{"description":"@deprecated Use 'share' field instead. Share newly created sessions automatically","type":"boolean"},"autoupdate":{"description":"Automatically update to the latest version. Set to true to auto-update, false to disable, or 'notify' to show update notifications","anyOf":[{"type":"boolean"},{"type":"string","const":"notify"}]},"disabled_providers":{"description":"Disable providers that are loaded automatically","type":"array","items":{"type":"string"}},"enabled_providers":{"description":"When set, ONLY these providers will be enabled. All other providers will be ignored","type":"array","items":{"type":"string"}},"model":{"description":"Model to use in the format of provider/model, eg anthropic/claude-2","type":"string"},"small_model":{"description":"Small model to use for tasks like title generation in the format of provider/model","type":"string"},"default_agent":{"description":"Default agent to use when none is specified. Must be a primary agent. Falls back to 'build' if not set or if the specified agent is invalid.","type":"string"},"username":{"description":"Custom username to display in conversations instead of system username","type":"string"},"mode":{"description":"@deprecated Use `agent` field instead.","type":"object","properties":{"build":{"$ref":"#/components/schemas/AgentConfig"},"plan":{"$ref":"#/components/schemas/AgentConfig"}},"additionalProperties":{"$ref":"#/components/schemas/AgentConfig"}},"agent":{"description":"Agent configuration, see https://opencode.ai/docs/agents","type":"object","properties":{"plan":{"$ref":"#/components/schemas/AgentConfig"},"build":{"$ref":"#/components/schemas/AgentConfig"},"general":{"$ref":"#/components/schemas/AgentConfig"},"explore":{"$ref":"#/components/schemas/AgentConfig"},"title":{"$ref":"#/components/schemas/AgentConfig"},"summary":{"$ref":"#/components/schemas/AgentConfig"},"compaction":{"$ref":"#/components/schemas/AgentConfig"}},"additionalProperties":{"$ref":"#/components/schemas/AgentConfig"}},"provider":{"description":"Custom provider configurations and model overrides","type":"object","propertyNames":{"type":"string"},"additionalProperties":{"$ref":"#/components/schemas/ProviderConfig"}},"mcp":{"description":"MCP (Model Context Protocol) server configurations","type":"object","propertyNames":{"type":"string"},"additionalProperties":{"anyOf":[{"anyOf":[{"$ref":"#/components/schemas/McpLocalConfig"},{"$ref":"#/components/schemas/McpRemoteConfig"}]},{"type":"object","properties":{"enabled":{"type":"boolean"}},"required":["enabled"],"additionalProperties":false}]}},"formatter":{"anyOf":[{"type":"boolean","const":false},{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","properties":{"disabled":{"type":"boolean"},"command":{"type":"array","items":{"type":"string"}},"environment":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}},"extensions":{"type":"array","items":{"type":"string"}}}}}]},"lsp":{"anyOf":[{"type":"boolean","const":false},{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"anyOf":[{"type":"object","properties":{"disabled":{"type":"boolean","const":true}},"required":["disabled"]},{"type":"object","properties":{"command":{"type":"array","items":{"type":"string"}},"extensions":{"type":"array","items":{"type":"string"}},"disabled":{"type":"boolean"},"env":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}},"initialization":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"required":["command"]}]}}]},"instructions":{"description":"Additional instruction files or patterns to include","type":"array","items":{"type":"string"}},"layout":{"$ref":"#/components/schemas/LayoutConfig"},"permission":{"$ref":"#/components/schemas/PermissionConfig"},"tools":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"boolean"}},"enterprise":{"type":"object","properties":{"url":{"description":"Enterprise URL","type":"string"}}},"compaction":{"type":"object","properties":{"auto":{"description":"Enable automatic compaction when context is full (default: true)","type":"boolean"},"prune":{"description":"Enable pruning of old tool outputs (default: true)","type":"boolean"}}},"experimental":{"type":"object","properties":{"disable_paste_summary":{"type":"boolean"},"batch_tool":{"description":"Enable the batch tool","type":"boolean"},"openTelemetry":{"description":"Enable OpenTelemetry spans for AI SDK calls (using the 'experimental_telemetry' flag)","type":"boolean"},"primary_tools":{"description":"Tools that should only be available to primary agents.","type":"array","items":{"type":"string"}},"continue_loop_on_deny":{"description":"Continue the agent loop when a tool call is denied","type":"boolean"},"mcp_timeout":{"description":"Timeout in milliseconds for model context protocol (MCP) requests","type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}}}},"additionalProperties":false},"BadRequestError":{"type":"object","properties":{"data":{},"errors":{"type":"array","items":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"success":{"type":"boolean","const":false}},"required":["data","errors","success"]},"OAuth":{"type":"object","properties":{"type":{"type":"string","const":"oauth"},"refresh":{"type":"string"},"access":{"type":"string"},"expires":{"type":"number"},"accountId":{"type":"string"},"enterpriseUrl":{"type":"string"}},"required":["type","refresh","access","expires"]},"ApiAuth":{"type":"object","properties":{"type":{"type":"string","const":"api"},"key":{"type":"string"}},"required":["type","key"]},"WellKnownAuth":{"type":"object","properties":{"type":{"type":"string","const":"wellknown"},"key":{"type":"string"},"token":{"type":"string"}},"required":["type","key","token"]},"Auth":{"anyOf":[{"$ref":"#/components/schemas/OAuth"},{"$ref":"#/components/schemas/ApiAuth"},{"$ref":"#/components/schemas/WellKnownAuth"}]},"NotFoundError":{"type":"object","properties":{"name":{"type":"string","const":"NotFoundError"},"data":{"type":"object","properties":{"message":{"type":"string"}},"required":["message"]}},"required":["name","data"]},"Model":{"type":"object","properties":{"id":{"type":"string"},"providerID":{"type":"string"},"api":{"type":"object","properties":{"id":{"type":"string"},"url":{"type":"string"},"npm":{"type":"string"}},"required":["id","url","npm"]},"name":{"type":"string"},"family":{"type":"string"},"capabilities":{"type":"object","properties":{"temperature":{"type":"boolean"},"reasoning":{"type":"boolean"},"attachment":{"type":"boolean"},"toolcall":{"type":"boolean"},"input":{"type":"object","properties":{"text":{"type":"boolean"},"audio":{"type":"boolean"},"image":{"type":"boolean"},"video":{"type":"boolean"},"pdf":{"type":"boolean"}},"required":["text","audio","image","video","pdf"]},"output":{"type":"object","properties":{"text":{"type":"boolean"},"audio":{"type":"boolean"},"image":{"type":"boolean"},"video":{"type":"boolean"},"pdf":{"type":"boolean"}},"required":["text","audio","image","video","pdf"]},"interleaved":{"anyOf":[{"type":"boolean"},{"type":"object","properties":{"field":{"type":"string","enum":["reasoning_content","reasoning_details"]}},"required":["field"]}]}},"required":["temperature","reasoning","attachment","toolcall","input","output","interleaved"]},"cost":{"type":"object","properties":{"input":{"type":"number"},"output":{"type":"number"},"cache":{"type":"object","properties":{"read":{"type":"number"},"write":{"type":"number"}},"required":["read","write"]},"experimentalOver200K":{"type":"object","properties":{"input":{"type":"number"},"output":{"type":"number"},"cache":{"type":"object","properties":{"read":{"type":"number"},"write":{"type":"number"}},"required":["read","write"]}},"required":["input","output","cache"]}},"required":["input","output","cache"]},"limit":{"type":"object","properties":{"context":{"type":"number"},"input":{"type":"number"},"output":{"type":"number"}},"required":["context","output"]},"status":{"type":"string","enum":["alpha","beta","deprecated","active"]},"options":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},"headers":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}},"release_date":{"type":"string"},"variants":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}}},"required":["id","providerID","api","name","capabilities","cost","limit","status","options","headers","release_date"]},"Provider":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"source":{"type":"string","enum":["env","config","custom","api"]},"env":{"type":"array","items":{"type":"string"}},"key":{"type":"string"},"options":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},"models":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"$ref":"#/components/schemas/Model"}}},"required":["id","name","source","env","options","models"]},"ToolIDs":{"type":"array","items":{"type":"string"}},"ToolListItem":{"type":"object","properties":{"id":{"type":"string"},"description":{"type":"string"},"parameters":{}},"required":["id","description","parameters"]},"ToolList":{"type":"array","items":{"$ref":"#/components/schemas/ToolListItem"}},"Worktree":{"type":"object","properties":{"name":{"type":"string"},"branch":{"type":"string"},"directory":{"type":"string"}},"required":["name","branch","directory"]},"WorktreeCreateInput":{"type":"object","properties":{"name":{"type":"string"},"startCommand":{"description":"Additional startup script to run after the project's start command","type":"string"}}},"WorktreeRemoveInput":{"type":"object","properties":{"directory":{"type":"string"}},"required":["directory"]},"WorktreeResetInput":{"type":"object","properties":{"directory":{"type":"string"}},"required":["directory"]},"McpResource":{"type":"object","properties":{"name":{"type":"string"},"uri":{"type":"string"},"description":{"type":"string"},"mimeType":{"type":"string"},"client":{"type":"string"}},"required":["name","uri","client"]},"TextPartInput":{"type":"object","properties":{"id":{"type":"string"},"type":{"type":"string","const":"text"},"text":{"type":"string"},"synthetic":{"type":"boolean"},"ignored":{"type":"boolean"},"time":{"type":"object","properties":{"start":{"type":"number"},"end":{"type":"number"}},"required":["start"]},"metadata":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}},"required":["type","text"]},"FilePartInput":{"type":"object","properties":{"id":{"type":"string"},"type":{"type":"string","const":"file"},"mime":{"type":"string"},"filename":{"type":"string"},"url":{"type":"string"},"source":{"$ref":"#/components/schemas/FilePartSource"}},"required":["type","mime","url"]},"AgentPartInput":{"type":"object","properties":{"id":{"type":"string"},"type":{"type":"string","const":"agent"},"name":{"type":"string"},"source":{"type":"object","properties":{"value":{"type":"string"},"start":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"end":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"required":["value","start","end"]}},"required":["type","name"]},"SubtaskPartInput":{"type":"object","properties":{"id":{"type":"string"},"type":{"type":"string","const":"subtask"},"prompt":{"type":"string"},"description":{"type":"string"},"agent":{"type":"string"},"model":{"type":"object","properties":{"providerID":{"type":"string"},"modelID":{"type":"string"}},"required":["providerID","modelID"]},"command":{"type":"string"}},"required":["type","prompt","description","agent"]},"ProviderAuthMethod":{"type":"object","properties":{"type":{"anyOf":[{"type":"string","const":"oauth"},{"type":"string","const":"api"}]},"label":{"type":"string"}},"required":["type","label"]},"ProviderAuthAuthorization":{"type":"object","properties":{"url":{"type":"string"},"method":{"anyOf":[{"type":"string","const":"auto"},{"type":"string","const":"code"}]},"instructions":{"type":"string"}},"required":["url","method","instructions"]},"Symbol":{"type":"object","properties":{"name":{"type":"string"},"kind":{"type":"number"},"location":{"type":"object","properties":{"uri":{"type":"string"},"range":{"$ref":"#/components/schemas/Range"}},"required":["uri","range"]}},"required":["name","kind","location"]},"FileNode":{"type":"object","properties":{"name":{"type":"string"},"path":{"type":"string"},"absolute":{"type":"string"},"type":{"type":"string","enum":["file","directory"]},"ignored":{"type":"boolean"}},"required":["name","path","absolute","type","ignored"]},"FileContent":{"type":"object","properties":{"type":{"type":"string","const":"text"},"content":{"type":"string"},"diff":{"type":"string"},"patch":{"type":"object","properties":{"oldFileName":{"type":"string"},"newFileName":{"type":"string"},"oldHeader":{"type":"string"},"newHeader":{"type":"string"},"hunks":{"type":"array","items":{"type":"object","properties":{"oldStart":{"type":"number"},"oldLines":{"type":"number"},"newStart":{"type":"number"},"newLines":{"type":"number"},"lines":{"type":"array","items":{"type":"string"}}},"required":["oldStart","oldLines","newStart","newLines","lines"]}},"index":{"type":"string"}},"required":["oldFileName","newFileName","hunks"]},"encoding":{"type":"string","const":"base64"},"mimeType":{"type":"string"}},"required":["type","content"]},"File":{"type":"object","properties":{"path":{"type":"string"},"added":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"removed":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"status":{"type":"string","enum":["added","deleted","modified"]}},"required":["path","added","removed","status"]},"MCPStatusConnected":{"type":"object","properties":{"status":{"type":"string","const":"connected"}},"required":["status"]},"MCPStatusDisabled":{"type":"object","properties":{"status":{"type":"string","const":"disabled"}},"required":["status"]},"MCPStatusFailed":{"type":"object","properties":{"status":{"type":"string","const":"failed"},"error":{"type":"string"}},"required":["status","error"]},"MCPStatusNeedsAuth":{"type":"object","properties":{"status":{"type":"string","const":"needs_auth"}},"required":["status"]},"MCPStatusNeedsClientRegistration":{"type":"object","properties":{"status":{"type":"string","const":"needs_client_registration"},"error":{"type":"string"}},"required":["status","error"]},"MCPStatus":{"anyOf":[{"$ref":"#/components/schemas/MCPStatusConnected"},{"$ref":"#/components/schemas/MCPStatusDisabled"},{"$ref":"#/components/schemas/MCPStatusFailed"},{"$ref":"#/components/schemas/MCPStatusNeedsAuth"},{"$ref":"#/components/schemas/MCPStatusNeedsClientRegistration"}]},"Path":{"type":"object","properties":{"home":{"type":"string"},"state":{"type":"string"},"config":{"type":"string"},"worktree":{"type":"string"},"directory":{"type":"string"}},"required":["home","state","config","worktree","directory"]},"VcsInfo":{"type":"object","properties":{"branch":{"type":"string"}},"required":["branch"]},"Command":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"agent":{"type":"string"},"model":{"type":"string"},"source":{"type":"string","enum":["command","mcp","skill"]},"template":{"anyOf":[{"type":"string"},{"type":"string"}]},"subtask":{"type":"boolean"},"hints":{"type":"array","items":{"type":"string"}}},"required":["name","template","hints"]},"Agent":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"},"mode":{"type":"string","enum":["subagent","primary","all"]},"native":{"type":"boolean"},"hidden":{"type":"boolean"},"topP":{"type":"number"},"temperature":{"type":"number"},"color":{"type":"string"},"permission":{"$ref":"#/components/schemas/PermissionRuleset"},"model":{"type":"object","properties":{"modelID":{"type":"string"},"providerID":{"type":"string"}},"required":["modelID","providerID"]},"prompt":{"type":"string"},"options":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},"steps":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991}},"required":["name","mode","permission","options"]},"LSPStatus":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"root":{"type":"string"},"status":{"anyOf":[{"type":"string","const":"connected"},{"type":"string","const":"error"}]}},"required":["id","name","root","status"]},"FormatterStatus":{"type":"object","properties":{"name":{"type":"string"},"extensions":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"}},"required":["name","extensions","enabled"]}}}} +{ + "openapi": "3.1.1", + "info": { "title": "opencode", "description": "opencode api", "version": "0.0.3" }, + "paths": { + "/global/health": { + "get": { + "operationId": "global.health", + "summary": "Get health", + "description": "Get health information about the OpenCode server.", + "responses": { + "200": { + "description": "Health information", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "healthy": { "type": "boolean", "const": true }, "version": { "type": "string" } }, + "required": ["healthy", "version"] + } + } + } + } + } + } + }, + "/global/event": { + "get": { + "operationId": "global.event", + "summary": "Get global events", + "description": "Subscribe to global events from the OpenCode system using server-sent events.", + "responses": { + "200": { + "description": "Event stream", + "content": { "text/event-stream": { "schema": { "$ref": "#/components/schemas/GlobalEvent" } } } + } + } + } + }, + "/global/config": { + "get": { + "operationId": "global.config.get", + "summary": "Get global configuration", + "description": "Retrieve the current global OpenCode configuration settings and preferences.", + "responses": { + "200": { + "description": "Get global config info", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Config" } } } + } + } + }, + "patch": { + "operationId": "global.config.update", + "summary": "Update global configuration", + "description": "Update global OpenCode configuration settings and preferences.", + "responses": { + "200": { + "description": "Successfully updated global config", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Config" } } } + }, + "400": { + "description": "Bad request", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BadRequestError" } } } + } + }, + "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Config" } } } } + } + }, + "/global/dispose": { + "post": { + "operationId": "global.dispose", + "summary": "Dispose instance", + "description": "Clean up and dispose all OpenCode instances, releasing all resources.", + "responses": { + "200": { + "description": "Global disposed", + "content": { "application/json": { "schema": { "type": "boolean" } } } + } + } + } + }, + "/auth/{providerID}": { + "put": { + "operationId": "auth.set", + "summary": "Set auth credentials", + "description": "Set authentication credentials", + "responses": { + "200": { + "description": "Successfully set authentication credentials", + "content": { "application/json": { "schema": { "type": "boolean" } } } + }, + "400": { + "description": "Bad request", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BadRequestError" } } } + } + }, + "parameters": [{ "in": "path", "name": "providerID", "schema": { "type": "string" }, "required": true }], + "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Auth" } } } } + }, + "delete": { + "operationId": "auth.remove", + "summary": "Remove auth credentials", + "description": "Remove authentication credentials", + "responses": { + "200": { + "description": "Successfully removed authentication credentials", + "content": { "application/json": { "schema": { "type": "boolean" } } } + }, + "400": { + "description": "Bad request", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BadRequestError" } } } + } + }, + "parameters": [{ "in": "path", "name": "providerID", "schema": { "type": "string" }, "required": true }] + } + }, + "/project": { + "get": { + "operationId": "project.list", + "parameters": [{ "in": "query", "name": "directory", "schema": { "type": "string" } }], + "summary": "List all projects", + "description": "Get a list of projects that have been opened with OpenCode.", + "responses": { + "200": { + "description": "List of projects", + "content": { + "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/Project" } } } + } + } + } + } + }, + "/project/current": { + "get": { + "operationId": "project.current", + "parameters": [{ "in": "query", "name": "directory", "schema": { "type": "string" } }], + "summary": "Get current project", + "description": "Retrieve the currently active project that OpenCode is working with.", + "responses": { + "200": { + "description": "Current project information", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Project" } } } + } + } + } + }, + "/project/{projectID}": { + "patch": { + "operationId": "project.update", + "parameters": [ + { "in": "query", "name": "directory", "schema": { "type": "string" } }, + { "in": "path", "name": "projectID", "schema": { "type": "string" }, "required": true } + ], + "summary": "Update project", + "description": "Update project properties such as name, icon, and commands.", + "responses": { + "200": { + "description": "Updated project information", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Project" } } } + }, + "400": { + "description": "Bad request", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BadRequestError" } } } + }, + "404": { + "description": "Not found", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/NotFoundError" } } } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "icon": { + "type": "object", + "properties": { + "url": { "type": "string" }, + "override": { "type": "string" }, + "color": { "type": "string" } + } + }, + "commands": { + "type": "object", + "properties": { + "start": { + "description": "Startup script to run when creating a new workspace (worktree)", + "type": "string" + } + } + } + } + } + } + } + } + } + }, + "/pty": { + "get": { + "operationId": "pty.list", + "parameters": [{ "in": "query", "name": "directory", "schema": { "type": "string" } }], + "summary": "List PTY sessions", + "description": "Get a list of all active pseudo-terminal (PTY) sessions managed by OpenCode.", + "responses": { + "200": { + "description": "List of sessions", + "content": { + "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/Pty" } } } + } + } + } + }, + "post": { + "operationId": "pty.create", + "parameters": [{ "in": "query", "name": "directory", "schema": { "type": "string" } }], + "summary": "Create PTY session", + "description": "Create a new pseudo-terminal (PTY) session for running shell commands and processes.", + "responses": { + "200": { + "description": "Created session", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Pty" } } } + }, + "400": { + "description": "Bad request", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BadRequestError" } } } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "command": { "type": "string" }, + "args": { "type": "array", "items": { "type": "string" } }, + "cwd": { "type": "string" }, + "title": { "type": "string" }, + "env": { + "type": "object", + "propertyNames": { "type": "string" }, + "additionalProperties": { "type": "string" } + } + } + } + } + } + } + } + }, + "/pty/{ptyID}": { + "get": { + "operationId": "pty.get", + "parameters": [ + { "in": "query", "name": "directory", "schema": { "type": "string" } }, + { "in": "path", "name": "ptyID", "schema": { "type": "string" }, "required": true } + ], + "summary": "Get PTY session", + "description": "Retrieve detailed information about a specific pseudo-terminal (PTY) session.", + "responses": { + "200": { + "description": "Session info", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Pty" } } } + }, + "404": { + "description": "Not found", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/NotFoundError" } } } + } + } + }, + "put": { + "operationId": "pty.update", + "parameters": [ + { "in": "query", "name": "directory", "schema": { "type": "string" } }, + { "in": "path", "name": "ptyID", "schema": { "type": "string" }, "required": true } + ], + "summary": "Update PTY session", + "description": "Update properties of an existing pseudo-terminal (PTY) session.", + "responses": { + "200": { + "description": "Updated session", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Pty" } } } + }, + "400": { + "description": "Bad request", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BadRequestError" } } } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "title": { "type": "string" }, + "size": { + "type": "object", + "properties": { "rows": { "type": "number" }, "cols": { "type": "number" } }, + "required": ["rows", "cols"] + } + } + } + } + } + } + }, + "delete": { + "operationId": "pty.remove", + "parameters": [ + { "in": "query", "name": "directory", "schema": { "type": "string" } }, + { "in": "path", "name": "ptyID", "schema": { "type": "string" }, "required": true } + ], + "summary": "Remove PTY session", + "description": "Remove and terminate a specific pseudo-terminal (PTY) session.", + "responses": { + "200": { + "description": "Session removed", + "content": { "application/json": { "schema": { "type": "boolean" } } } + }, + "404": { + "description": "Not found", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/NotFoundError" } } } + } + } + } + }, + "/pty/{ptyID}/connect": { + "get": { + "operationId": "pty.connect", + "parameters": [ + { "in": "query", "name": "directory", "schema": { "type": "string" } }, + { "in": "path", "name": "ptyID", "schema": { "type": "string" }, "required": true } + ], + "summary": "Connect to PTY session", + "description": "Establish a WebSocket connection to interact with a pseudo-terminal (PTY) session in real-time.", + "responses": { + "200": { + "description": "Connected session", + "content": { "application/json": { "schema": { "type": "boolean" } } } + }, + "404": { + "description": "Not found", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/NotFoundError" } } } + } + } + } + }, + "/config": { + "get": { + "operationId": "config.get", + "parameters": [{ "in": "query", "name": "directory", "schema": { "type": "string" } }], + "summary": "Get configuration", + "description": "Retrieve the current OpenCode configuration settings and preferences.", + "responses": { + "200": { + "description": "Get config info", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Config" } } } + } + } + }, + "patch": { + "operationId": "config.update", + "parameters": [{ "in": "query", "name": "directory", "schema": { "type": "string" } }], + "summary": "Update configuration", + "description": "Update OpenCode configuration settings and preferences.", + "responses": { + "200": { + "description": "Successfully updated config", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Config" } } } + }, + "400": { + "description": "Bad request", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BadRequestError" } } } + } + }, + "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Config" } } } } + } + }, + "/config/providers": { + "get": { + "operationId": "config.providers", + "parameters": [{ "in": "query", "name": "directory", "schema": { "type": "string" } }], + "summary": "List config providers", + "description": "Get a list of all configured AI providers and their default models.", + "responses": { + "200": { + "description": "List of providers", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "providers": { "type": "array", "items": { "$ref": "#/components/schemas/Provider" } }, + "default": { + "type": "object", + "propertyNames": { "type": "string" }, + "additionalProperties": { "type": "string" } + } + }, + "required": ["providers", "default"] + } + } + } + } + } + } + }, + "/experimental/tool/ids": { + "get": { + "operationId": "tool.ids", + "parameters": [{ "in": "query", "name": "directory", "schema": { "type": "string" } }], + "summary": "List tool IDs", + "description": "Get a list of all available tool IDs, including both built-in tools and dynamically registered tools.", + "responses": { + "200": { + "description": "Tool IDs", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ToolIDs" } } } + }, + "400": { + "description": "Bad request", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BadRequestError" } } } + } + } + } + }, + "/experimental/tool": { + "get": { + "operationId": "tool.list", + "parameters": [ + { "in": "query", "name": "directory", "schema": { "type": "string" } }, + { "in": "query", "name": "provider", "schema": { "type": "string" }, "required": true }, + { "in": "query", "name": "model", "schema": { "type": "string" }, "required": true } + ], + "summary": "List tools", + "description": "Get a list of available tools with their JSON schema parameters for a specific provider and model combination.", + "responses": { + "200": { + "description": "Tools", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ToolList" } } } + }, + "400": { + "description": "Bad request", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BadRequestError" } } } + } + } + } + }, + "/experimental/worktree": { + "post": { + "operationId": "worktree.create", + "parameters": [{ "in": "query", "name": "directory", "schema": { "type": "string" } }], + "summary": "Create worktree", + "description": "Create a new git worktree for the current project and run any configured startup scripts.", + "responses": { + "200": { + "description": "Worktree created", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Worktree" } } } + }, + "400": { + "description": "Bad request", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BadRequestError" } } } + } + }, + "requestBody": { + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WorktreeCreateInput" } } } + } + }, + "get": { + "operationId": "worktree.list", + "parameters": [{ "in": "query", "name": "directory", "schema": { "type": "string" } }], + "summary": "List worktrees", + "description": "List all sandbox worktrees for the current project.", + "responses": { + "200": { + "description": "List of worktree directories", + "content": { "application/json": { "schema": { "type": "array", "items": { "type": "string" } } } } + } + } + }, + "delete": { + "operationId": "worktree.remove", + "parameters": [{ "in": "query", "name": "directory", "schema": { "type": "string" } }], + "summary": "Remove worktree", + "description": "Remove a git worktree and delete its branch.", + "responses": { + "200": { + "description": "Worktree removed", + "content": { "application/json": { "schema": { "type": "boolean" } } } + }, + "400": { + "description": "Bad request", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BadRequestError" } } } + } + }, + "requestBody": { + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WorktreeRemoveInput" } } } + } + } + }, + "/experimental/worktree/reset": { + "post": { + "operationId": "worktree.reset", + "parameters": [{ "in": "query", "name": "directory", "schema": { "type": "string" } }], + "summary": "Reset worktree", + "description": "Reset a worktree branch to the primary default branch.", + "responses": { + "200": { + "description": "Worktree reset", + "content": { "application/json": { "schema": { "type": "boolean" } } } + }, + "400": { + "description": "Bad request", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BadRequestError" } } } + } + }, + "requestBody": { + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WorktreeResetInput" } } } + } + } + }, + "/experimental/resource": { + "get": { + "operationId": "experimental.resource.list", + "parameters": [{ "in": "query", "name": "directory", "schema": { "type": "string" } }], + "summary": "Get MCP resources", + "description": "Get all available MCP resources from connected servers. Optionally filter by name.", + "responses": { + "200": { + "description": "MCP resources", + "content": { + "application/json": { + "schema": { + "type": "object", + "propertyNames": { "type": "string" }, + "additionalProperties": { "$ref": "#/components/schemas/McpResource" } + } + } + } + } + } + } + }, + "/session": { + "get": { + "operationId": "session.list", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { "type": "string" }, + "description": "Filter sessions by project directory" + }, + { + "in": "query", + "name": "roots", + "schema": { "type": "boolean" }, + "description": "Only return root sessions (no parentID)" + }, + { + "in": "query", + "name": "start", + "schema": { "type": "number" }, + "description": "Filter sessions updated on or after this timestamp (milliseconds since epoch)" + }, + { + "in": "query", + "name": "search", + "schema": { "type": "string" }, + "description": "Filter sessions by title (case-insensitive)" + }, + { + "in": "query", + "name": "limit", + "schema": { "type": "number" }, + "description": "Maximum number of sessions to return" + } + ], + "summary": "List sessions", + "description": "Get a list of all OpenCode sessions, sorted by most recently updated.", + "responses": { + "200": { + "description": "List of sessions", + "content": { + "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/Session" } } } + } + } + } + }, + "post": { + "operationId": "session.create", + "parameters": [{ "in": "query", "name": "directory", "schema": { "type": "string" } }], + "summary": "Create session", + "description": "Create a new OpenCode session for interacting with AI assistants and managing conversations.", + "responses": { + "200": { + "description": "Successfully created session", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Session" } } } + }, + "400": { + "description": "Bad request", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BadRequestError" } } } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "parentID": { "type": "string", "pattern": "^ses.*" }, + "title": { "type": "string" }, + "permission": { "$ref": "#/components/schemas/PermissionRuleset" } + } + } + } + } + } + } + }, + "/session/status": { + "get": { + "operationId": "session.status", + "parameters": [{ "in": "query", "name": "directory", "schema": { "type": "string" } }], + "summary": "Get session status", + "description": "Retrieve the current status of all sessions, including active, idle, and completed states.", + "responses": { + "200": { + "description": "Get session status", + "content": { + "application/json": { + "schema": { + "type": "object", + "propertyNames": { "type": "string" }, + "additionalProperties": { "$ref": "#/components/schemas/SessionStatus" } + } + } + } + }, + "400": { + "description": "Bad request", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BadRequestError" } } } + } + } + } + }, + "/session/{sessionID}": { + "get": { + "operationId": "session.get", + "parameters": [ + { "in": "query", "name": "directory", "schema": { "type": "string" } }, + { "in": "path", "name": "sessionID", "schema": { "type": "string", "pattern": "^ses.*" }, "required": true } + ], + "summary": "Get session", + "description": "Retrieve detailed information about a specific OpenCode session.", + "tags": ["Session"], + "responses": { + "200": { + "description": "Get session", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Session" } } } + }, + "400": { + "description": "Bad request", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BadRequestError" } } } + }, + "404": { + "description": "Not found", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/NotFoundError" } } } + } + } + }, + "delete": { + "operationId": "session.delete", + "parameters": [ + { "in": "query", "name": "directory", "schema": { "type": "string" } }, + { "in": "path", "name": "sessionID", "schema": { "type": "string", "pattern": "^ses.*" }, "required": true } + ], + "summary": "Delete session", + "description": "Delete a session and permanently remove all associated data, including messages and history.", + "responses": { + "200": { + "description": "Successfully deleted session", + "content": { "application/json": { "schema": { "type": "boolean" } } } + }, + "400": { + "description": "Bad request", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BadRequestError" } } } + }, + "404": { + "description": "Not found", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/NotFoundError" } } } + } + } + }, + "patch": { + "operationId": "session.update", + "parameters": [ + { "in": "query", "name": "directory", "schema": { "type": "string" } }, + { "in": "path", "name": "sessionID", "schema": { "type": "string" }, "required": true } + ], + "summary": "Update session", + "description": "Update properties of an existing session, such as title or other metadata.", + "responses": { + "200": { + "description": "Successfully updated session", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Session" } } } + }, + "400": { + "description": "Bad request", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BadRequestError" } } } + }, + "404": { + "description": "Not found", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/NotFoundError" } } } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "title": { "type": "string" }, + "time": { "type": "object", "properties": { "archived": { "type": "number" } } } + } + } + } + } + } + } + }, + "/session/{sessionID}/children": { + "get": { + "operationId": "session.children", + "parameters": [ + { "in": "query", "name": "directory", "schema": { "type": "string" } }, + { "in": "path", "name": "sessionID", "schema": { "type": "string", "pattern": "^ses.*" }, "required": true } + ], + "summary": "Get session children", + "tags": ["Session"], + "description": "Retrieve all child sessions that were forked from the specified parent session.", + "responses": { + "200": { + "description": "List of children", + "content": { + "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/Session" } } } + } + }, + "400": { + "description": "Bad request", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BadRequestError" } } } + }, + "404": { + "description": "Not found", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/NotFoundError" } } } + } + } + } + }, + "/session/{sessionID}/todo": { + "get": { + "operationId": "session.todo", + "parameters": [ + { "in": "query", "name": "directory", "schema": { "type": "string" } }, + { + "in": "path", + "name": "sessionID", + "schema": { "type": "string" }, + "required": true, + "description": "Session ID" + } + ], + "summary": "Get session todos", + "description": "Retrieve the todo list associated with a specific session, showing tasks and action items.", + "responses": { + "200": { + "description": "Todo list", + "content": { + "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/Todo" } } } + } + }, + "400": { + "description": "Bad request", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BadRequestError" } } } + }, + "404": { + "description": "Not found", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/NotFoundError" } } } + } + } + } + }, + "/session/{sessionID}/init": { + "post": { + "operationId": "session.init", + "parameters": [ + { "in": "query", "name": "directory", "schema": { "type": "string" } }, + { + "in": "path", + "name": "sessionID", + "schema": { "type": "string" }, + "required": true, + "description": "Session ID" + } + ], + "summary": "Initialize session", + "description": "Analyze the current application and create an AGENTS.md file with project-specific agent configurations.", + "responses": { + "200": { "description": "200", "content": { "application/json": { "schema": { "type": "boolean" } } } }, + "400": { + "description": "Bad request", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BadRequestError" } } } + }, + "404": { + "description": "Not found", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/NotFoundError" } } } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "modelID": { "type": "string" }, + "providerID": { "type": "string" }, + "messageID": { "type": "string", "pattern": "^msg.*" } + }, + "required": ["modelID", "providerID", "messageID"] + } + } + } + } + } + }, + "/session/{sessionID}/fork": { + "post": { + "operationId": "session.fork", + "parameters": [ + { "in": "query", "name": "directory", "schema": { "type": "string" } }, + { "in": "path", "name": "sessionID", "schema": { "type": "string", "pattern": "^ses.*" }, "required": true } + ], + "summary": "Fork session", + "description": "Create a new session by forking an existing session at a specific message point.", + "responses": { + "200": { + "description": "200", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Session" } } } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { "type": "object", "properties": { "messageID": { "type": "string", "pattern": "^msg.*" } } } + } + } + } + } + }, + "/session/{sessionID}/abort": { + "post": { + "operationId": "session.abort", + "parameters": [ + { "in": "query", "name": "directory", "schema": { "type": "string" } }, + { "in": "path", "name": "sessionID", "schema": { "type": "string" }, "required": true } + ], + "summary": "Abort session", + "description": "Abort an active session and stop any ongoing AI processing or command execution.", + "responses": { + "200": { + "description": "Aborted session", + "content": { "application/json": { "schema": { "type": "boolean" } } } + }, + "400": { + "description": "Bad request", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BadRequestError" } } } + }, + "404": { + "description": "Not found", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/NotFoundError" } } } + } + } + } + }, + "/session/{sessionID}/share": { + "post": { + "operationId": "session.share", + "parameters": [ + { "in": "query", "name": "directory", "schema": { "type": "string" } }, + { "in": "path", "name": "sessionID", "schema": { "type": "string" }, "required": true } + ], + "summary": "Share session", + "description": "Create a shareable link for a session, allowing others to view the conversation.", + "responses": { + "200": { + "description": "Successfully shared session", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Session" } } } + }, + "400": { + "description": "Bad request", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BadRequestError" } } } + }, + "404": { + "description": "Not found", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/NotFoundError" } } } + } + } + }, + "delete": { + "operationId": "session.unshare", + "parameters": [ + { "in": "query", "name": "directory", "schema": { "type": "string" } }, + { "in": "path", "name": "sessionID", "schema": { "type": "string", "pattern": "^ses.*" }, "required": true } + ], + "summary": "Unshare session", + "description": "Remove the shareable link for a session, making it private again.", + "responses": { + "200": { + "description": "Successfully unshared session", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Session" } } } + }, + "400": { + "description": "Bad request", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BadRequestError" } } } + }, + "404": { + "description": "Not found", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/NotFoundError" } } } + } + } + } + }, + "/session/{sessionID}/diff": { + "get": { + "operationId": "session.diff", + "parameters": [ + { "in": "query", "name": "directory", "schema": { "type": "string" } }, + { "in": "path", "name": "sessionID", "schema": { "type": "string", "pattern": "^ses.*" }, "required": true }, + { "in": "query", "name": "messageID", "schema": { "type": "string", "pattern": "^msg.*" } } + ], + "summary": "Get message diff", + "description": "Get the file changes (diff) that resulted from a specific user message in the session.", + "responses": { + "200": { + "description": "Successfully retrieved diff", + "content": { + "application/json": { + "schema": { "type": "array", "items": { "$ref": "#/components/schemas/FileDiff" } } + } + } + } + } + } + }, + "/session/{sessionID}/summarize": { + "post": { + "operationId": "session.summarize", + "parameters": [ + { "in": "query", "name": "directory", "schema": { "type": "string" } }, + { + "in": "path", + "name": "sessionID", + "schema": { "type": "string" }, + "required": true, + "description": "Session ID" + } + ], + "summary": "Summarize session", + "description": "Generate a concise summary of the session using AI compaction to preserve key information.", + "responses": { + "200": { + "description": "Summarized session", + "content": { "application/json": { "schema": { "type": "boolean" } } } + }, + "400": { + "description": "Bad request", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BadRequestError" } } } + }, + "404": { + "description": "Not found", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/NotFoundError" } } } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "providerID": { "type": "string" }, + "modelID": { "type": "string" }, + "auto": { "default": false, "type": "boolean" } + }, + "required": ["providerID", "modelID"] + } + } + } + } + } + }, + "/session/{sessionID}/message": { + "get": { + "operationId": "session.messages", + "parameters": [ + { "in": "query", "name": "directory", "schema": { "type": "string" } }, + { + "in": "path", + "name": "sessionID", + "schema": { "type": "string" }, + "required": true, + "description": "Session ID" + }, + { "in": "query", "name": "limit", "schema": { "type": "number" } } + ], + "summary": "Get session messages", + "description": "Retrieve all messages in a session, including user prompts and AI responses.", + "responses": { + "200": { + "description": "List of messages", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "info": { "$ref": "#/components/schemas/Message" }, + "parts": { "type": "array", "items": { "$ref": "#/components/schemas/Part" } } + }, + "required": ["info", "parts"] + } + } + } + } + }, + "400": { + "description": "Bad request", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BadRequestError" } } } + }, + "404": { + "description": "Not found", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/NotFoundError" } } } + } + } + }, + "post": { + "operationId": "session.prompt", + "parameters": [ + { "in": "query", "name": "directory", "schema": { "type": "string" } }, + { + "in": "path", + "name": "sessionID", + "schema": { "type": "string" }, + "required": true, + "description": "Session ID" + } + ], + "summary": "Send message", + "description": "Create and send a new message to a session, streaming the AI response.", + "responses": { + "200": { + "description": "Created message", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "info": { "$ref": "#/components/schemas/AssistantMessage" }, + "parts": { "type": "array", "items": { "$ref": "#/components/schemas/Part" } } + }, + "required": ["info", "parts"] + } + } + } + }, + "400": { + "description": "Bad request", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BadRequestError" } } } + }, + "404": { + "description": "Not found", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/NotFoundError" } } } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "messageID": { "type": "string", "pattern": "^msg.*" }, + "model": { + "type": "object", + "properties": { "providerID": { "type": "string" }, "modelID": { "type": "string" } }, + "required": ["providerID", "modelID"] + }, + "agent": { "type": "string" }, + "noReply": { "type": "boolean" }, + "tools": { + "description": "@deprecated tools and permissions have been merged, you can set permissions on the session itself now", + "type": "object", + "propertyNames": { "type": "string" }, + "additionalProperties": { "type": "boolean" } + }, + "system": { "type": "string" }, + "variant": { "type": "string" }, + "parts": { + "type": "array", + "items": { + "anyOf": [ + { "$ref": "#/components/schemas/TextPartInput" }, + { "$ref": "#/components/schemas/FilePartInput" }, + { "$ref": "#/components/schemas/AgentPartInput" }, + { "$ref": "#/components/schemas/SubtaskPartInput" } + ] + } + } + }, + "required": ["parts"] + } + } + } + } + } + }, + "/session/{sessionID}/message/{messageID}": { + "get": { + "operationId": "session.message", + "parameters": [ + { "in": "query", "name": "directory", "schema": { "type": "string" } }, + { + "in": "path", + "name": "sessionID", + "schema": { "type": "string" }, + "required": true, + "description": "Session ID" + }, + { + "in": "path", + "name": "messageID", + "schema": { "type": "string" }, + "required": true, + "description": "Message ID" + } + ], + "summary": "Get message", + "description": "Retrieve a specific message from a session by its message ID.", + "responses": { + "200": { + "description": "Message", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "info": { "$ref": "#/components/schemas/Message" }, + "parts": { "type": "array", "items": { "$ref": "#/components/schemas/Part" } } + }, + "required": ["info", "parts"] + } + } + } + }, + "400": { + "description": "Bad request", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BadRequestError" } } } + }, + "404": { + "description": "Not found", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/NotFoundError" } } } + } + } + } + }, + "/session/{sessionID}/message/{messageID}/part/{partID}": { + "delete": { + "operationId": "part.delete", + "parameters": [ + { "in": "query", "name": "directory", "schema": { "type": "string" } }, + { + "in": "path", + "name": "sessionID", + "schema": { "type": "string" }, + "required": true, + "description": "Session ID" + }, + { + "in": "path", + "name": "messageID", + "schema": { "type": "string" }, + "required": true, + "description": "Message ID" + }, + { "in": "path", "name": "partID", "schema": { "type": "string" }, "required": true, "description": "Part ID" } + ], + "description": "Delete a part from a message", + "responses": { + "200": { + "description": "Successfully deleted part", + "content": { "application/json": { "schema": { "type": "boolean" } } } + }, + "400": { + "description": "Bad request", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BadRequestError" } } } + }, + "404": { + "description": "Not found", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/NotFoundError" } } } + } + } + }, + "patch": { + "operationId": "part.update", + "parameters": [ + { "in": "query", "name": "directory", "schema": { "type": "string" } }, + { + "in": "path", + "name": "sessionID", + "schema": { "type": "string" }, + "required": true, + "description": "Session ID" + }, + { + "in": "path", + "name": "messageID", + "schema": { "type": "string" }, + "required": true, + "description": "Message ID" + }, + { "in": "path", "name": "partID", "schema": { "type": "string" }, "required": true, "description": "Part ID" } + ], + "description": "Update a part in a message", + "responses": { + "200": { + "description": "Successfully updated part", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Part" } } } + }, + "400": { + "description": "Bad request", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BadRequestError" } } } + }, + "404": { + "description": "Not found", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/NotFoundError" } } } + } + }, + "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Part" } } } } + } + }, + "/session/{sessionID}/prompt_async": { + "post": { + "operationId": "session.prompt_async", + "parameters": [ + { "in": "query", "name": "directory", "schema": { "type": "string" } }, + { + "in": "path", + "name": "sessionID", + "schema": { "type": "string" }, + "required": true, + "description": "Session ID" + } + ], + "summary": "Send async message", + "description": "Create and send a new message to a session asynchronously, starting the session if needed and returning immediately.", + "responses": { + "204": { "description": "Prompt accepted" }, + "400": { + "description": "Bad request", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BadRequestError" } } } + }, + "404": { + "description": "Not found", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/NotFoundError" } } } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "messageID": { "type": "string", "pattern": "^msg.*" }, + "model": { + "type": "object", + "properties": { "providerID": { "type": "string" }, "modelID": { "type": "string" } }, + "required": ["providerID", "modelID"] + }, + "agent": { "type": "string" }, + "noReply": { "type": "boolean" }, + "tools": { + "description": "@deprecated tools and permissions have been merged, you can set permissions on the session itself now", + "type": "object", + "propertyNames": { "type": "string" }, + "additionalProperties": { "type": "boolean" } + }, + "system": { "type": "string" }, + "variant": { "type": "string" }, + "parts": { + "type": "array", + "items": { + "anyOf": [ + { "$ref": "#/components/schemas/TextPartInput" }, + { "$ref": "#/components/schemas/FilePartInput" }, + { "$ref": "#/components/schemas/AgentPartInput" }, + { "$ref": "#/components/schemas/SubtaskPartInput" } + ] + } + } + }, + "required": ["parts"] + } + } + } + } + } + }, + "/session/{sessionID}/command": { + "post": { + "operationId": "session.command", + "parameters": [ + { "in": "query", "name": "directory", "schema": { "type": "string" } }, + { + "in": "path", + "name": "sessionID", + "schema": { "type": "string" }, + "required": true, + "description": "Session ID" + } + ], + "summary": "Send command", + "description": "Send a new command to a session for execution by the AI assistant.", + "responses": { + "200": { + "description": "Created message", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "info": { "$ref": "#/components/schemas/AssistantMessage" }, + "parts": { "type": "array", "items": { "$ref": "#/components/schemas/Part" } } + }, + "required": ["info", "parts"] + } + } + } + }, + "400": { + "description": "Bad request", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BadRequestError" } } } + }, + "404": { + "description": "Not found", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/NotFoundError" } } } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "messageID": { "type": "string", "pattern": "^msg.*" }, + "agent": { "type": "string" }, + "model": { "type": "string" }, + "arguments": { "type": "string" }, + "command": { "type": "string" }, + "variant": { "type": "string" }, + "parts": { + "type": "array", + "items": { + "anyOf": [ + { + "type": "object", + "properties": { + "id": { "type": "string" }, + "type": { "type": "string", "const": "file" }, + "mime": { "type": "string" }, + "filename": { "type": "string" }, + "url": { "type": "string" }, + "source": { "$ref": "#/components/schemas/FilePartSource" } + }, + "required": ["type", "mime", "url"] + } + ] + } + } + }, + "required": ["arguments", "command"] + } + } + } + } + } + }, + "/session/{sessionID}/shell": { + "post": { + "operationId": "session.shell", + "parameters": [ + { "in": "query", "name": "directory", "schema": { "type": "string" } }, + { + "in": "path", + "name": "sessionID", + "schema": { "type": "string" }, + "required": true, + "description": "Session ID" + } + ], + "summary": "Run shell command", + "description": "Execute a shell command within the session context and return the AI's response.", + "responses": { + "200": { + "description": "Created message", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AssistantMessage" } } } + }, + "400": { + "description": "Bad request", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BadRequestError" } } } + }, + "404": { + "description": "Not found", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/NotFoundError" } } } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "agent": { "type": "string" }, + "model": { + "type": "object", + "properties": { "providerID": { "type": "string" }, "modelID": { "type": "string" } }, + "required": ["providerID", "modelID"] + }, + "command": { "type": "string" } + }, + "required": ["agent", "command"] + } + } + } + } + } + }, + "/session/{sessionID}/revert": { + "post": { + "operationId": "session.revert", + "parameters": [ + { "in": "query", "name": "directory", "schema": { "type": "string" } }, + { "in": "path", "name": "sessionID", "schema": { "type": "string" }, "required": true } + ], + "summary": "Revert message", + "description": "Revert a specific message in a session, undoing its effects and restoring the previous state.", + "responses": { + "200": { + "description": "Updated session", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Session" } } } + }, + "400": { + "description": "Bad request", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BadRequestError" } } } + }, + "404": { + "description": "Not found", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/NotFoundError" } } } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "messageID": { "type": "string", "pattern": "^msg.*" }, + "partID": { "type": "string", "pattern": "^prt.*" } + }, + "required": ["messageID"] + } + } + } + } + } + }, + "/session/{sessionID}/unrevert": { + "post": { + "operationId": "session.unrevert", + "parameters": [ + { "in": "query", "name": "directory", "schema": { "type": "string" } }, + { "in": "path", "name": "sessionID", "schema": { "type": "string" }, "required": true } + ], + "summary": "Restore reverted messages", + "description": "Restore all previously reverted messages in a session.", + "responses": { + "200": { + "description": "Updated session", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Session" } } } + }, + "400": { + "description": "Bad request", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BadRequestError" } } } + }, + "404": { + "description": "Not found", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/NotFoundError" } } } + } + } + } + }, + "/session/{sessionID}/permissions/{permissionID}": { + "post": { + "operationId": "permission.respond", + "parameters": [ + { "in": "query", "name": "directory", "schema": { "type": "string" } }, + { "in": "path", "name": "sessionID", "schema": { "type": "string" }, "required": true }, + { "in": "path", "name": "permissionID", "schema": { "type": "string" }, "required": true } + ], + "summary": "Respond to permission", + "deprecated": true, + "description": "Approve or deny a permission request from the AI assistant.", + "responses": { + "200": { + "description": "Permission processed successfully", + "content": { "application/json": { "schema": { "type": "boolean" } } } + }, + "400": { + "description": "Bad request", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BadRequestError" } } } + }, + "404": { + "description": "Not found", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/NotFoundError" } } } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "response": { "type": "string", "enum": ["once", "always", "reject"] } }, + "required": ["response"] + } + } + } + } + } + }, + "/permission/{requestID}/reply": { + "post": { + "operationId": "permission.reply", + "parameters": [ + { "in": "query", "name": "directory", "schema": { "type": "string" } }, + { "in": "path", "name": "requestID", "schema": { "type": "string" }, "required": true } + ], + "summary": "Respond to permission request", + "description": "Approve or deny a permission request from the AI assistant.", + "responses": { + "200": { + "description": "Permission processed successfully", + "content": { "application/json": { "schema": { "type": "boolean" } } } + }, + "400": { + "description": "Bad request", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BadRequestError" } } } + }, + "404": { + "description": "Not found", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/NotFoundError" } } } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "reply": { "type": "string", "enum": ["once", "always", "reject"] }, + "message": { "type": "string" } + }, + "required": ["reply"] + } + } + } + } + } + }, + "/permission": { + "get": { + "operationId": "permission.list", + "parameters": [{ "in": "query", "name": "directory", "schema": { "type": "string" } }], + "summary": "List pending permissions", + "description": "Get all pending permission requests across all sessions.", + "responses": { + "200": { + "description": "List of pending permissions", + "content": { + "application/json": { + "schema": { "type": "array", "items": { "$ref": "#/components/schemas/PermissionRequest" } } + } + } + } + } + } + }, + "/question": { + "get": { + "operationId": "question.list", + "parameters": [{ "in": "query", "name": "directory", "schema": { "type": "string" } }], + "summary": "List pending questions", + "description": "Get all pending question requests across all sessions.", + "responses": { + "200": { + "description": "List of pending questions", + "content": { + "application/json": { + "schema": { "type": "array", "items": { "$ref": "#/components/schemas/QuestionRequest" } } + } + } + } + } + } + }, + "/question/{requestID}/reply": { + "post": { + "operationId": "question.reply", + "parameters": [ + { "in": "query", "name": "directory", "schema": { "type": "string" } }, + { "in": "path", "name": "requestID", "schema": { "type": "string" }, "required": true } + ], + "summary": "Reply to question request", + "description": "Provide answers to a question request from the AI assistant.", + "responses": { + "200": { + "description": "Question answered successfully", + "content": { "application/json": { "schema": { "type": "boolean" } } } + }, + "400": { + "description": "Bad request", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BadRequestError" } } } + }, + "404": { + "description": "Not found", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/NotFoundError" } } } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "answers": { + "description": "User answers in order of questions (each answer is an array of selected labels)", + "type": "array", + "items": { "$ref": "#/components/schemas/QuestionAnswer" } + } + }, + "required": ["answers"] + } + } + } + } + } + }, + "/question/{requestID}/reject": { + "post": { + "operationId": "question.reject", + "parameters": [ + { "in": "query", "name": "directory", "schema": { "type": "string" } }, + { "in": "path", "name": "requestID", "schema": { "type": "string" }, "required": true } + ], + "summary": "Reject question request", + "description": "Reject a question request from the AI assistant.", + "responses": { + "200": { + "description": "Question rejected successfully", + "content": { "application/json": { "schema": { "type": "boolean" } } } + }, + "400": { + "description": "Bad request", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BadRequestError" } } } + }, + "404": { + "description": "Not found", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/NotFoundError" } } } + } + } + } + }, + "/provider": { + "get": { + "operationId": "provider.list", + "parameters": [{ "in": "query", "name": "directory", "schema": { "type": "string" } }], + "summary": "List providers", + "description": "Get a list of all available AI providers, including both available and connected ones.", + "responses": { + "200": { + "description": "List of providers", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "all": { + "type": "array", + "items": { + "type": "object", + "properties": { + "api": { "type": "string" }, + "name": { "type": "string" }, + "env": { "type": "array", "items": { "type": "string" } }, + "id": { "type": "string" }, + "npm": { "type": "string" }, + "models": { + "type": "object", + "propertyNames": { "type": "string" }, + "additionalProperties": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "name": { "type": "string" }, + "family": { "type": "string" }, + "release_date": { "type": "string" }, + "attachment": { "type": "boolean" }, + "reasoning": { "type": "boolean" }, + "temperature": { "type": "boolean" }, + "tool_call": { "type": "boolean" }, + "interleaved": { + "anyOf": [ + { "type": "boolean", "const": true }, + { + "type": "object", + "properties": { + "field": { + "type": "string", + "enum": ["reasoning_content", "reasoning_details"] + } + }, + "required": ["field"], + "additionalProperties": false + } + ] + }, + "cost": { + "type": "object", + "properties": { + "input": { "type": "number" }, + "output": { "type": "number" }, + "cache_read": { "type": "number" }, + "cache_write": { "type": "number" }, + "context_over_200k": { + "type": "object", + "properties": { + "input": { "type": "number" }, + "output": { "type": "number" }, + "cache_read": { "type": "number" }, + "cache_write": { "type": "number" } + }, + "required": ["input", "output"] + } + }, + "required": ["input", "output"] + }, + "limit": { + "type": "object", + "properties": { + "context": { "type": "number" }, + "input": { "type": "number" }, + "output": { "type": "number" } + }, + "required": ["context", "output"] + }, + "modalities": { + "type": "object", + "properties": { + "input": { + "type": "array", + "items": { "type": "string", "enum": ["text", "audio", "image", "video", "pdf"] } + }, + "output": { + "type": "array", + "items": { "type": "string", "enum": ["text", "audio", "image", "video", "pdf"] } + } + }, + "required": ["input", "output"] + }, + "experimental": { "type": "boolean" }, + "status": { "type": "string", "enum": ["alpha", "beta", "deprecated"] }, + "options": { + "type": "object", + "propertyNames": { "type": "string" }, + "additionalProperties": {} + }, + "headers": { + "type": "object", + "propertyNames": { "type": "string" }, + "additionalProperties": { "type": "string" } + }, + "provider": { + "type": "object", + "properties": { "npm": { "type": "string" } }, + "required": ["npm"] + }, + "variants": { + "type": "object", + "propertyNames": { "type": "string" }, + "additionalProperties": { + "type": "object", + "propertyNames": { "type": "string" }, + "additionalProperties": {} + } + } + }, + "required": [ + "id", + "name", + "release_date", + "attachment", + "reasoning", + "temperature", + "tool_call", + "limit", + "options" + ] + } + } + }, + "required": ["name", "env", "id", "models"] + } + }, + "default": { + "type": "object", + "propertyNames": { "type": "string" }, + "additionalProperties": { "type": "string" } + }, + "connected": { "type": "array", "items": { "type": "string" } } + }, + "required": ["all", "default", "connected"] + } + } + } + } + } + } + }, + "/provider/auth": { + "get": { + "operationId": "provider.auth", + "parameters": [{ "in": "query", "name": "directory", "schema": { "type": "string" } }], + "summary": "Get provider auth methods", + "description": "Retrieve available authentication methods for all AI providers.", + "responses": { + "200": { + "description": "Provider auth methods", + "content": { + "application/json": { + "schema": { + "type": "object", + "propertyNames": { "type": "string" }, + "additionalProperties": { + "type": "array", + "items": { "$ref": "#/components/schemas/ProviderAuthMethod" } + } + } + } + } + } + } + } + }, + "/provider/{providerID}/oauth/authorize": { + "post": { + "operationId": "provider.oauth.authorize", + "parameters": [ + { "in": "query", "name": "directory", "schema": { "type": "string" } }, + { + "in": "path", + "name": "providerID", + "schema": { "type": "string" }, + "required": true, + "description": "Provider ID" + } + ], + "summary": "OAuth authorize", + "description": "Initiate OAuth authorization for a specific AI provider to get an authorization URL.", + "responses": { + "200": { + "description": "Authorization URL and method", + "content": { + "application/json": { "schema": { "$ref": "#/components/schemas/ProviderAuthAuthorization" } } + } + }, + "400": { + "description": "Bad request", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BadRequestError" } } } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "method": { "description": "Auth method index", "type": "number" } }, + "required": ["method"] + } + } + } + } + } + }, + "/provider/{providerID}/oauth/callback": { + "post": { + "operationId": "provider.oauth.callback", + "parameters": [ + { "in": "query", "name": "directory", "schema": { "type": "string" } }, + { + "in": "path", + "name": "providerID", + "schema": { "type": "string" }, + "required": true, + "description": "Provider ID" + } + ], + "summary": "OAuth callback", + "description": "Handle the OAuth callback from a provider after user authorization.", + "responses": { + "200": { + "description": "OAuth callback processed successfully", + "content": { "application/json": { "schema": { "type": "boolean" } } } + }, + "400": { + "description": "Bad request", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BadRequestError" } } } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "method": { "description": "Auth method index", "type": "number" }, + "code": { "description": "OAuth authorization code", "type": "string" } + }, + "required": ["method"] + } + } + } + } + } + }, + "/find": { + "get": { + "operationId": "find.text", + "parameters": [ + { "in": "query", "name": "directory", "schema": { "type": "string" } }, + { "in": "query", "name": "pattern", "schema": { "type": "string" }, "required": true } + ], + "summary": "Find text", + "description": "Search for text patterns across files in the project using ripgrep.", + "responses": { + "200": { + "description": "Matches", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "path": { + "type": "object", + "properties": { "text": { "type": "string" } }, + "required": ["text"] + }, + "lines": { + "type": "object", + "properties": { "text": { "type": "string" } }, + "required": ["text"] + }, + "line_number": { "type": "number" }, + "absolute_offset": { "type": "number" }, + "submatches": { + "type": "array", + "items": { + "type": "object", + "properties": { + "match": { + "type": "object", + "properties": { "text": { "type": "string" } }, + "required": ["text"] + }, + "start": { "type": "number" }, + "end": { "type": "number" } + }, + "required": ["match", "start", "end"] + } + } + }, + "required": ["path", "lines", "line_number", "absolute_offset", "submatches"] + } + } + } + } + } + } + } + }, + "/find/file": { + "get": { + "operationId": "find.files", + "parameters": [ + { "in": "query", "name": "directory", "schema": { "type": "string" } }, + { "in": "query", "name": "query", "schema": { "type": "string" }, "required": true }, + { "in": "query", "name": "dirs", "schema": { "type": "string", "enum": ["true", "false"] } }, + { "in": "query", "name": "type", "schema": { "type": "string", "enum": ["file", "directory"] } }, + { "in": "query", "name": "limit", "schema": { "type": "integer", "minimum": 1, "maximum": 200 } } + ], + "summary": "Find files", + "description": "Search for files or directories by name or pattern in the project directory.", + "responses": { + "200": { + "description": "File paths", + "content": { "application/json": { "schema": { "type": "array", "items": { "type": "string" } } } } + } + } + } + }, + "/find/symbol": { + "get": { + "operationId": "find.symbols", + "parameters": [ + { "in": "query", "name": "directory", "schema": { "type": "string" } }, + { "in": "query", "name": "query", "schema": { "type": "string" }, "required": true } + ], + "summary": "Find symbols", + "description": "Search for workspace symbols like functions, classes, and variables using LSP.", + "responses": { + "200": { + "description": "Symbols", + "content": { + "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/Symbol" } } } + } + } + } + } + }, + "/file": { + "get": { + "operationId": "file.list", + "parameters": [ + { "in": "query", "name": "directory", "schema": { "type": "string" } }, + { "in": "query", "name": "path", "schema": { "type": "string" }, "required": true } + ], + "summary": "List files", + "description": "List files and directories in a specified path.", + "responses": { + "200": { + "description": "Files and directories", + "content": { + "application/json": { + "schema": { "type": "array", "items": { "$ref": "#/components/schemas/FileNode" } } + } + } + } + } + } + }, + "/file/content": { + "get": { + "operationId": "file.read", + "parameters": [ + { "in": "query", "name": "directory", "schema": { "type": "string" } }, + { "in": "query", "name": "path", "schema": { "type": "string" }, "required": true } + ], + "summary": "Read file", + "description": "Read the content of a specified file.", + "responses": { + "200": { + "description": "File content", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/FileContent" } } } + } + } + } + }, + "/file/status": { + "get": { + "operationId": "file.status", + "parameters": [{ "in": "query", "name": "directory", "schema": { "type": "string" } }], + "summary": "Get file status", + "description": "Get the git status of all files in the project.", + "responses": { + "200": { + "description": "File status", + "content": { + "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/File" } } } + } + } + } + } + }, + "/mcp": { + "get": { + "operationId": "mcp.status", + "parameters": [{ "in": "query", "name": "directory", "schema": { "type": "string" } }], + "summary": "Get MCP status", + "description": "Get the status of all Model Context Protocol (MCP) servers.", + "responses": { + "200": { + "description": "MCP server status", + "content": { + "application/json": { + "schema": { + "type": "object", + "propertyNames": { "type": "string" }, + "additionalProperties": { "$ref": "#/components/schemas/MCPStatus" } + } + } + } + } + } + }, + "post": { + "operationId": "mcp.add", + "parameters": [{ "in": "query", "name": "directory", "schema": { "type": "string" } }], + "summary": "Add MCP server", + "description": "Dynamically add a new Model Context Protocol (MCP) server to the system.", + "responses": { + "200": { + "description": "MCP server added successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "propertyNames": { "type": "string" }, + "additionalProperties": { "$ref": "#/components/schemas/MCPStatus" } + } + } + } + }, + "400": { + "description": "Bad request", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BadRequestError" } } } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "config": { + "anyOf": [ + { "$ref": "#/components/schemas/McpLocalConfig" }, + { "$ref": "#/components/schemas/McpRemoteConfig" } + ] + } + }, + "required": ["name", "config"] + } + } + } + } + } + }, + "/mcp/{name}/auth": { + "post": { + "operationId": "mcp.auth.start", + "parameters": [ + { "in": "query", "name": "directory", "schema": { "type": "string" } }, + { "schema": { "type": "string" }, "in": "path", "name": "name", "required": true } + ], + "summary": "Start MCP OAuth", + "description": "Start OAuth authentication flow for a Model Context Protocol (MCP) server.", + "responses": { + "200": { + "description": "OAuth flow started", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "authorizationUrl": { "description": "URL to open in browser for authorization", "type": "string" } + }, + "required": ["authorizationUrl"] + } + } + } + }, + "400": { + "description": "Bad request", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BadRequestError" } } } + }, + "404": { + "description": "Not found", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/NotFoundError" } } } + } + } + }, + "delete": { + "operationId": "mcp.auth.remove", + "parameters": [ + { "in": "query", "name": "directory", "schema": { "type": "string" } }, + { "schema": { "type": "string" }, "in": "path", "name": "name", "required": true } + ], + "summary": "Remove MCP OAuth", + "description": "Remove OAuth credentials for an MCP server", + "responses": { + "200": { + "description": "OAuth credentials removed", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "success": { "type": "boolean", "const": true } }, + "required": ["success"] + } + } + } + }, + "404": { + "description": "Not found", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/NotFoundError" } } } + } + } + } + }, + "/mcp/{name}/auth/callback": { + "post": { + "operationId": "mcp.auth.callback", + "parameters": [ + { "in": "query", "name": "directory", "schema": { "type": "string" } }, + { "schema": { "type": "string" }, "in": "path", "name": "name", "required": true } + ], + "summary": "Complete MCP OAuth", + "description": "Complete OAuth authentication for a Model Context Protocol (MCP) server using the authorization code.", + "responses": { + "200": { + "description": "OAuth authentication completed", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MCPStatus" } } } + }, + "400": { + "description": "Bad request", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BadRequestError" } } } + }, + "404": { + "description": "Not found", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/NotFoundError" } } } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "code": { "description": "Authorization code from OAuth callback", "type": "string" } }, + "required": ["code"] + } + } + } + } + } + }, + "/mcp/{name}/auth/authenticate": { + "post": { + "operationId": "mcp.auth.authenticate", + "parameters": [ + { "in": "query", "name": "directory", "schema": { "type": "string" } }, + { "schema": { "type": "string" }, "in": "path", "name": "name", "required": true } + ], + "summary": "Authenticate MCP OAuth", + "description": "Start OAuth flow and wait for callback (opens browser)", + "responses": { + "200": { + "description": "OAuth authentication completed", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MCPStatus" } } } + }, + "400": { + "description": "Bad request", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BadRequestError" } } } + }, + "404": { + "description": "Not found", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/NotFoundError" } } } + } + } + } + }, + "/mcp/{name}/connect": { + "post": { + "operationId": "mcp.connect", + "parameters": [ + { "in": "query", "name": "directory", "schema": { "type": "string" } }, + { "in": "path", "name": "name", "schema": { "type": "string" }, "required": true } + ], + "description": "Connect an MCP server", + "responses": { + "200": { + "description": "MCP server connected successfully", + "content": { "application/json": { "schema": { "type": "boolean" } } } + } + } + } + }, + "/mcp/{name}/disconnect": { + "post": { + "operationId": "mcp.disconnect", + "parameters": [ + { "in": "query", "name": "directory", "schema": { "type": "string" } }, + { "in": "path", "name": "name", "schema": { "type": "string" }, "required": true } + ], + "description": "Disconnect an MCP server", + "responses": { + "200": { + "description": "MCP server disconnected successfully", + "content": { "application/json": { "schema": { "type": "boolean" } } } + } + } + } + }, + "/tui/append-prompt": { + "post": { + "operationId": "tui.appendPrompt", + "parameters": [{ "in": "query", "name": "directory", "schema": { "type": "string" } }], + "summary": "Append TUI prompt", + "description": "Append prompt to the TUI", + "responses": { + "200": { + "description": "Prompt processed successfully", + "content": { "application/json": { "schema": { "type": "boolean" } } } + }, + "400": { + "description": "Bad request", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BadRequestError" } } } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { "type": "object", "properties": { "text": { "type": "string" } }, "required": ["text"] } + } + } + } + } + }, + "/tui/open-help": { + "post": { + "operationId": "tui.openHelp", + "parameters": [{ "in": "query", "name": "directory", "schema": { "type": "string" } }], + "summary": "Open help dialog", + "description": "Open the help dialog in the TUI to display user assistance information.", + "responses": { + "200": { + "description": "Help dialog opened successfully", + "content": { "application/json": { "schema": { "type": "boolean" } } } + } + } + } + }, + "/tui/open-sessions": { + "post": { + "operationId": "tui.openSessions", + "parameters": [{ "in": "query", "name": "directory", "schema": { "type": "string" } }], + "summary": "Open sessions dialog", + "description": "Open the session dialog", + "responses": { + "200": { + "description": "Session dialog opened successfully", + "content": { "application/json": { "schema": { "type": "boolean" } } } + } + } + } + }, + "/tui/open-themes": { + "post": { + "operationId": "tui.openThemes", + "parameters": [{ "in": "query", "name": "directory", "schema": { "type": "string" } }], + "summary": "Open themes dialog", + "description": "Open the theme dialog", + "responses": { + "200": { + "description": "Theme dialog opened successfully", + "content": { "application/json": { "schema": { "type": "boolean" } } } + } + } + } + }, + "/tui/open-models": { + "post": { + "operationId": "tui.openModels", + "parameters": [{ "in": "query", "name": "directory", "schema": { "type": "string" } }], + "summary": "Open models dialog", + "description": "Open the model dialog", + "responses": { + "200": { + "description": "Model dialog opened successfully", + "content": { "application/json": { "schema": { "type": "boolean" } } } + } + } + } + }, + "/tui/submit-prompt": { + "post": { + "operationId": "tui.submitPrompt", + "parameters": [{ "in": "query", "name": "directory", "schema": { "type": "string" } }], + "summary": "Submit TUI prompt", + "description": "Submit the prompt", + "responses": { + "200": { + "description": "Prompt submitted successfully", + "content": { "application/json": { "schema": { "type": "boolean" } } } + } + } + } + }, + "/tui/clear-prompt": { + "post": { + "operationId": "tui.clearPrompt", + "parameters": [{ "in": "query", "name": "directory", "schema": { "type": "string" } }], + "summary": "Clear TUI prompt", + "description": "Clear the prompt", + "responses": { + "200": { + "description": "Prompt cleared successfully", + "content": { "application/json": { "schema": { "type": "boolean" } } } + } + } + } + }, + "/tui/execute-command": { + "post": { + "operationId": "tui.executeCommand", + "parameters": [{ "in": "query", "name": "directory", "schema": { "type": "string" } }], + "summary": "Execute TUI command", + "description": "Execute a TUI command (e.g. agent_cycle)", + "responses": { + "200": { + "description": "Command executed successfully", + "content": { "application/json": { "schema": { "type": "boolean" } } } + }, + "400": { + "description": "Bad request", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BadRequestError" } } } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { "type": "object", "properties": { "command": { "type": "string" } }, "required": ["command"] } + } + } + } + } + }, + "/tui/show-toast": { + "post": { + "operationId": "tui.showToast", + "parameters": [{ "in": "query", "name": "directory", "schema": { "type": "string" } }], + "summary": "Show TUI toast", + "description": "Show a toast notification in the TUI", + "responses": { + "200": { + "description": "Toast notification shown successfully", + "content": { "application/json": { "schema": { "type": "boolean" } } } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "title": { "type": "string" }, + "message": { "type": "string" }, + "variant": { "type": "string", "enum": ["info", "success", "warning", "error"] }, + "duration": { "description": "Duration in milliseconds", "default": 5000, "type": "number" } + }, + "required": ["message", "variant"] + } + } + } + } + } + }, + "/tui/publish": { + "post": { + "operationId": "tui.publish", + "parameters": [{ "in": "query", "name": "directory", "schema": { "type": "string" } }], + "summary": "Publish TUI event", + "description": "Publish a TUI event", + "responses": { + "200": { + "description": "Event published successfully", + "content": { "application/json": { "schema": { "type": "boolean" } } } + }, + "400": { + "description": "Bad request", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BadRequestError" } } } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "anyOf": [ + { "$ref": "#/components/schemas/Event.tui.prompt.append" }, + { "$ref": "#/components/schemas/Event.tui.command.execute" }, + { "$ref": "#/components/schemas/Event.tui.toast.show" }, + { "$ref": "#/components/schemas/Event.tui.session.select" } + ] + } + } + } + } + } + }, + "/tui/select-session": { + "post": { + "operationId": "tui.selectSession", + "parameters": [{ "in": "query", "name": "directory", "schema": { "type": "string" } }], + "summary": "Select session", + "description": "Navigate the TUI to display the specified session.", + "responses": { + "200": { + "description": "Session selected successfully", + "content": { "application/json": { "schema": { "type": "boolean" } } } + }, + "400": { + "description": "Bad request", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BadRequestError" } } } + }, + "404": { + "description": "Not found", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/NotFoundError" } } } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "sessionID": { "description": "Session ID to navigate to", "type": "string", "pattern": "^ses" } + }, + "required": ["sessionID"] + } + } + } + } + } + }, + "/tui/control/next": { + "get": { + "operationId": "tui.control.next", + "parameters": [{ "in": "query", "name": "directory", "schema": { "type": "string" } }], + "summary": "Get next TUI request", + "description": "Retrieve the next TUI (Terminal User Interface) request from the queue for processing.", + "responses": { + "200": { + "description": "Next TUI request", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { "path": { "type": "string" }, "body": {} }, + "required": ["path", "body"] + } + } + } + } + } + } + }, + "/tui/control/response": { + "post": { + "operationId": "tui.control.response", + "parameters": [{ "in": "query", "name": "directory", "schema": { "type": "string" } }], + "summary": "Submit TUI response", + "description": "Submit a response to the TUI request queue to complete a pending request.", + "responses": { + "200": { + "description": "Response submitted successfully", + "content": { "application/json": { "schema": { "type": "boolean" } } } + } + }, + "requestBody": { "content": { "application/json": { "schema": {} } } } + } + }, + "/instance/dispose": { + "post": { + "operationId": "instance.dispose", + "parameters": [{ "in": "query", "name": "directory", "schema": { "type": "string" } }], + "summary": "Dispose instance", + "description": "Clean up and dispose the current OpenCode instance, releasing all resources.", + "responses": { + "200": { + "description": "Instance disposed", + "content": { "application/json": { "schema": { "type": "boolean" } } } + } + } + } + }, + "/path": { + "get": { + "operationId": "path.get", + "parameters": [{ "in": "query", "name": "directory", "schema": { "type": "string" } }], + "summary": "Get paths", + "description": "Retrieve the current working directory and related path information for the OpenCode instance.", + "responses": { + "200": { + "description": "Path", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Path" } } } + } + } + } + }, + "/vcs": { + "get": { + "operationId": "vcs.get", + "parameters": [{ "in": "query", "name": "directory", "schema": { "type": "string" } }], + "summary": "Get VCS info", + "description": "Retrieve version control system (VCS) information for the current project, such as git branch.", + "responses": { + "200": { + "description": "VCS info", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/VcsInfo" } } } + } + } + } + }, + "/command": { + "get": { + "operationId": "command.list", + "parameters": [{ "in": "query", "name": "directory", "schema": { "type": "string" } }], + "summary": "List commands", + "description": "Get a list of all available commands in the OpenCode system.", + "responses": { + "200": { + "description": "List of commands", + "content": { + "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/Command" } } } + } + } + } + } + }, + "/log": { + "post": { + "operationId": "app.log", + "parameters": [{ "in": "query", "name": "directory", "schema": { "type": "string" } }], + "summary": "Write log", + "description": "Write a log entry to the server logs with specified level and metadata.", + "responses": { + "200": { + "description": "Log entry written successfully", + "content": { "application/json": { "schema": { "type": "boolean" } } } + }, + "400": { + "description": "Bad request", + "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BadRequestError" } } } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "service": { "description": "Service name for the log entry", "type": "string" }, + "level": { "description": "Log level", "type": "string", "enum": ["debug", "info", "error", "warn"] }, + "message": { "description": "Log message", "type": "string" }, + "extra": { + "description": "Additional metadata for the log entry", + "type": "object", + "propertyNames": { "type": "string" }, + "additionalProperties": {} + } + }, + "required": ["service", "level", "message"] + } + } + } + } + } + }, + "/agent": { + "get": { + "operationId": "app.agents", + "parameters": [{ "in": "query", "name": "directory", "schema": { "type": "string" } }], + "summary": "List agents", + "description": "Get a list of all available AI agents in the OpenCode system.", + "responses": { + "200": { + "description": "List of agents", + "content": { + "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/Agent" } } } + } + } + } + } + }, + "/skill": { + "get": { + "operationId": "app.skills", + "parameters": [{ "in": "query", "name": "directory", "schema": { "type": "string" } }], + "summary": "List skills", + "description": "Get a list of all available skills in the OpenCode system.", + "responses": { + "200": { + "description": "List of skills", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "description": { "type": "string" }, + "location": { "type": "string" }, + "content": { "type": "string" } + }, + "required": ["name", "description", "location", "content"] + } + } + } + } + } + } + } + }, + "/lsp": { + "get": { + "operationId": "lsp.status", + "parameters": [{ "in": "query", "name": "directory", "schema": { "type": "string" } }], + "summary": "Get LSP status", + "description": "Get LSP server status", + "responses": { + "200": { + "description": "LSP server status", + "content": { + "application/json": { + "schema": { "type": "array", "items": { "$ref": "#/components/schemas/LSPStatus" } } + } + } + } + } + } + }, + "/formatter": { + "get": { + "operationId": "formatter.status", + "parameters": [{ "in": "query", "name": "directory", "schema": { "type": "string" } }], + "summary": "Get formatter status", + "description": "Get formatter status", + "responses": { + "200": { + "description": "Formatter status", + "content": { + "application/json": { + "schema": { "type": "array", "items": { "$ref": "#/components/schemas/FormatterStatus" } } + } + } + } + } + } + }, + "/event": { + "get": { + "operationId": "event.subscribe", + "parameters": [{ "in": "query", "name": "directory", "schema": { "type": "string" } }], + "summary": "Subscribe to events", + "description": "Get events", + "responses": { + "200": { + "description": "Event stream", + "content": { "text/event-stream": { "schema": { "$ref": "#/components/schemas/Event" } } } + } + } + } + } + }, + "components": { + "schemas": { + "Event.server.connected": { + "type": "object", + "properties": { + "type": { "type": "string", "const": "server.connected" }, + "properties": { "type": "object", "properties": {} } + }, + "required": ["type", "properties"] + }, + "Event.global.disposed": { + "type": "object", + "properties": { + "type": { "type": "string", "const": "global.disposed" }, + "properties": { "type": "object", "properties": {} } + }, + "required": ["type", "properties"] + }, + "Event.tui.prompt.append": { + "type": "object", + "properties": { + "type": { "type": "string", "const": "tui.prompt.append" }, + "properties": { "type": "object", "properties": { "text": { "type": "string" } }, "required": ["text"] } + }, + "required": ["type", "properties"] + }, + "Event.tui.command.execute": { + "type": "object", + "properties": { + "type": { "type": "string", "const": "tui.command.execute" }, + "properties": { + "type": "object", + "properties": { + "command": { + "anyOf": [ + { + "type": "string", + "enum": [ + "session.list", + "session.new", + "session.share", + "session.interrupt", + "session.compact", + "session.page.up", + "session.page.down", + "session.line.up", + "session.line.down", + "session.half.page.up", + "session.half.page.down", + "session.first", + "session.last", + "prompt.clear", + "prompt.submit", + "agent.cycle" + ] + }, + { "type": "string" } + ] + } + }, + "required": ["command"] + } + }, + "required": ["type", "properties"] + }, + "Event.tui.toast.show": { + "type": "object", + "properties": { + "type": { "type": "string", "const": "tui.toast.show" }, + "properties": { + "type": "object", + "properties": { + "title": { "type": "string" }, + "message": { "type": "string" }, + "variant": { "type": "string", "enum": ["info", "success", "warning", "error"] }, + "duration": { "description": "Duration in milliseconds", "default": 5000, "type": "number" } + }, + "required": ["message", "variant"] + } + }, + "required": ["type", "properties"] + }, + "Event.tui.session.select": { + "type": "object", + "properties": { + "type": { "type": "string", "const": "tui.session.select" }, + "properties": { + "type": "object", + "properties": { + "sessionID": { "description": "Session ID to navigate to", "type": "string", "pattern": "^ses" } + }, + "required": ["sessionID"] + } + }, + "required": ["type", "properties"] + }, + "Event.installation.updated": { + "type": "object", + "properties": { + "type": { "type": "string", "const": "installation.updated" }, + "properties": { "type": "object", "properties": { "version": { "type": "string" } }, "required": ["version"] } + }, + "required": ["type", "properties"] + }, + "Event.installation.update-available": { + "type": "object", + "properties": { + "type": { "type": "string", "const": "installation.update-available" }, + "properties": { "type": "object", "properties": { "version": { "type": "string" } }, "required": ["version"] } + }, + "required": ["type", "properties"] + }, + "Project": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "worktree": { "type": "string" }, + "vcs": { "type": "string", "const": "git" }, + "name": { "type": "string" }, + "icon": { + "type": "object", + "properties": { + "url": { "type": "string" }, + "override": { "type": "string" }, + "color": { "type": "string" } + } + }, + "commands": { + "type": "object", + "properties": { + "start": { + "description": "Startup script to run when creating a new workspace (worktree)", + "type": "string" + } + } + }, + "time": { + "type": "object", + "properties": { + "created": { "type": "number" }, + "updated": { "type": "number" }, + "initialized": { "type": "number" } + }, + "required": ["created", "updated"] + }, + "sandboxes": { "type": "array", "items": { "type": "string" } } + }, + "required": ["id", "worktree", "time", "sandboxes"] + }, + "Event.project.updated": { + "type": "object", + "properties": { + "type": { "type": "string", "const": "project.updated" }, + "properties": { "$ref": "#/components/schemas/Project" } + }, + "required": ["type", "properties"] + }, + "Event.server.instance.disposed": { + "type": "object", + "properties": { + "type": { "type": "string", "const": "server.instance.disposed" }, + "properties": { + "type": "object", + "properties": { "directory": { "type": "string" } }, + "required": ["directory"] + } + }, + "required": ["type", "properties"] + }, + "Event.file.edited": { + "type": "object", + "properties": { + "type": { "type": "string", "const": "file.edited" }, + "properties": { "type": "object", "properties": { "file": { "type": "string" } }, "required": ["file"] } + }, + "required": ["type", "properties"] + }, + "Event.worktree.ready": { + "type": "object", + "properties": { + "type": { "type": "string", "const": "worktree.ready" }, + "properties": { + "type": "object", + "properties": { "name": { "type": "string" }, "branch": { "type": "string" } }, + "required": ["name", "branch"] + } + }, + "required": ["type", "properties"] + }, + "Event.worktree.failed": { + "type": "object", + "properties": { + "type": { "type": "string", "const": "worktree.failed" }, + "properties": { "type": "object", "properties": { "message": { "type": "string" } }, "required": ["message"] } + }, + "required": ["type", "properties"] + }, + "Event.lsp.client.diagnostics": { + "type": "object", + "properties": { + "type": { "type": "string", "const": "lsp.client.diagnostics" }, + "properties": { + "type": "object", + "properties": { "serverID": { "type": "string" }, "path": { "type": "string" } }, + "required": ["serverID", "path"] + } + }, + "required": ["type", "properties"] + }, + "PermissionRequest": { + "type": "object", + "properties": { + "id": { "type": "string", "pattern": "^per.*" }, + "sessionID": { "type": "string", "pattern": "^ses.*" }, + "permission": { "type": "string" }, + "patterns": { "type": "array", "items": { "type": "string" } }, + "metadata": { "type": "object", "propertyNames": { "type": "string" }, "additionalProperties": {} }, + "always": { "type": "array", "items": { "type": "string" } }, + "tool": { + "type": "object", + "properties": { "messageID": { "type": "string" }, "callID": { "type": "string" } }, + "required": ["messageID", "callID"] + } + }, + "required": ["id", "sessionID", "permission", "patterns", "metadata", "always"] + }, + "Event.permission.asked": { + "type": "object", + "properties": { + "type": { "type": "string", "const": "permission.asked" }, + "properties": { "$ref": "#/components/schemas/PermissionRequest" } + }, + "required": ["type", "properties"] + }, + "Event.permission.replied": { + "type": "object", + "properties": { + "type": { "type": "string", "const": "permission.replied" }, + "properties": { + "type": "object", + "properties": { + "sessionID": { "type": "string" }, + "requestID": { "type": "string" }, + "reply": { "type": "string", "enum": ["once", "always", "reject"] } + }, + "required": ["sessionID", "requestID", "reply"] + } + }, + "required": ["type", "properties"] + }, + "SessionStatus": { + "anyOf": [ + { "type": "object", "properties": { "type": { "type": "string", "const": "idle" } }, "required": ["type"] }, + { + "type": "object", + "properties": { + "type": { "type": "string", "const": "retry" }, + "attempt": { "type": "number" }, + "message": { "type": "string" }, + "next": { "type": "number" } + }, + "required": ["type", "attempt", "message", "next"] + }, + { "type": "object", "properties": { "type": { "type": "string", "const": "busy" } }, "required": ["type"] } + ] + }, + "Event.session.status": { + "type": "object", + "properties": { + "type": { "type": "string", "const": "session.status" }, + "properties": { + "type": "object", + "properties": { + "sessionID": { "type": "string" }, + "status": { "$ref": "#/components/schemas/SessionStatus" } + }, + "required": ["sessionID", "status"] + } + }, + "required": ["type", "properties"] + }, + "Event.session.idle": { + "type": "object", + "properties": { + "type": { "type": "string", "const": "session.idle" }, + "properties": { + "type": "object", + "properties": { "sessionID": { "type": "string" } }, + "required": ["sessionID"] + } + }, + "required": ["type", "properties"] + }, + "QuestionOption": { + "type": "object", + "properties": { + "label": { "description": "Display text (1-5 words, concise)", "type": "string" }, + "description": { "description": "Explanation of choice", "type": "string" } + }, + "required": ["label", "description"] + }, + "QuestionInfo": { + "type": "object", + "properties": { + "question": { "description": "Complete question", "type": "string" }, + "header": { "description": "Very short label (max 30 chars)", "type": "string" }, + "options": { + "description": "Available choices", + "type": "array", + "items": { "$ref": "#/components/schemas/QuestionOption" } + }, + "multiple": { "description": "Allow selecting multiple choices", "type": "boolean" }, + "custom": { "description": "Allow typing a custom answer (default: true)", "type": "boolean" } + }, + "required": ["question", "header", "options"] + }, + "QuestionRequest": { + "type": "object", + "properties": { + "id": { "type": "string", "pattern": "^que.*" }, + "sessionID": { "type": "string", "pattern": "^ses.*" }, + "questions": { + "description": "Questions to ask", + "type": "array", + "items": { "$ref": "#/components/schemas/QuestionInfo" } + }, + "tool": { + "type": "object", + "properties": { "messageID": { "type": "string" }, "callID": { "type": "string" } }, + "required": ["messageID", "callID"] + } + }, + "required": ["id", "sessionID", "questions"] + }, + "Event.question.asked": { + "type": "object", + "properties": { + "type": { "type": "string", "const": "question.asked" }, + "properties": { "$ref": "#/components/schemas/QuestionRequest" } + }, + "required": ["type", "properties"] + }, + "QuestionAnswer": { "type": "array", "items": { "type": "string" } }, + "Event.question.replied": { + "type": "object", + "properties": { + "type": { "type": "string", "const": "question.replied" }, + "properties": { + "type": "object", + "properties": { + "sessionID": { "type": "string" }, + "requestID": { "type": "string" }, + "answers": { "type": "array", "items": { "$ref": "#/components/schemas/QuestionAnswer" } } + }, + "required": ["sessionID", "requestID", "answers"] + } + }, + "required": ["type", "properties"] + }, + "Event.question.rejected": { + "type": "object", + "properties": { + "type": { "type": "string", "const": "question.rejected" }, + "properties": { + "type": "object", + "properties": { "sessionID": { "type": "string" }, "requestID": { "type": "string" } }, + "required": ["sessionID", "requestID"] + } + }, + "required": ["type", "properties"] + }, + "Todo": { + "type": "object", + "properties": { + "content": { "description": "Brief description of the task", "type": "string" }, + "status": { + "description": "Current status of the task: pending, in_progress, completed, cancelled", + "type": "string" + }, + "priority": { "description": "Priority level of the task: high, medium, low", "type": "string" }, + "id": { "description": "Unique identifier for the todo item", "type": "string" } + }, + "required": ["content", "status", "priority", "id"] + }, + "Event.todo.updated": { + "type": "object", + "properties": { + "type": { "type": "string", "const": "todo.updated" }, + "properties": { + "type": "object", + "properties": { + "sessionID": { "type": "string" }, + "todos": { "type": "array", "items": { "$ref": "#/components/schemas/Todo" } } + }, + "required": ["sessionID", "todos"] + } + }, + "required": ["type", "properties"] + }, + "Pty": { + "type": "object", + "properties": { + "id": { "type": "string", "pattern": "^pty.*" }, + "title": { "type": "string" }, + "command": { "type": "string" }, + "args": { "type": "array", "items": { "type": "string" } }, + "cwd": { "type": "string" }, + "status": { "type": "string", "enum": ["running", "exited"] }, + "pid": { "type": "number" } + }, + "required": ["id", "title", "command", "args", "cwd", "status", "pid"] + }, + "Event.pty.created": { + "type": "object", + "properties": { + "type": { "type": "string", "const": "pty.created" }, + "properties": { + "type": "object", + "properties": { "info": { "$ref": "#/components/schemas/Pty" } }, + "required": ["info"] + } + }, + "required": ["type", "properties"] + }, + "Event.pty.updated": { + "type": "object", + "properties": { + "type": { "type": "string", "const": "pty.updated" }, + "properties": { + "type": "object", + "properties": { "info": { "$ref": "#/components/schemas/Pty" } }, + "required": ["info"] + } + }, + "required": ["type", "properties"] + }, + "Event.pty.exited": { + "type": "object", + "properties": { + "type": { "type": "string", "const": "pty.exited" }, + "properties": { + "type": "object", + "properties": { "id": { "type": "string", "pattern": "^pty.*" }, "exitCode": { "type": "number" } }, + "required": ["id", "exitCode"] + } + }, + "required": ["type", "properties"] + }, + "Event.pty.deleted": { + "type": "object", + "properties": { + "type": { "type": "string", "const": "pty.deleted" }, + "properties": { + "type": "object", + "properties": { "id": { "type": "string", "pattern": "^pty.*" } }, + "required": ["id"] + } + }, + "required": ["type", "properties"] + }, + "Event.file.watcher.updated": { + "type": "object", + "properties": { + "type": { "type": "string", "const": "file.watcher.updated" }, + "properties": { + "type": "object", + "properties": { + "file": { "type": "string" }, + "event": { + "anyOf": [ + { "type": "string", "const": "add" }, + { "type": "string", "const": "change" }, + { "type": "string", "const": "unlink" } + ] + } + }, + "required": ["file", "event"] + } + }, + "required": ["type", "properties"] + }, + "Event.mcp.tools.changed": { + "type": "object", + "properties": { + "type": { "type": "string", "const": "mcp.tools.changed" }, + "properties": { "type": "object", "properties": { "server": { "type": "string" } }, "required": ["server"] } + }, + "required": ["type", "properties"] + }, + "Event.mcp.browser.open.failed": { + "type": "object", + "properties": { + "type": { "type": "string", "const": "mcp.browser.open.failed" }, + "properties": { + "type": "object", + "properties": { "mcpName": { "type": "string" }, "url": { "type": "string" } }, + "required": ["mcpName", "url"] + } + }, + "required": ["type", "properties"] + }, + "Event.lsp.updated": { + "type": "object", + "properties": { + "type": { "type": "string", "const": "lsp.updated" }, + "properties": { "type": "object", "properties": {} } + }, + "required": ["type", "properties"] + }, + "Event.vcs.branch.updated": { + "type": "object", + "properties": { + "type": { "type": "string", "const": "vcs.branch.updated" }, + "properties": { "type": "object", "properties": { "branch": { "type": "string" } } } + }, + "required": ["type", "properties"] + }, + "Event.command.executed": { + "type": "object", + "properties": { + "type": { "type": "string", "const": "command.executed" }, + "properties": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "sessionID": { "type": "string", "pattern": "^ses.*" }, + "arguments": { "type": "string" }, + "messageID": { "type": "string", "pattern": "^msg.*" } + }, + "required": ["name", "sessionID", "arguments", "messageID"] + } + }, + "required": ["type", "properties"] + }, + "FileDiff": { + "type": "object", + "properties": { + "file": { "type": "string" }, + "before": { "type": "string" }, + "after": { "type": "string" }, + "additions": { "type": "number" }, + "deletions": { "type": "number" } + }, + "required": ["file", "before", "after", "additions", "deletions"] + }, + "UserMessage": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "sessionID": { "type": "string" }, + "role": { "type": "string", "const": "user" }, + "time": { "type": "object", "properties": { "created": { "type": "number" } }, "required": ["created"] }, + "summary": { + "type": "object", + "properties": { + "title": { "type": "string" }, + "body": { "type": "string" }, + "diffs": { "type": "array", "items": { "$ref": "#/components/schemas/FileDiff" } } + }, + "required": ["diffs"] + }, + "agent": { "type": "string" }, + "model": { + "type": "object", + "properties": { "providerID": { "type": "string" }, "modelID": { "type": "string" } }, + "required": ["providerID", "modelID"] + }, + "system": { "type": "string" }, + "tools": { + "type": "object", + "propertyNames": { "type": "string" }, + "additionalProperties": { "type": "boolean" } + }, + "variant": { "type": "string" } + }, + "required": ["id", "sessionID", "role", "time", "agent", "model"] + }, + "ProviderAuthError": { + "type": "object", + "properties": { + "name": { "type": "string", "const": "ProviderAuthError" }, + "data": { + "type": "object", + "properties": { "providerID": { "type": "string" }, "message": { "type": "string" } }, + "required": ["providerID", "message"] + } + }, + "required": ["name", "data"] + }, + "UnknownError": { + "type": "object", + "properties": { + "name": { "type": "string", "const": "UnknownError" }, + "data": { "type": "object", "properties": { "message": { "type": "string" } }, "required": ["message"] } + }, + "required": ["name", "data"] + }, + "MessageOutputLengthError": { + "type": "object", + "properties": { + "name": { "type": "string", "const": "MessageOutputLengthError" }, + "data": { "type": "object", "properties": {} } + }, + "required": ["name", "data"] + }, + "MessageAbortedError": { + "type": "object", + "properties": { + "name": { "type": "string", "const": "MessageAbortedError" }, + "data": { "type": "object", "properties": { "message": { "type": "string" } }, "required": ["message"] } + }, + "required": ["name", "data"] + }, + "APIError": { + "type": "object", + "properties": { + "name": { "type": "string", "const": "APIError" }, + "data": { + "type": "object", + "properties": { + "message": { "type": "string" }, + "statusCode": { "type": "number" }, + "isRetryable": { "type": "boolean" }, + "responseHeaders": { + "type": "object", + "propertyNames": { "type": "string" }, + "additionalProperties": { "type": "string" } + }, + "responseBody": { "type": "string" }, + "metadata": { + "type": "object", + "propertyNames": { "type": "string" }, + "additionalProperties": { "type": "string" } + } + }, + "required": ["message", "isRetryable"] + } + }, + "required": ["name", "data"] + }, + "AssistantMessage": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "sessionID": { "type": "string" }, + "role": { "type": "string", "const": "assistant" }, + "time": { + "type": "object", + "properties": { "created": { "type": "number" }, "completed": { "type": "number" } }, + "required": ["created"] + }, + "error": { + "anyOf": [ + { "$ref": "#/components/schemas/ProviderAuthError" }, + { "$ref": "#/components/schemas/UnknownError" }, + { "$ref": "#/components/schemas/MessageOutputLengthError" }, + { "$ref": "#/components/schemas/MessageAbortedError" }, + { "$ref": "#/components/schemas/APIError" } + ] + }, + "parentID": { "type": "string" }, + "modelID": { "type": "string" }, + "providerID": { "type": "string" }, + "mode": { "type": "string" }, + "agent": { "type": "string" }, + "path": { + "type": "object", + "properties": { "cwd": { "type": "string" }, "root": { "type": "string" } }, + "required": ["cwd", "root"] + }, + "summary": { "type": "boolean" }, + "cost": { "type": "number" }, + "tokens": { + "type": "object", + "properties": { + "input": { "type": "number" }, + "output": { "type": "number" }, + "reasoning": { "type": "number" }, + "cache": { + "type": "object", + "properties": { "read": { "type": "number" }, "write": { "type": "number" } }, + "required": ["read", "write"] + } + }, + "required": ["input", "output", "reasoning", "cache"] + }, + "finish": { "type": "string" } + }, + "required": [ + "id", + "sessionID", + "role", + "time", + "parentID", + "modelID", + "providerID", + "mode", + "agent", + "path", + "cost", + "tokens" + ] + }, + "Message": { + "anyOf": [{ "$ref": "#/components/schemas/UserMessage" }, { "$ref": "#/components/schemas/AssistantMessage" }] + }, + "Event.message.updated": { + "type": "object", + "properties": { + "type": { "type": "string", "const": "message.updated" }, + "properties": { + "type": "object", + "properties": { "info": { "$ref": "#/components/schemas/Message" } }, + "required": ["info"] + } + }, + "required": ["type", "properties"] + }, + "Event.message.removed": { + "type": "object", + "properties": { + "type": { "type": "string", "const": "message.removed" }, + "properties": { + "type": "object", + "properties": { "sessionID": { "type": "string" }, "messageID": { "type": "string" } }, + "required": ["sessionID", "messageID"] + } + }, + "required": ["type", "properties"] + }, + "TextPart": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "sessionID": { "type": "string" }, + "messageID": { "type": "string" }, + "type": { "type": "string", "const": "text" }, + "text": { "type": "string" }, + "synthetic": { "type": "boolean" }, + "ignored": { "type": "boolean" }, + "time": { + "type": "object", + "properties": { "start": { "type": "number" }, "end": { "type": "number" } }, + "required": ["start"] + }, + "metadata": { "type": "object", "propertyNames": { "type": "string" }, "additionalProperties": {} } + }, + "required": ["id", "sessionID", "messageID", "type", "text"] + }, + "SubtaskPart": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "sessionID": { "type": "string" }, + "messageID": { "type": "string" }, + "type": { "type": "string", "const": "subtask" }, + "prompt": { "type": "string" }, + "description": { "type": "string" }, + "agent": { "type": "string" }, + "model": { + "type": "object", + "properties": { "providerID": { "type": "string" }, "modelID": { "type": "string" } }, + "required": ["providerID", "modelID"] + }, + "command": { "type": "string" } + }, + "required": ["id", "sessionID", "messageID", "type", "prompt", "description", "agent"] + }, + "ReasoningPart": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "sessionID": { "type": "string" }, + "messageID": { "type": "string" }, + "type": { "type": "string", "const": "reasoning" }, + "text": { "type": "string" }, + "metadata": { "type": "object", "propertyNames": { "type": "string" }, "additionalProperties": {} }, + "time": { + "type": "object", + "properties": { "start": { "type": "number" }, "end": { "type": "number" } }, + "required": ["start"] + } + }, + "required": ["id", "sessionID", "messageID", "type", "text", "time"] + }, + "FilePartSourceText": { + "type": "object", + "properties": { + "value": { "type": "string" }, + "start": { "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, + "end": { "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 } + }, + "required": ["value", "start", "end"] + }, + "FileSource": { + "type": "object", + "properties": { + "text": { "$ref": "#/components/schemas/FilePartSourceText" }, + "type": { "type": "string", "const": "file" }, + "path": { "type": "string" } + }, + "required": ["text", "type", "path"] + }, + "Range": { + "type": "object", + "properties": { + "start": { + "type": "object", + "properties": { "line": { "type": "number" }, "character": { "type": "number" } }, + "required": ["line", "character"] + }, + "end": { + "type": "object", + "properties": { "line": { "type": "number" }, "character": { "type": "number" } }, + "required": ["line", "character"] + } + }, + "required": ["start", "end"] + }, + "SymbolSource": { + "type": "object", + "properties": { + "text": { "$ref": "#/components/schemas/FilePartSourceText" }, + "type": { "type": "string", "const": "symbol" }, + "path": { "type": "string" }, + "range": { "$ref": "#/components/schemas/Range" }, + "name": { "type": "string" }, + "kind": { "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 } + }, + "required": ["text", "type", "path", "range", "name", "kind"] + }, + "ResourceSource": { + "type": "object", + "properties": { + "text": { "$ref": "#/components/schemas/FilePartSourceText" }, + "type": { "type": "string", "const": "resource" }, + "clientName": { "type": "string" }, + "uri": { "type": "string" } + }, + "required": ["text", "type", "clientName", "uri"] + }, + "FilePartSource": { + "anyOf": [ + { "$ref": "#/components/schemas/FileSource" }, + { "$ref": "#/components/schemas/SymbolSource" }, + { "$ref": "#/components/schemas/ResourceSource" } + ] + }, + "FilePart": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "sessionID": { "type": "string" }, + "messageID": { "type": "string" }, + "type": { "type": "string", "const": "file" }, + "mime": { "type": "string" }, + "filename": { "type": "string" }, + "url": { "type": "string" }, + "source": { "$ref": "#/components/schemas/FilePartSource" } + }, + "required": ["id", "sessionID", "messageID", "type", "mime", "url"] + }, + "ToolStatePending": { + "type": "object", + "properties": { + "status": { "type": "string", "const": "pending" }, + "input": { "type": "object", "propertyNames": { "type": "string" }, "additionalProperties": {} }, + "raw": { "type": "string" } + }, + "required": ["status", "input", "raw"] + }, + "ToolStateRunning": { + "type": "object", + "properties": { + "status": { "type": "string", "const": "running" }, + "input": { "type": "object", "propertyNames": { "type": "string" }, "additionalProperties": {} }, + "title": { "type": "string" }, + "metadata": { "type": "object", "propertyNames": { "type": "string" }, "additionalProperties": {} }, + "time": { "type": "object", "properties": { "start": { "type": "number" } }, "required": ["start"] } + }, + "required": ["status", "input", "time"] + }, + "ToolStateCompleted": { + "type": "object", + "properties": { + "status": { "type": "string", "const": "completed" }, + "input": { "type": "object", "propertyNames": { "type": "string" }, "additionalProperties": {} }, + "output": { "type": "string" }, + "title": { "type": "string" }, + "metadata": { "type": "object", "propertyNames": { "type": "string" }, "additionalProperties": {} }, + "time": { + "type": "object", + "properties": { + "start": { "type": "number" }, + "end": { "type": "number" }, + "compacted": { "type": "number" } + }, + "required": ["start", "end"] + }, + "attachments": { "type": "array", "items": { "$ref": "#/components/schemas/FilePart" } } + }, + "required": ["status", "input", "output", "title", "metadata", "time"] + }, + "ToolStateError": { + "type": "object", + "properties": { + "status": { "type": "string", "const": "error" }, + "input": { "type": "object", "propertyNames": { "type": "string" }, "additionalProperties": {} }, + "error": { "type": "string" }, + "metadata": { "type": "object", "propertyNames": { "type": "string" }, "additionalProperties": {} }, + "time": { + "type": "object", + "properties": { "start": { "type": "number" }, "end": { "type": "number" } }, + "required": ["start", "end"] + } + }, + "required": ["status", "input", "error", "time"] + }, + "ToolState": { + "anyOf": [ + { "$ref": "#/components/schemas/ToolStatePending" }, + { "$ref": "#/components/schemas/ToolStateRunning" }, + { "$ref": "#/components/schemas/ToolStateCompleted" }, + { "$ref": "#/components/schemas/ToolStateError" } + ] + }, + "ToolPart": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "sessionID": { "type": "string" }, + "messageID": { "type": "string" }, + "type": { "type": "string", "const": "tool" }, + "callID": { "type": "string" }, + "tool": { "type": "string" }, + "state": { "$ref": "#/components/schemas/ToolState" }, + "metadata": { "type": "object", "propertyNames": { "type": "string" }, "additionalProperties": {} } + }, + "required": ["id", "sessionID", "messageID", "type", "callID", "tool", "state"] + }, + "StepStartPart": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "sessionID": { "type": "string" }, + "messageID": { "type": "string" }, + "type": { "type": "string", "const": "step-start" }, + "snapshot": { "type": "string" } + }, + "required": ["id", "sessionID", "messageID", "type"] + }, + "StepFinishPart": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "sessionID": { "type": "string" }, + "messageID": { "type": "string" }, + "type": { "type": "string", "const": "step-finish" }, + "reason": { "type": "string" }, + "snapshot": { "type": "string" }, + "cost": { "type": "number" }, + "tokens": { + "type": "object", + "properties": { + "input": { "type": "number" }, + "output": { "type": "number" }, + "reasoning": { "type": "number" }, + "cache": { + "type": "object", + "properties": { "read": { "type": "number" }, "write": { "type": "number" } }, + "required": ["read", "write"] + } + }, + "required": ["input", "output", "reasoning", "cache"] + } + }, + "required": ["id", "sessionID", "messageID", "type", "reason", "cost", "tokens"] + }, + "SnapshotPart": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "sessionID": { "type": "string" }, + "messageID": { "type": "string" }, + "type": { "type": "string", "const": "snapshot" }, + "snapshot": { "type": "string" } + }, + "required": ["id", "sessionID", "messageID", "type", "snapshot"] + }, + "PatchPart": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "sessionID": { "type": "string" }, + "messageID": { "type": "string" }, + "type": { "type": "string", "const": "patch" }, + "hash": { "type": "string" }, + "files": { "type": "array", "items": { "type": "string" } } + }, + "required": ["id", "sessionID", "messageID", "type", "hash", "files"] + }, + "AgentPart": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "sessionID": { "type": "string" }, + "messageID": { "type": "string" }, + "type": { "type": "string", "const": "agent" }, + "name": { "type": "string" }, + "source": { + "type": "object", + "properties": { + "value": { "type": "string" }, + "start": { "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, + "end": { "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 } + }, + "required": ["value", "start", "end"] + } + }, + "required": ["id", "sessionID", "messageID", "type", "name"] + }, + "RetryPart": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "sessionID": { "type": "string" }, + "messageID": { "type": "string" }, + "type": { "type": "string", "const": "retry" }, + "attempt": { "type": "number" }, + "error": { "$ref": "#/components/schemas/APIError" }, + "time": { "type": "object", "properties": { "created": { "type": "number" } }, "required": ["created"] } + }, + "required": ["id", "sessionID", "messageID", "type", "attempt", "error", "time"] + }, + "CompactionPart": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "sessionID": { "type": "string" }, + "messageID": { "type": "string" }, + "type": { "type": "string", "const": "compaction" }, + "auto": { "type": "boolean" } + }, + "required": ["id", "sessionID", "messageID", "type", "auto"] + }, + "Part": { + "anyOf": [ + { "$ref": "#/components/schemas/TextPart" }, + { "$ref": "#/components/schemas/SubtaskPart" }, + { "$ref": "#/components/schemas/ReasoningPart" }, + { "$ref": "#/components/schemas/FilePart" }, + { "$ref": "#/components/schemas/ToolPart" }, + { "$ref": "#/components/schemas/StepStartPart" }, + { "$ref": "#/components/schemas/StepFinishPart" }, + { "$ref": "#/components/schemas/SnapshotPart" }, + { "$ref": "#/components/schemas/PatchPart" }, + { "$ref": "#/components/schemas/AgentPart" }, + { "$ref": "#/components/schemas/RetryPart" }, + { "$ref": "#/components/schemas/CompactionPart" } + ] + }, + "Event.message.part.updated": { + "type": "object", + "properties": { + "type": { "type": "string", "const": "message.part.updated" }, + "properties": { + "type": "object", + "properties": { "part": { "$ref": "#/components/schemas/Part" }, "delta": { "type": "string" } }, + "required": ["part"] + } + }, + "required": ["type", "properties"] + }, + "Event.message.part.removed": { + "type": "object", + "properties": { + "type": { "type": "string", "const": "message.part.removed" }, + "properties": { + "type": "object", + "properties": { + "sessionID": { "type": "string" }, + "messageID": { "type": "string" }, + "partID": { "type": "string" } + }, + "required": ["sessionID", "messageID", "partID"] + } + }, + "required": ["type", "properties"] + }, + "Event.session.compacted": { + "type": "object", + "properties": { + "type": { "type": "string", "const": "session.compacted" }, + "properties": { + "type": "object", + "properties": { "sessionID": { "type": "string" } }, + "required": ["sessionID"] + } + }, + "required": ["type", "properties"] + }, + "PermissionAction": { "type": "string", "enum": ["allow", "deny", "ask"] }, + "PermissionRule": { + "type": "object", + "properties": { + "permission": { "type": "string" }, + "pattern": { "type": "string" }, + "action": { "$ref": "#/components/schemas/PermissionAction" } + }, + "required": ["permission", "pattern", "action"] + }, + "PermissionRuleset": { "type": "array", "items": { "$ref": "#/components/schemas/PermissionRule" } }, + "Session": { + "type": "object", + "properties": { + "id": { "type": "string", "pattern": "^ses.*" }, + "slug": { "type": "string" }, + "projectID": { "type": "string" }, + "directory": { "type": "string" }, + "parentID": { "type": "string", "pattern": "^ses.*" }, + "summary": { + "type": "object", + "properties": { + "additions": { "type": "number" }, + "deletions": { "type": "number" }, + "files": { "type": "number" }, + "diffs": { "type": "array", "items": { "$ref": "#/components/schemas/FileDiff" } } + }, + "required": ["additions", "deletions", "files"] + }, + "share": { "type": "object", "properties": { "url": { "type": "string" } }, "required": ["url"] }, + "title": { "type": "string" }, + "version": { "type": "string" }, + "time": { + "type": "object", + "properties": { + "created": { "type": "number" }, + "updated": { "type": "number" }, + "compacting": { "type": "number" }, + "archived": { "type": "number" } + }, + "required": ["created", "updated"] + }, + "permission": { "$ref": "#/components/schemas/PermissionRuleset" }, + "revert": { + "type": "object", + "properties": { + "messageID": { "type": "string" }, + "partID": { "type": "string" }, + "snapshot": { "type": "string" }, + "diff": { "type": "string" } + }, + "required": ["messageID"] + } + }, + "required": ["id", "slug", "projectID", "directory", "title", "version", "time"] + }, + "Event.session.created": { + "type": "object", + "properties": { + "type": { "type": "string", "const": "session.created" }, + "properties": { + "type": "object", + "properties": { "info": { "$ref": "#/components/schemas/Session" } }, + "required": ["info"] + } + }, + "required": ["type", "properties"] + }, + "Event.session.updated": { + "type": "object", + "properties": { + "type": { "type": "string", "const": "session.updated" }, + "properties": { + "type": "object", + "properties": { "info": { "$ref": "#/components/schemas/Session" } }, + "required": ["info"] + } + }, + "required": ["type", "properties"] + }, + "Event.session.deleted": { + "type": "object", + "properties": { + "type": { "type": "string", "const": "session.deleted" }, + "properties": { + "type": "object", + "properties": { "info": { "$ref": "#/components/schemas/Session" } }, + "required": ["info"] + } + }, + "required": ["type", "properties"] + }, + "Event.session.diff": { + "type": "object", + "properties": { + "type": { "type": "string", "const": "session.diff" }, + "properties": { + "type": "object", + "properties": { + "sessionID": { "type": "string" }, + "diff": { "type": "array", "items": { "$ref": "#/components/schemas/FileDiff" } } + }, + "required": ["sessionID", "diff"] + } + }, + "required": ["type", "properties"] + }, + "Event.session.error": { + "type": "object", + "properties": { + "type": { "type": "string", "const": "session.error" }, + "properties": { + "type": "object", + "properties": { + "sessionID": { "type": "string" }, + "error": { + "anyOf": [ + { "$ref": "#/components/schemas/ProviderAuthError" }, + { "$ref": "#/components/schemas/UnknownError" }, + { "$ref": "#/components/schemas/MessageOutputLengthError" }, + { "$ref": "#/components/schemas/MessageAbortedError" }, + { "$ref": "#/components/schemas/APIError" } + ] + } + } + } + }, + "required": ["type", "properties"] + }, + "Event": { + "anyOf": [ + { "$ref": "#/components/schemas/Event.server.connected" }, + { "$ref": "#/components/schemas/Event.global.disposed" }, + { "$ref": "#/components/schemas/Event.tui.prompt.append" }, + { "$ref": "#/components/schemas/Event.tui.command.execute" }, + { "$ref": "#/components/schemas/Event.tui.toast.show" }, + { "$ref": "#/components/schemas/Event.tui.session.select" }, + { "$ref": "#/components/schemas/Event.installation.updated" }, + { "$ref": "#/components/schemas/Event.installation.update-available" }, + { "$ref": "#/components/schemas/Event.project.updated" }, + { "$ref": "#/components/schemas/Event.server.instance.disposed" }, + { "$ref": "#/components/schemas/Event.file.edited" }, + { "$ref": "#/components/schemas/Event.worktree.ready" }, + { "$ref": "#/components/schemas/Event.worktree.failed" }, + { "$ref": "#/components/schemas/Event.lsp.client.diagnostics" }, + { "$ref": "#/components/schemas/Event.permission.asked" }, + { "$ref": "#/components/schemas/Event.permission.replied" }, + { "$ref": "#/components/schemas/Event.session.status" }, + { "$ref": "#/components/schemas/Event.session.idle" }, + { "$ref": "#/components/schemas/Event.question.asked" }, + { "$ref": "#/components/schemas/Event.question.replied" }, + { "$ref": "#/components/schemas/Event.question.rejected" }, + { "$ref": "#/components/schemas/Event.todo.updated" }, + { "$ref": "#/components/schemas/Event.pty.created" }, + { "$ref": "#/components/schemas/Event.pty.updated" }, + { "$ref": "#/components/schemas/Event.pty.exited" }, + { "$ref": "#/components/schemas/Event.pty.deleted" }, + { "$ref": "#/components/schemas/Event.file.watcher.updated" }, + { "$ref": "#/components/schemas/Event.mcp.tools.changed" }, + { "$ref": "#/components/schemas/Event.mcp.browser.open.failed" }, + { "$ref": "#/components/schemas/Event.lsp.updated" }, + { "$ref": "#/components/schemas/Event.vcs.branch.updated" }, + { "$ref": "#/components/schemas/Event.command.executed" }, + { "$ref": "#/components/schemas/Event.message.updated" }, + { "$ref": "#/components/schemas/Event.message.removed" }, + { "$ref": "#/components/schemas/Event.message.part.updated" }, + { "$ref": "#/components/schemas/Event.message.part.removed" }, + { "$ref": "#/components/schemas/Event.session.compacted" }, + { "$ref": "#/components/schemas/Event.session.created" }, + { "$ref": "#/components/schemas/Event.session.updated" }, + { "$ref": "#/components/schemas/Event.session.deleted" }, + { "$ref": "#/components/schemas/Event.session.diff" }, + { "$ref": "#/components/schemas/Event.session.error" } + ] + }, + "GlobalEvent": { + "type": "object", + "properties": { "directory": { "type": "string" }, "payload": { "$ref": "#/components/schemas/Event" } }, + "required": ["directory", "payload"] + }, + "KeybindsConfig": { + "description": "Custom keybind configurations", + "type": "object", + "properties": { + "leader": { "description": "Leader key for keybind combinations", "default": "ctrl+x", "type": "string" }, + "app_exit": { "description": "Exit the application", "default": "ctrl+c,ctrl+d,q", "type": "string" }, + "editor_open": { "description": "Open external editor", "default": "e", "type": "string" }, + "theme_list": { "description": "List available themes", "default": "t", "type": "string" }, + "sidebar_toggle": { "description": "Toggle sidebar", "default": "b", "type": "string" }, + "scrollbar_toggle": { "description": "Toggle session scrollbar", "default": "none", "type": "string" }, + "username_toggle": { "description": "Toggle username visibility", "default": "none", "type": "string" }, + "status_view": { "description": "View status", "default": "s", "type": "string" }, + "session_export": { "description": "Export session to editor", "default": "x", "type": "string" }, + "session_new": { "description": "Create a new session", "default": "n", "type": "string" }, + "session_list": { "description": "List all sessions", "default": "l", "type": "string" }, + "session_timeline": { "description": "Show session timeline", "default": "g", "type": "string" }, + "session_fork": { "description": "Fork session from message", "default": "none", "type": "string" }, + "session_rename": { "description": "Rename session", "default": "ctrl+r", "type": "string" }, + "session_delete": { "description": "Delete session", "default": "ctrl+d", "type": "string" }, + "stash_delete": { "description": "Delete stash entry", "default": "ctrl+d", "type": "string" }, + "model_provider_list": { + "description": "Open provider list from model dialog", + "default": "ctrl+a", + "type": "string" + }, + "model_favorite_toggle": { + "description": "Toggle model favorite status", + "default": "ctrl+f", + "type": "string" + }, + "session_share": { "description": "Share current session", "default": "none", "type": "string" }, + "session_unshare": { "description": "Unshare current session", "default": "none", "type": "string" }, + "session_interrupt": { "description": "Interrupt current session", "default": "escape", "type": "string" }, + "session_compact": { "description": "Compact the session", "default": "c", "type": "string" }, + "messages_page_up": { + "description": "Scroll messages up by one page", + "default": "pageup,ctrl+alt+b", + "type": "string" + }, + "messages_page_down": { + "description": "Scroll messages down by one page", + "default": "pagedown,ctrl+alt+f", + "type": "string" + }, + "messages_line_up": { + "description": "Scroll messages up by one line", + "default": "ctrl+alt+y", + "type": "string" + }, + "messages_line_down": { + "description": "Scroll messages down by one line", + "default": "ctrl+alt+e", + "type": "string" + }, + "messages_half_page_up": { + "description": "Scroll messages up by half page", + "default": "ctrl+alt+u", + "type": "string" + }, + "messages_half_page_down": { + "description": "Scroll messages down by half page", + "default": "ctrl+alt+d", + "type": "string" + }, + "messages_first": { "description": "Navigate to first message", "default": "ctrl+g,home", "type": "string" }, + "messages_last": { "description": "Navigate to last message", "default": "ctrl+alt+g,end", "type": "string" }, + "messages_next": { "description": "Navigate to next message", "default": "none", "type": "string" }, + "messages_previous": { "description": "Navigate to previous message", "default": "none", "type": "string" }, + "messages_last_user": { "description": "Navigate to last user message", "default": "none", "type": "string" }, + "messages_copy": { "description": "Copy message", "default": "y", "type": "string" }, + "messages_undo": { "description": "Undo message", "default": "u", "type": "string" }, + "messages_redo": { "description": "Redo message", "default": "r", "type": "string" }, + "messages_toggle_conceal": { + "description": "Toggle code block concealment in messages", + "default": "h", + "type": "string" + }, + "tool_details": { "description": "Toggle tool details visibility", "default": "none", "type": "string" }, + "model_list": { "description": "List available models", "default": "m", "type": "string" }, + "model_cycle_recent": { "description": "Next recently used model", "default": "f2", "type": "string" }, + "model_cycle_recent_reverse": { + "description": "Previous recently used model", + "default": "shift+f2", + "type": "string" + }, + "model_cycle_favorite": { "description": "Next favorite model", "default": "none", "type": "string" }, + "model_cycle_favorite_reverse": { + "description": "Previous favorite model", + "default": "none", + "type": "string" + }, + "command_list": { "description": "List available commands", "default": "ctrl+p", "type": "string" }, + "agent_list": { "description": "List agents", "default": "a", "type": "string" }, + "agent_cycle": { "description": "Next agent", "default": "tab", "type": "string" }, + "agent_cycle_reverse": { "description": "Previous agent", "default": "shift+tab", "type": "string" }, + "variant_cycle": { "description": "Cycle model variants", "default": "ctrl+t", "type": "string" }, + "input_clear": { "description": "Clear input field", "default": "ctrl+c", "type": "string" }, + "input_paste": { "description": "Paste from clipboard", "default": "ctrl+v", "type": "string" }, + "input_submit": { "description": "Submit input", "default": "return", "type": "string" }, + "input_newline": { + "description": "Insert newline in input", + "default": "shift+return,ctrl+return,alt+return,ctrl+j", + "type": "string" + }, + "input_move_left": { "description": "Move cursor left in input", "default": "left,ctrl+b", "type": "string" }, + "input_move_right": { + "description": "Move cursor right in input", + "default": "right,ctrl+f", + "type": "string" + }, + "input_move_up": { "description": "Move cursor up in input", "default": "up", "type": "string" }, + "input_move_down": { "description": "Move cursor down in input", "default": "down", "type": "string" }, + "input_select_left": { "description": "Select left in input", "default": "shift+left", "type": "string" }, + "input_select_right": { "description": "Select right in input", "default": "shift+right", "type": "string" }, + "input_select_up": { "description": "Select up in input", "default": "shift+up", "type": "string" }, + "input_select_down": { "description": "Select down in input", "default": "shift+down", "type": "string" }, + "input_line_home": { "description": "Move to start of line in input", "default": "ctrl+a", "type": "string" }, + "input_line_end": { "description": "Move to end of line in input", "default": "ctrl+e", "type": "string" }, + "input_select_line_home": { + "description": "Select to start of line in input", + "default": "ctrl+shift+a", + "type": "string" + }, + "input_select_line_end": { + "description": "Select to end of line in input", + "default": "ctrl+shift+e", + "type": "string" + }, + "input_visual_line_home": { + "description": "Move to start of visual line in input", + "default": "alt+a", + "type": "string" + }, + "input_visual_line_end": { + "description": "Move to end of visual line in input", + "default": "alt+e", + "type": "string" + }, + "input_select_visual_line_home": { + "description": "Select to start of visual line in input", + "default": "alt+shift+a", + "type": "string" + }, + "input_select_visual_line_end": { + "description": "Select to end of visual line in input", + "default": "alt+shift+e", + "type": "string" + }, + "input_buffer_home": { + "description": "Move to start of buffer in input", + "default": "home", + "type": "string" + }, + "input_buffer_end": { "description": "Move to end of buffer in input", "default": "end", "type": "string" }, + "input_select_buffer_home": { + "description": "Select to start of buffer in input", + "default": "shift+home", + "type": "string" + }, + "input_select_buffer_end": { + "description": "Select to end of buffer in input", + "default": "shift+end", + "type": "string" + }, + "input_delete_line": { "description": "Delete line in input", "default": "ctrl+shift+d", "type": "string" }, + "input_delete_to_line_end": { + "description": "Delete to end of line in input", + "default": "ctrl+k", + "type": "string" + }, + "input_delete_to_line_start": { + "description": "Delete to start of line in input", + "default": "ctrl+u", + "type": "string" + }, + "input_backspace": { + "description": "Backspace in input", + "default": "backspace,shift+backspace", + "type": "string" + }, + "input_delete": { + "description": "Delete character in input", + "default": "ctrl+d,delete,shift+delete", + "type": "string" + }, + "input_undo": { "description": "Undo in input", "default": "ctrl+-,super+z", "type": "string" }, + "input_redo": { "description": "Redo in input", "default": "ctrl+.,super+shift+z", "type": "string" }, + "input_word_forward": { + "description": "Move word forward in input", + "default": "alt+f,alt+right,ctrl+right", + "type": "string" + }, + "input_word_backward": { + "description": "Move word backward in input", + "default": "alt+b,alt+left,ctrl+left", + "type": "string" + }, + "input_select_word_forward": { + "description": "Select word forward in input", + "default": "alt+shift+f,alt+shift+right", + "type": "string" + }, + "input_select_word_backward": { + "description": "Select word backward in input", + "default": "alt+shift+b,alt+shift+left", + "type": "string" + }, + "input_delete_word_forward": { + "description": "Delete word forward in input", + "default": "alt+d,alt+delete,ctrl+delete", + "type": "string" + }, + "input_delete_word_backward": { + "description": "Delete word backward in input", + "default": "ctrl+w,ctrl+backspace,alt+backspace", + "type": "string" + }, + "history_previous": { "description": "Previous history item", "default": "up", "type": "string" }, + "history_next": { "description": "Next history item", "default": "down", "type": "string" }, + "session_child_cycle": { "description": "Next child session", "default": "right", "type": "string" }, + "session_child_cycle_reverse": { + "description": "Previous child session", + "default": "left", + "type": "string" + }, + "session_parent": { "description": "Go to parent session", "default": "up", "type": "string" }, + "terminal_suspend": { "description": "Suspend terminal", "default": "ctrl+z", "type": "string" }, + "terminal_title_toggle": { "description": "Toggle terminal title", "default": "none", "type": "string" }, + "tips_toggle": { "description": "Toggle tips on home screen", "default": "h", "type": "string" } + }, + "additionalProperties": false + }, + "LogLevel": { "description": "Log level", "type": "string", "enum": ["DEBUG", "INFO", "WARN", "ERROR"] }, + "ServerConfig": { + "description": "Server configuration for opencode serve and web commands", + "type": "object", + "properties": { + "port": { + "description": "Port to listen on", + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + "hostname": { "description": "Hostname to listen on", "type": "string" }, + "mdns": { "description": "Enable mDNS service discovery", "type": "boolean" }, + "cors": { + "description": "Additional domains to allow for CORS", + "type": "array", + "items": { "type": "string" } + } + }, + "additionalProperties": false + }, + "PermissionActionConfig": { "type": "string", "enum": ["ask", "allow", "deny"] }, + "PermissionObjectConfig": { + "type": "object", + "propertyNames": { "type": "string" }, + "additionalProperties": { "$ref": "#/components/schemas/PermissionActionConfig" } + }, + "PermissionRuleConfig": { + "anyOf": [ + { "$ref": "#/components/schemas/PermissionActionConfig" }, + { "$ref": "#/components/schemas/PermissionObjectConfig" } + ] + }, + "PermissionConfig": { + "anyOf": [ + { + "type": "object", + "properties": { + "__originalKeys": { "type": "array", "items": { "type": "string" } }, + "read": { "$ref": "#/components/schemas/PermissionRuleConfig" }, + "edit": { "$ref": "#/components/schemas/PermissionRuleConfig" }, + "glob": { "$ref": "#/components/schemas/PermissionRuleConfig" }, + "grep": { "$ref": "#/components/schemas/PermissionRuleConfig" }, + "list": { "$ref": "#/components/schemas/PermissionRuleConfig" }, + "bash": { "$ref": "#/components/schemas/PermissionRuleConfig" }, + "task": { "$ref": "#/components/schemas/PermissionRuleConfig" }, + "external_directory": { "$ref": "#/components/schemas/PermissionRuleConfig" }, + "todowrite": { "$ref": "#/components/schemas/PermissionActionConfig" }, + "todoread": { "$ref": "#/components/schemas/PermissionActionConfig" }, + "question": { "$ref": "#/components/schemas/PermissionActionConfig" }, + "webfetch": { "$ref": "#/components/schemas/PermissionActionConfig" }, + "websearch": { "$ref": "#/components/schemas/PermissionActionConfig" }, + "codesearch": { "$ref": "#/components/schemas/PermissionActionConfig" }, + "lsp": { "$ref": "#/components/schemas/PermissionRuleConfig" }, + "doom_loop": { "$ref": "#/components/schemas/PermissionActionConfig" }, + "skill": { "$ref": "#/components/schemas/PermissionRuleConfig" } + }, + "additionalProperties": { "$ref": "#/components/schemas/PermissionRuleConfig" } + }, + { "$ref": "#/components/schemas/PermissionActionConfig" } + ] + }, + "AgentConfig": { + "type": "object", + "properties": { + "model": { "type": "string" }, + "temperature": { "type": "number" }, + "top_p": { "type": "number" }, + "prompt": { "type": "string" }, + "tools": { + "description": "@deprecated Use 'permission' field instead", + "type": "object", + "propertyNames": { "type": "string" }, + "additionalProperties": { "type": "boolean" } + }, + "disable": { "type": "boolean" }, + "description": { "description": "Description of when to use the agent", "type": "string" }, + "mode": { "type": "string", "enum": ["subagent", "primary", "all"] }, + "hidden": { + "description": "Hide this subagent from the @ autocomplete menu (default: false, only applies to mode: subagent)", + "type": "boolean" + }, + "options": { "type": "object", "propertyNames": { "type": "string" }, "additionalProperties": {} }, + "color": { + "description": "Hex color code for the agent (e.g., #FF5733)", + "type": "string", + "pattern": "^#[0-9a-fA-F]{6}$" + }, + "steps": { + "description": "Maximum number of agentic iterations before forcing text-only response", + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + "maxSteps": { + "description": "@deprecated Use 'steps' field instead.", + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + "permission": { "$ref": "#/components/schemas/PermissionConfig" } + }, + "additionalProperties": {} + }, + "ProviderConfig": { + "type": "object", + "properties": { + "api": { "type": "string" }, + "name": { "type": "string" }, + "env": { "type": "array", "items": { "type": "string" } }, + "id": { "type": "string" }, + "npm": { "type": "string" }, + "models": { + "type": "object", + "propertyNames": { "type": "string" }, + "additionalProperties": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "name": { "type": "string" }, + "family": { "type": "string" }, + "release_date": { "type": "string" }, + "attachment": { "type": "boolean" }, + "reasoning": { "type": "boolean" }, + "temperature": { "type": "boolean" }, + "tool_call": { "type": "boolean" }, + "interleaved": { + "anyOf": [ + { "type": "boolean", "const": true }, + { + "type": "object", + "properties": { + "field": { "type": "string", "enum": ["reasoning_content", "reasoning_details"] } + }, + "required": ["field"], + "additionalProperties": false + } + ] + }, + "cost": { + "type": "object", + "properties": { + "input": { "type": "number" }, + "output": { "type": "number" }, + "cache_read": { "type": "number" }, + "cache_write": { "type": "number" }, + "context_over_200k": { + "type": "object", + "properties": { + "input": { "type": "number" }, + "output": { "type": "number" }, + "cache_read": { "type": "number" }, + "cache_write": { "type": "number" } + }, + "required": ["input", "output"] + } + }, + "required": ["input", "output"] + }, + "limit": { + "type": "object", + "properties": { + "context": { "type": "number" }, + "input": { "type": "number" }, + "output": { "type": "number" } + }, + "required": ["context", "output"] + }, + "modalities": { + "type": "object", + "properties": { + "input": { + "type": "array", + "items": { "type": "string", "enum": ["text", "audio", "image", "video", "pdf"] } + }, + "output": { + "type": "array", + "items": { "type": "string", "enum": ["text", "audio", "image", "video", "pdf"] } + } + }, + "required": ["input", "output"] + }, + "experimental": { "type": "boolean" }, + "status": { "type": "string", "enum": ["alpha", "beta", "deprecated"] }, + "options": { "type": "object", "propertyNames": { "type": "string" }, "additionalProperties": {} }, + "headers": { + "type": "object", + "propertyNames": { "type": "string" }, + "additionalProperties": { "type": "string" } + }, + "provider": { "type": "object", "properties": { "npm": { "type": "string" } }, "required": ["npm"] }, + "variants": { + "description": "Variant-specific configuration", + "type": "object", + "propertyNames": { "type": "string" }, + "additionalProperties": { + "type": "object", + "properties": { + "disabled": { "description": "Disable this variant for the model", "type": "boolean" } + }, + "additionalProperties": {} + } + } + } + } + }, + "whitelist": { "type": "array", "items": { "type": "string" } }, + "blacklist": { "type": "array", "items": { "type": "string" } }, + "options": { + "type": "object", + "properties": { + "apiKey": { "type": "string" }, + "baseURL": { "type": "string" }, + "enterpriseUrl": { "description": "GitHub Enterprise URL for copilot authentication", "type": "string" }, + "setCacheKey": { + "description": "Enable promptCacheKey for this provider (default false)", + "type": "boolean" + }, + "timeout": { + "description": "Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout.", + "anyOf": [ + { + "description": "Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout.", + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + { "description": "Disable timeout for this provider entirely.", "type": "boolean", "const": false } + ] + } + }, + "additionalProperties": {} + } + }, + "additionalProperties": false + }, + "McpLocalConfig": { + "type": "object", + "properties": { + "type": { "description": "Type of MCP server connection", "type": "string", "const": "local" }, + "command": { + "description": "Command and arguments to run the MCP server", + "type": "array", + "items": { "type": "string" } + }, + "environment": { + "description": "Environment variables to set when running the MCP server", + "type": "object", + "propertyNames": { "type": "string" }, + "additionalProperties": { "type": "string" } + }, + "enabled": { "description": "Enable or disable the MCP server on startup", "type": "boolean" }, + "timeout": { + "description": "Timeout in ms for MCP server requests. Defaults to 5000 (5 seconds) if not specified.", + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + } + }, + "required": ["type", "command"], + "additionalProperties": false + }, + "McpOAuthConfig": { + "type": "object", + "properties": { + "clientId": { + "description": "OAuth client ID. If not provided, dynamic client registration (RFC 7591) will be attempted.", + "type": "string" + }, + "clientSecret": { + "description": "OAuth client secret (if required by the authorization server)", + "type": "string" + }, + "scope": { "description": "OAuth scopes to request during authorization", "type": "string" } + }, + "additionalProperties": false + }, + "McpRemoteConfig": { + "type": "object", + "properties": { + "type": { "description": "Type of MCP server connection", "type": "string", "const": "remote" }, + "url": { "description": "URL of the remote MCP server", "type": "string" }, + "enabled": { "description": "Enable or disable the MCP server on startup", "type": "boolean" }, + "headers": { + "description": "Headers to send with the request", + "type": "object", + "propertyNames": { "type": "string" }, + "additionalProperties": { "type": "string" } + }, + "oauth": { + "description": "OAuth authentication configuration for the MCP server. Set to false to disable OAuth auto-detection.", + "anyOf": [{ "$ref": "#/components/schemas/McpOAuthConfig" }, { "type": "boolean", "const": false }] + }, + "timeout": { + "description": "Timeout in ms for MCP server requests. Defaults to 5000 (5 seconds) if not specified.", + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + } + }, + "required": ["type", "url"], + "additionalProperties": false + }, + "LayoutConfig": { + "description": "@deprecated Always uses stretch layout.", + "type": "string", + "enum": ["auto", "stretch"] + }, + "Config": { + "type": "object", + "properties": { + "$schema": { "description": "JSON schema reference for configuration validation", "type": "string" }, + "theme": { "description": "Theme name to use for the interface", "type": "string" }, + "keybinds": { "$ref": "#/components/schemas/KeybindsConfig" }, + "logLevel": { "$ref": "#/components/schemas/LogLevel" }, + "tui": { + "description": "TUI specific settings", + "type": "object", + "properties": { + "scroll_speed": { "description": "TUI scroll speed", "type": "number", "minimum": 0.001 }, + "scroll_acceleration": { + "description": "Scroll acceleration settings", + "type": "object", + "properties": { "enabled": { "description": "Enable scroll acceleration", "type": "boolean" } }, + "required": ["enabled"] + }, + "diff_style": { + "description": "Control diff rendering style: 'auto' adapts to terminal width, 'stacked' always shows single column", + "type": "string", + "enum": ["auto", "stacked"] + } + } + }, + "server": { "$ref": "#/components/schemas/ServerConfig" }, + "command": { + "description": "Command configuration, see https://opencode.ai/docs/commands", + "type": "object", + "propertyNames": { "type": "string" }, + "additionalProperties": { + "type": "object", + "properties": { + "template": { "type": "string" }, + "description": { "type": "string" }, + "agent": { "type": "string" }, + "model": { "type": "string" }, + "subtask": { "type": "boolean" } + }, + "required": ["template"] + } + }, + "skills": { + "description": "Additional skill folder paths", + "type": "object", + "properties": { + "paths": { + "description": "Additional paths to skill folders", + "type": "array", + "items": { "type": "string" } + } + } + }, + "watcher": { + "type": "object", + "properties": { "ignore": { "type": "array", "items": { "type": "string" } } } + }, + "plugin": { "type": "array", "items": { "type": "string" } }, + "snapshot": { "type": "boolean" }, + "share": { + "description": "Control sharing behavior:'manual' allows manual sharing via commands, 'auto' enables automatic sharing, 'disabled' disables all sharing", + "type": "string", + "enum": ["manual", "auto", "disabled"] + }, + "autoshare": { + "description": "@deprecated Use 'share' field instead. Share newly created sessions automatically", + "type": "boolean" + }, + "autoupdate": { + "description": "Automatically update to the latest version. Set to true to auto-update, false to disable, or 'notify' to show update notifications", + "anyOf": [{ "type": "boolean" }, { "type": "string", "const": "notify" }] + }, + "disabled_providers": { + "description": "Disable providers that are loaded automatically", + "type": "array", + "items": { "type": "string" } + }, + "enabled_providers": { + "description": "When set, ONLY these providers will be enabled. All other providers will be ignored", + "type": "array", + "items": { "type": "string" } + }, + "model": { + "description": "Model to use in the format of provider/model, eg anthropic/claude-2", + "type": "string" + }, + "small_model": { + "description": "Small model to use for tasks like title generation in the format of provider/model", + "type": "string" + }, + "default_agent": { + "description": "Default agent to use when none is specified. Must be a primary agent. Falls back to 'build' if not set or if the specified agent is invalid.", + "type": "string" + }, + "username": { + "description": "Custom username to display in conversations instead of system username", + "type": "string" + }, + "mode": { + "description": "@deprecated Use `agent` field instead.", + "type": "object", + "properties": { + "build": { "$ref": "#/components/schemas/AgentConfig" }, + "plan": { "$ref": "#/components/schemas/AgentConfig" } + }, + "additionalProperties": { "$ref": "#/components/schemas/AgentConfig" } + }, + "agent": { + "description": "Agent configuration, see https://opencode.ai/docs/agents", + "type": "object", + "properties": { + "plan": { "$ref": "#/components/schemas/AgentConfig" }, + "build": { "$ref": "#/components/schemas/AgentConfig" }, + "general": { "$ref": "#/components/schemas/AgentConfig" }, + "explore": { "$ref": "#/components/schemas/AgentConfig" }, + "title": { "$ref": "#/components/schemas/AgentConfig" }, + "summary": { "$ref": "#/components/schemas/AgentConfig" }, + "compaction": { "$ref": "#/components/schemas/AgentConfig" } + }, + "additionalProperties": { "$ref": "#/components/schemas/AgentConfig" } + }, + "provider": { + "description": "Custom provider configurations and model overrides", + "type": "object", + "propertyNames": { "type": "string" }, + "additionalProperties": { "$ref": "#/components/schemas/ProviderConfig" } + }, + "mcp": { + "description": "MCP (Model Context Protocol) server configurations", + "type": "object", + "propertyNames": { "type": "string" }, + "additionalProperties": { + "anyOf": [ + { + "anyOf": [ + { "$ref": "#/components/schemas/McpLocalConfig" }, + { "$ref": "#/components/schemas/McpRemoteConfig" } + ] + }, + { + "type": "object", + "properties": { "enabled": { "type": "boolean" } }, + "required": ["enabled"], + "additionalProperties": false + } + ] + } + }, + "formatter": { + "anyOf": [ + { "type": "boolean", "const": false }, + { + "type": "object", + "propertyNames": { "type": "string" }, + "additionalProperties": { + "type": "object", + "properties": { + "disabled": { "type": "boolean" }, + "command": { "type": "array", "items": { "type": "string" } }, + "environment": { + "type": "object", + "propertyNames": { "type": "string" }, + "additionalProperties": { "type": "string" } + }, + "extensions": { "type": "array", "items": { "type": "string" } } + } + } + } + ] + }, + "lsp": { + "anyOf": [ + { "type": "boolean", "const": false }, + { + "type": "object", + "propertyNames": { "type": "string" }, + "additionalProperties": { + "anyOf": [ + { + "type": "object", + "properties": { "disabled": { "type": "boolean", "const": true } }, + "required": ["disabled"] + }, + { + "type": "object", + "properties": { + "command": { "type": "array", "items": { "type": "string" } }, + "extensions": { "type": "array", "items": { "type": "string" } }, + "disabled": { "type": "boolean" }, + "env": { + "type": "object", + "propertyNames": { "type": "string" }, + "additionalProperties": { "type": "string" } + }, + "initialization": { + "type": "object", + "propertyNames": { "type": "string" }, + "additionalProperties": {} + } + }, + "required": ["command"] + } + ] + } + } + ] + }, + "instructions": { + "description": "Additional instruction files or patterns to include", + "type": "array", + "items": { "type": "string" } + }, + "layout": { "$ref": "#/components/schemas/LayoutConfig" }, + "permission": { "$ref": "#/components/schemas/PermissionConfig" }, + "tools": { + "type": "object", + "propertyNames": { "type": "string" }, + "additionalProperties": { "type": "boolean" } + }, + "enterprise": { + "type": "object", + "properties": { "url": { "description": "Enterprise URL", "type": "string" } } + }, + "compaction": { + "type": "object", + "properties": { + "auto": { + "description": "Enable automatic compaction when context is full (default: true)", + "type": "boolean" + }, + "prune": { "description": "Enable pruning of old tool outputs (default: true)", "type": "boolean" } + } + }, + "experimental": { + "type": "object", + "properties": { + "disable_paste_summary": { "type": "boolean" }, + "batch_tool": { "description": "Enable the batch tool", "type": "boolean" }, + "openTelemetry": { + "description": "Enable OpenTelemetry spans for AI SDK calls (using the 'experimental_telemetry' flag)", + "type": "boolean" + }, + "primary_tools": { + "description": "Tools that should only be available to primary agents.", + "type": "array", + "items": { "type": "string" } + }, + "continue_loop_on_deny": { + "description": "Continue the agent loop when a tool call is denied", + "type": "boolean" + }, + "mcp_timeout": { + "description": "Timeout in milliseconds for model context protocol (MCP) requests", + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + } + } + } + }, + "additionalProperties": false + }, + "BadRequestError": { + "type": "object", + "properties": { + "data": {}, + "errors": { + "type": "array", + "items": { "type": "object", "propertyNames": { "type": "string" }, "additionalProperties": {} } + }, + "success": { "type": "boolean", "const": false } + }, + "required": ["data", "errors", "success"] + }, + "OAuth": { + "type": "object", + "properties": { + "type": { "type": "string", "const": "oauth" }, + "refresh": { "type": "string" }, + "access": { "type": "string" }, + "expires": { "type": "number" }, + "accountId": { "type": "string" }, + "enterpriseUrl": { "type": "string" } + }, + "required": ["type", "refresh", "access", "expires"] + }, + "ApiAuth": { + "type": "object", + "properties": { "type": { "type": "string", "const": "api" }, "key": { "type": "string" } }, + "required": ["type", "key"] + }, + "WellKnownAuth": { + "type": "object", + "properties": { + "type": { "type": "string", "const": "wellknown" }, + "key": { "type": "string" }, + "token": { "type": "string" } + }, + "required": ["type", "key", "token"] + }, + "Auth": { + "anyOf": [ + { "$ref": "#/components/schemas/OAuth" }, + { "$ref": "#/components/schemas/ApiAuth" }, + { "$ref": "#/components/schemas/WellKnownAuth" } + ] + }, + "NotFoundError": { + "type": "object", + "properties": { + "name": { "type": "string", "const": "NotFoundError" }, + "data": { "type": "object", "properties": { "message": { "type": "string" } }, "required": ["message"] } + }, + "required": ["name", "data"] + }, + "Model": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "providerID": { "type": "string" }, + "api": { + "type": "object", + "properties": { "id": { "type": "string" }, "url": { "type": "string" }, "npm": { "type": "string" } }, + "required": ["id", "url", "npm"] + }, + "name": { "type": "string" }, + "family": { "type": "string" }, + "capabilities": { + "type": "object", + "properties": { + "temperature": { "type": "boolean" }, + "reasoning": { "type": "boolean" }, + "attachment": { "type": "boolean" }, + "toolcall": { "type": "boolean" }, + "input": { + "type": "object", + "properties": { + "text": { "type": "boolean" }, + "audio": { "type": "boolean" }, + "image": { "type": "boolean" }, + "video": { "type": "boolean" }, + "pdf": { "type": "boolean" } + }, + "required": ["text", "audio", "image", "video", "pdf"] + }, + "output": { + "type": "object", + "properties": { + "text": { "type": "boolean" }, + "audio": { "type": "boolean" }, + "image": { "type": "boolean" }, + "video": { "type": "boolean" }, + "pdf": { "type": "boolean" } + }, + "required": ["text", "audio", "image", "video", "pdf"] + }, + "interleaved": { + "anyOf": [ + { "type": "boolean" }, + { + "type": "object", + "properties": { "field": { "type": "string", "enum": ["reasoning_content", "reasoning_details"] } }, + "required": ["field"] + } + ] + } + }, + "required": ["temperature", "reasoning", "attachment", "toolcall", "input", "output", "interleaved"] + }, + "cost": { + "type": "object", + "properties": { + "input": { "type": "number" }, + "output": { "type": "number" }, + "cache": { + "type": "object", + "properties": { "read": { "type": "number" }, "write": { "type": "number" } }, + "required": ["read", "write"] + }, + "experimentalOver200K": { + "type": "object", + "properties": { + "input": { "type": "number" }, + "output": { "type": "number" }, + "cache": { + "type": "object", + "properties": { "read": { "type": "number" }, "write": { "type": "number" } }, + "required": ["read", "write"] + } + }, + "required": ["input", "output", "cache"] + } + }, + "required": ["input", "output", "cache"] + }, + "limit": { + "type": "object", + "properties": { + "context": { "type": "number" }, + "input": { "type": "number" }, + "output": { "type": "number" } + }, + "required": ["context", "output"] + }, + "status": { "type": "string", "enum": ["alpha", "beta", "deprecated", "active"] }, + "options": { "type": "object", "propertyNames": { "type": "string" }, "additionalProperties": {} }, + "headers": { + "type": "object", + "propertyNames": { "type": "string" }, + "additionalProperties": { "type": "string" } + }, + "release_date": { "type": "string" }, + "variants": { + "type": "object", + "propertyNames": { "type": "string" }, + "additionalProperties": { + "type": "object", + "propertyNames": { "type": "string" }, + "additionalProperties": {} + } + } + }, + "required": [ + "id", + "providerID", + "api", + "name", + "capabilities", + "cost", + "limit", + "status", + "options", + "headers", + "release_date" + ] + }, + "Provider": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "name": { "type": "string" }, + "source": { "type": "string", "enum": ["env", "config", "custom", "api"] }, + "env": { "type": "array", "items": { "type": "string" } }, + "key": { "type": "string" }, + "options": { "type": "object", "propertyNames": { "type": "string" }, "additionalProperties": {} }, + "models": { + "type": "object", + "propertyNames": { "type": "string" }, + "additionalProperties": { "$ref": "#/components/schemas/Model" } + } + }, + "required": ["id", "name", "source", "env", "options", "models"] + }, + "ToolIDs": { "type": "array", "items": { "type": "string" } }, + "ToolListItem": { + "type": "object", + "properties": { "id": { "type": "string" }, "description": { "type": "string" }, "parameters": {} }, + "required": ["id", "description", "parameters"] + }, + "ToolList": { "type": "array", "items": { "$ref": "#/components/schemas/ToolListItem" } }, + "Worktree": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "branch": { "type": "string" }, + "directory": { "type": "string" } + }, + "required": ["name", "branch", "directory"] + }, + "WorktreeCreateInput": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "startCommand": { + "description": "Additional startup script to run after the project's start command", + "type": "string" + } + } + }, + "WorktreeRemoveInput": { + "type": "object", + "properties": { "directory": { "type": "string" } }, + "required": ["directory"] + }, + "WorktreeResetInput": { + "type": "object", + "properties": { "directory": { "type": "string" } }, + "required": ["directory"] + }, + "McpResource": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "uri": { "type": "string" }, + "description": { "type": "string" }, + "mimeType": { "type": "string" }, + "client": { "type": "string" } + }, + "required": ["name", "uri", "client"] + }, + "TextPartInput": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "type": { "type": "string", "const": "text" }, + "text": { "type": "string" }, + "synthetic": { "type": "boolean" }, + "ignored": { "type": "boolean" }, + "time": { + "type": "object", + "properties": { "start": { "type": "number" }, "end": { "type": "number" } }, + "required": ["start"] + }, + "metadata": { "type": "object", "propertyNames": { "type": "string" }, "additionalProperties": {} } + }, + "required": ["type", "text"] + }, + "FilePartInput": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "type": { "type": "string", "const": "file" }, + "mime": { "type": "string" }, + "filename": { "type": "string" }, + "url": { "type": "string" }, + "source": { "$ref": "#/components/schemas/FilePartSource" } + }, + "required": ["type", "mime", "url"] + }, + "AgentPartInput": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "type": { "type": "string", "const": "agent" }, + "name": { "type": "string" }, + "source": { + "type": "object", + "properties": { + "value": { "type": "string" }, + "start": { "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, + "end": { "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 } + }, + "required": ["value", "start", "end"] + } + }, + "required": ["type", "name"] + }, + "SubtaskPartInput": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "type": { "type": "string", "const": "subtask" }, + "prompt": { "type": "string" }, + "description": { "type": "string" }, + "agent": { "type": "string" }, + "model": { + "type": "object", + "properties": { "providerID": { "type": "string" }, "modelID": { "type": "string" } }, + "required": ["providerID", "modelID"] + }, + "command": { "type": "string" } + }, + "required": ["type", "prompt", "description", "agent"] + }, + "ProviderAuthMethod": { + "type": "object", + "properties": { + "type": { + "anyOf": [ + { "type": "string", "const": "oauth" }, + { "type": "string", "const": "api" } + ] + }, + "label": { "type": "string" } + }, + "required": ["type", "label"] + }, + "ProviderAuthAuthorization": { + "type": "object", + "properties": { + "url": { "type": "string" }, + "method": { + "anyOf": [ + { "type": "string", "const": "auto" }, + { "type": "string", "const": "code" } + ] + }, + "instructions": { "type": "string" } + }, + "required": ["url", "method", "instructions"] + }, + "Symbol": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "kind": { "type": "number" }, + "location": { + "type": "object", + "properties": { "uri": { "type": "string" }, "range": { "$ref": "#/components/schemas/Range" } }, + "required": ["uri", "range"] + } + }, + "required": ["name", "kind", "location"] + }, + "FileNode": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "path": { "type": "string" }, + "absolute": { "type": "string" }, + "type": { "type": "string", "enum": ["file", "directory"] }, + "ignored": { "type": "boolean" } + }, + "required": ["name", "path", "absolute", "type", "ignored"] + }, + "FileContent": { + "type": "object", + "properties": { + "type": { "type": "string", "const": "text" }, + "content": { "type": "string" }, + "diff": { "type": "string" }, + "patch": { + "type": "object", + "properties": { + "oldFileName": { "type": "string" }, + "newFileName": { "type": "string" }, + "oldHeader": { "type": "string" }, + "newHeader": { "type": "string" }, + "hunks": { + "type": "array", + "items": { + "type": "object", + "properties": { + "oldStart": { "type": "number" }, + "oldLines": { "type": "number" }, + "newStart": { "type": "number" }, + "newLines": { "type": "number" }, + "lines": { "type": "array", "items": { "type": "string" } } + }, + "required": ["oldStart", "oldLines", "newStart", "newLines", "lines"] + } + }, + "index": { "type": "string" } + }, + "required": ["oldFileName", "newFileName", "hunks"] + }, + "encoding": { "type": "string", "const": "base64" }, + "mimeType": { "type": "string" } + }, + "required": ["type", "content"] + }, + "File": { + "type": "object", + "properties": { + "path": { "type": "string" }, + "added": { "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, + "removed": { "type": "integer", "minimum": -9007199254740991, "maximum": 9007199254740991 }, + "status": { "type": "string", "enum": ["added", "deleted", "modified"] } + }, + "required": ["path", "added", "removed", "status"] + }, + "MCPStatusConnected": { + "type": "object", + "properties": { "status": { "type": "string", "const": "connected" } }, + "required": ["status"] + }, + "MCPStatusDisabled": { + "type": "object", + "properties": { "status": { "type": "string", "const": "disabled" } }, + "required": ["status"] + }, + "MCPStatusFailed": { + "type": "object", + "properties": { "status": { "type": "string", "const": "failed" }, "error": { "type": "string" } }, + "required": ["status", "error"] + }, + "MCPStatusNeedsAuth": { + "type": "object", + "properties": { "status": { "type": "string", "const": "needs_auth" } }, + "required": ["status"] + }, + "MCPStatusNeedsClientRegistration": { + "type": "object", + "properties": { + "status": { "type": "string", "const": "needs_client_registration" }, + "error": { "type": "string" } + }, + "required": ["status", "error"] + }, + "MCPStatus": { + "anyOf": [ + { "$ref": "#/components/schemas/MCPStatusConnected" }, + { "$ref": "#/components/schemas/MCPStatusDisabled" }, + { "$ref": "#/components/schemas/MCPStatusFailed" }, + { "$ref": "#/components/schemas/MCPStatusNeedsAuth" }, + { "$ref": "#/components/schemas/MCPStatusNeedsClientRegistration" } + ] + }, + "Path": { + "type": "object", + "properties": { + "home": { "type": "string" }, + "state": { "type": "string" }, + "config": { "type": "string" }, + "worktree": { "type": "string" }, + "directory": { "type": "string" } + }, + "required": ["home", "state", "config", "worktree", "directory"] + }, + "VcsInfo": { "type": "object", "properties": { "branch": { "type": "string" } }, "required": ["branch"] }, + "Command": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "description": { "type": "string" }, + "agent": { "type": "string" }, + "model": { "type": "string" }, + "source": { "type": "string", "enum": ["command", "mcp", "skill"] }, + "template": { "anyOf": [{ "type": "string" }, { "type": "string" }] }, + "subtask": { "type": "boolean" }, + "hints": { "type": "array", "items": { "type": "string" } } + }, + "required": ["name", "template", "hints"] + }, + "Agent": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "description": { "type": "string" }, + "mode": { "type": "string", "enum": ["subagent", "primary", "all"] }, + "native": { "type": "boolean" }, + "hidden": { "type": "boolean" }, + "topP": { "type": "number" }, + "temperature": { "type": "number" }, + "color": { "type": "string" }, + "permission": { "$ref": "#/components/schemas/PermissionRuleset" }, + "model": { + "type": "object", + "properties": { "modelID": { "type": "string" }, "providerID": { "type": "string" } }, + "required": ["modelID", "providerID"] + }, + "prompt": { "type": "string" }, + "options": { "type": "object", "propertyNames": { "type": "string" }, "additionalProperties": {} }, + "steps": { "type": "integer", "exclusiveMinimum": 0, "maximum": 9007199254740991 } + }, + "required": ["name", "mode", "permission", "options"] + }, + "LSPStatus": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "name": { "type": "string" }, + "root": { "type": "string" }, + "status": { + "anyOf": [ + { "type": "string", "const": "connected" }, + { "type": "string", "const": "error" } + ] + } + }, + "required": ["id", "name", "root", "status"] + }, + "FormatterStatus": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "extensions": { "type": "array", "items": { "type": "string" } }, + "enabled": { "type": "boolean" } + }, + "required": ["name", "extensions", "enabled"] + } + } + } +} diff --git a/openapi_formatted.json b/openapi_formatted.json index d559f9c6..80bba789 100644 --- a/openapi_formatted.json +++ b/openapi_formatted.json @@ -1,11277 +1,10277 @@ { - "openapi": "3.1.1", - "info": { - "title": "opencode", - "description": "opencode api", - "version": "0.0.3" - }, - "paths": { - "/global/health": { - "get": { - "operationId": "global.health", - "summary": "Get health", - "description": "Get health information about the OpenCode server.", - "responses": { - "200": { - "description": "Health information", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "healthy": { - "type": "boolean", - "const": true - }, - "version": { - "type": "string" - } - }, - "required": [ - "healthy", - "version" - ] - } - } - } + "openapi": "3.1.1", + "info": { + "title": "opencode", + "description": "opencode api", + "version": "0.0.3" + }, + "paths": { + "/global/health": { + "get": { + "operationId": "global.health", + "summary": "Get health", + "description": "Get health information about the OpenCode server.", + "responses": { + "200": { + "description": "Health information", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "healthy": { + "type": "boolean", + "const": true + }, + "version": { + "type": "string" } + }, + "required": ["healthy", "version"] } + } } - }, - "/global/event": { - "get": { - "operationId": "global.event", - "summary": "Get global events", - "description": "Subscribe to global events from the OpenCode system using server-sent events.", - "responses": { - "200": { - "description": "Event stream", - "content": { - "text/event-stream": { - "schema": { - "$ref": "#/components/schemas/GlobalEvent" - } - } - } - } + } + } + } + }, + "/global/event": { + "get": { + "operationId": "global.event", + "summary": "Get global events", + "description": "Subscribe to global events from the OpenCode system using server-sent events.", + "responses": { + "200": { + "description": "Event stream", + "content": { + "text/event-stream": { + "schema": { + "$ref": "#/components/schemas/GlobalEvent" } + } } - }, - "/global/dispose": { - "post": { - "operationId": "global.dispose", - "summary": "Dispose instance", - "description": "Clean up and dispose all OpenCode instances, releasing all resources.", - "responses": { - "200": { - "description": "Global disposed", - "content": { - "application/json": { - "schema": { - "type": "boolean" - } - } - } - } + } + } + } + }, + "/global/dispose": { + "post": { + "operationId": "global.dispose", + "summary": "Dispose instance", + "description": "Clean up and dispose all OpenCode instances, releasing all resources.", + "responses": { + "200": { + "description": "Global disposed", + "content": { + "application/json": { + "schema": { + "type": "boolean" } + } } - }, - "/project": { - "get": { - "operationId": "project.list", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - } - ], - "summary": "List all projects", - "description": "Get a list of projects that have been opened with OpenCode.", - "responses": { - "200": { - "description": "List of projects", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Project" - } - } - } - } - } + } + } + } + }, + "/project": { + "get": { + "operationId": "project.list", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + } + ], + "summary": "List all projects", + "description": "Get a list of projects that have been opened with OpenCode.", + "responses": { + "200": { + "description": "List of projects", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Project" + } } + } } - }, - "/project/current": { - "get": { - "operationId": "project.current", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - } - ], - "summary": "Get current project", - "description": "Retrieve the currently active project that OpenCode is working with.", - "responses": { - "200": { - "description": "Current project information", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Project" - } - } - } - } + } + } + } + }, + "/project/current": { + "get": { + "operationId": "project.current", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + } + ], + "summary": "Get current project", + "description": "Retrieve the currently active project that OpenCode is working with.", + "responses": { + "200": { + "description": "Current project information", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Project" } + } } - }, - "/project/{projectID}": { - "patch": { - "operationId": "project.update", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - }, - { - "in": "path", - "name": "projectID", - "schema": { - "type": "string" - }, - "required": true - } - ], - "summary": "Update project", - "description": "Update project properties such as name, icon, and commands.", - "responses": { - "200": { - "description": "Updated project information", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Project" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NotFoundError" - } - } - } - } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "icon": { - "type": "object", - "properties": { - "url": { - "type": "string" - }, - "override": { - "type": "string" - }, - "color": { - "type": "string" - } - } - }, - "commands": { - "type": "object", - "properties": { - "start": { - "description": "Startup script to run when creating a new workspace (worktree)", - "type": "string" - } - } - } - } - } - } - } + } + } + } + }, + "/project/{projectID}": { + "patch": { + "operationId": "project.update", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "projectID", + "schema": { + "type": "string" + }, + "required": true + } + ], + "summary": "Update project", + "description": "Update project properties such as name, icon, and commands.", + "responses": { + "200": { + "description": "Updated project information", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Project" } + } } - }, - "/pty": { - "get": { - "operationId": "pty.list", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - } - ], - "summary": "List PTY sessions", - "description": "Get a list of all active pseudo-terminal (PTY) sessions managed by OpenCode.", - "responses": { - "200": { - "description": "List of sessions", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Pty" - } - } - } - } - } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" } - }, - "post": { - "operationId": "pty.create", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - } - ], - "summary": "Create PTY session", - "description": "Create a new pseudo-terminal (PTY) session for running shell commands and processes.", - "responses": { - "200": { - "description": "Created session", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Pty" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "command": { - "type": "string" - }, - "args": { - "type": "array", - "items": { - "type": "string" - } - }, - "cwd": { - "type": "string" - }, - "title": { - "type": "string" - }, - "env": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": { - "type": "string" - } - } - } - } - } - } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundError" } + } } + } }, - "/pty/{ptyID}": { - "get": { - "operationId": "pty.get", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - }, - { - "in": "path", - "name": "ptyID", - "schema": { - "type": "string" - }, - "required": true + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "icon": { + "type": "object", + "properties": { + "url": { + "type": "string" + }, + "override": { + "type": "string" + }, + "color": { + "type": "string" + } } - ], - "summary": "Get PTY session", - "description": "Retrieve detailed information about a specific pseudo-terminal (PTY) session.", - "responses": { - "200": { - "description": "Session info", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Pty" - } - } - } - }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NotFoundError" - } - } - } + }, + "commands": { + "type": "object", + "properties": { + "start": { + "description": "Startup script to run when creating a new workspace (worktree)", + "type": "string" + } } + } } - }, - "put": { - "operationId": "pty.update", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - }, - { - "in": "path", - "name": "ptyID", - "schema": { - "type": "string" - }, - "required": true - } - ], - "summary": "Update PTY session", - "description": "Update properties of an existing pseudo-terminal (PTY) session.", - "responses": { - "200": { - "description": "Updated session", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Pty" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "title": { - "type": "string" - }, - "size": { - "type": "object", - "properties": { - "rows": { - "type": "number" - }, - "cols": { - "type": "number" - } - }, - "required": [ - "rows", - "cols" - ] - } - } - } - } - } + } + } + } + } + } + }, + "/pty": { + "get": { + "operationId": "pty.list", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + } + ], + "summary": "List PTY sessions", + "description": "Get a list of all active pseudo-terminal (PTY) sessions managed by OpenCode.", + "responses": { + "200": { + "description": "List of sessions", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pty" + } } - }, - "delete": { - "operationId": "pty.remove", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - }, - { - "in": "path", - "name": "ptyID", - "schema": { - "type": "string" - }, - "required": true - } - ], - "summary": "Remove PTY session", - "description": "Remove and terminate a specific pseudo-terminal (PTY) session.", - "responses": { - "200": { - "description": "Session removed", - "content": { - "application/json": { - "schema": { - "type": "boolean" - } - } - } - }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NotFoundError" - } - } - } - } + } + } + } + } + }, + "post": { + "operationId": "pty.create", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + } + ], + "summary": "Create PTY session", + "description": "Create a new pseudo-terminal (PTY) session for running shell commands and processes.", + "responses": { + "200": { + "description": "Created session", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pty" } + } } - }, - "/pty/{ptyID}/connect": { - "get": { - "operationId": "pty.connect", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - }, - { - "in": "path", - "name": "ptyID", - "schema": { - "type": "string" - }, - "required": true - } - ], - "summary": "Connect to PTY session", - "description": "Establish a WebSocket connection to interact with a pseudo-terminal (PTY) session in real-time.", - "responses": { - "200": { - "description": "Connected session", - "content": { - "application/json": { - "schema": { - "type": "boolean" - } - } - } - }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NotFoundError" - } - } - } - } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" } + } } + } }, - "/config": { - "get": { - "operationId": "config.get", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - } - ], - "summary": "Get configuration", - "description": "Retrieve the current OpenCode configuration settings and preferences.", - "responses": { - "200": { - "description": "Get config info", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Config" - } - } - } - } - } - }, - "patch": { - "operationId": "config.update", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - } - ], - "summary": "Update configuration", - "description": "Update OpenCode configuration settings and preferences.", - "responses": { - "200": { - "description": "Successfully updated config", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Config" - } - } - } + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "command": { + "type": "string" + }, + "args": { + "type": "array", + "items": { + "type": "string" + } + }, + "cwd": { + "type": "string" + }, + "title": { + "type": "string" + }, + "env": { + "type": "object", + "propertyNames": { + "type": "string" }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Config" - } - } + "additionalProperties": { + "type": "string" } + } } + } } - }, - "/config/providers": { - "get": { - "operationId": "config.providers", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - } - ], - "summary": "List config providers", - "description": "Get a list of all configured AI providers and their default models.", - "responses": { - "200": { - "description": "List of providers", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "providers": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Provider" - } - }, - "default": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": { - "type": "string" - } - } - }, - "required": [ - "providers", - "default" - ] - } - } - } - } + } + } + } + }, + "/pty/{ptyID}": { + "get": { + "operationId": "pty.get", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "ptyID", + "schema": { + "type": "string" + }, + "required": true + } + ], + "summary": "Get PTY session", + "description": "Retrieve detailed information about a specific pseudo-terminal (PTY) session.", + "responses": { + "200": { + "description": "Session info", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pty" } + } } - }, - "/experimental/tool/ids": { - "get": { - "operationId": "tool.ids", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - } - ], - "summary": "List tool IDs", - "description": "Get a list of all available tool IDs, including both built-in tools and dynamically registered tools.", - "responses": { - "200": { - "description": "Tool IDs", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ToolIDs" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundError" } + } } - }, - "/experimental/tool": { - "get": { - "operationId": "tool.list", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - }, - { - "in": "query", - "name": "provider", - "schema": { - "type": "string" - }, - "required": true - }, - { - "in": "query", - "name": "model", - "schema": { - "type": "string" - }, - "required": true - } - ], - "summary": "List tools", - "description": "Get a list of available tools with their JSON schema parameters for a specific provider and model combination.", - "responses": { - "200": { - "description": "Tools", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ToolList" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - } + } + } + }, + "put": { + "operationId": "pty.update", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "ptyID", + "schema": { + "type": "string" + }, + "required": true + } + ], + "summary": "Update PTY session", + "description": "Update properties of an existing pseudo-terminal (PTY) session.", + "responses": { + "200": { + "description": "Updated session", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pty" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" } + } } + } }, - "/experimental/worktree": { - "post": { - "operationId": "worktree.create", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - } - ], - "summary": "Create worktree", - "description": "Create a new git worktree for the current project and run any configured startup scripts.", - "responses": { - "200": { - "description": "Worktree created", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Worktree" - } - } - } + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "size": { + "type": "object", + "properties": { + "rows": { + "type": "number" + }, + "cols": { + "type": "number" + } }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/WorktreeCreateInput" - } - } - } + "required": ["rows", "cols"] + } } - }, - "get": { - "operationId": "worktree.list", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - } - ], - "summary": "List worktrees", - "description": "List all sandbox worktrees for the current project.", - "responses": { - "200": { - "description": "List of worktree directories", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "type": "string" - } - } - } - } - } + } + } + } + } + }, + "delete": { + "operationId": "pty.remove", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "ptyID", + "schema": { + "type": "string" + }, + "required": true + } + ], + "summary": "Remove PTY session", + "description": "Remove and terminate a specific pseudo-terminal (PTY) session.", + "responses": { + "200": { + "description": "Session removed", + "content": { + "application/json": { + "schema": { + "type": "boolean" } - }, - "delete": { - "operationId": "worktree.remove", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - } - ], - "summary": "Remove worktree", - "description": "Remove a git worktree and delete its branch.", - "responses": { - "200": { - "description": "Worktree removed", - "content": { - "application/json": { - "schema": { - "type": "boolean" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/WorktreeRemoveInput" - } - } - } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundError" + } + } + } + } + } + } + }, + "/pty/{ptyID}/connect": { + "get": { + "operationId": "pty.connect", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "ptyID", + "schema": { + "type": "string" + }, + "required": true + } + ], + "summary": "Connect to PTY session", + "description": "Establish a WebSocket connection to interact with a pseudo-terminal (PTY) session in real-time.", + "responses": { + "200": { + "description": "Connected session", + "content": { + "application/json": { + "schema": { + "type": "boolean" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundError" + } + } + } + } + } + } + }, + "/config": { + "get": { + "operationId": "config.get", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + } + ], + "summary": "Get configuration", + "description": "Retrieve the current OpenCode configuration settings and preferences.", + "responses": { + "200": { + "description": "Get config info", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Config" + } + } + } + } + } + }, + "patch": { + "operationId": "config.update", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + } + ], + "summary": "Update configuration", + "description": "Update OpenCode configuration settings and preferences.", + "responses": { + "200": { + "description": "Successfully updated config", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Config" } + } } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } + } }, - "/experimental/worktree/reset": { - "post": { - "operationId": "worktree.reset", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - } - ], - "summary": "Reset worktree", - "description": "Reset a worktree branch to the primary default branch.", - "responses": { - "200": { - "description": "Worktree reset", - "content": { - "application/json": { - "schema": { - "type": "boolean" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/WorktreeResetInput" - } - } - } + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Config" + } + } + } + } + } + }, + "/config/providers": { + "get": { + "operationId": "config.providers", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + } + ], + "summary": "List config providers", + "description": "Get a list of all configured AI providers and their default models.", + "responses": { + "200": { + "description": "List of providers", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "providers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Provider" + } + }, + "default": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string" + } + } + }, + "required": ["providers", "default"] + } + } + } + } + } + } + }, + "/experimental/tool/ids": { + "get": { + "operationId": "tool.ids", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + } + ], + "summary": "List tool IDs", + "description": "Get a list of all available tool IDs, including both built-in tools and dynamically registered tools.", + "responses": { + "200": { + "description": "Tool IDs", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ToolIDs" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } + } + } + } + }, + "/experimental/tool": { + "get": { + "operationId": "tool.list", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "provider", + "schema": { + "type": "string" + }, + "required": true + }, + { + "in": "query", + "name": "model", + "schema": { + "type": "string" + }, + "required": true + } + ], + "summary": "List tools", + "description": "Get a list of available tools with their JSON schema parameters for a specific provider and model combination.", + "responses": { + "200": { + "description": "Tools", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ToolList" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } + } + } + } + }, + "/experimental/worktree": { + "post": { + "operationId": "worktree.create", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + } + ], + "summary": "Create worktree", + "description": "Create a new git worktree for the current project and run any configured startup scripts.", + "responses": { + "200": { + "description": "Worktree created", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Worktree" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" } + } } + } }, - "/experimental/resource": { - "get": { - "operationId": "experimental.resource.list", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - } - ], - "summary": "Get MCP resources", - "description": "Get all available MCP resources from connected servers. Optionally filter by name.", - "responses": { - "200": { - "description": "MCP resources", - "content": { - "application/json": { - "schema": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": { - "$ref": "#/components/schemas/McpResource" - } - } - } - } - } + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorktreeCreateInput" + } + } + } + } + }, + "get": { + "operationId": "worktree.list", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + } + ], + "summary": "List worktrees", + "description": "List all sandbox worktrees for the current project.", + "responses": { + "200": { + "description": "List of worktree directories", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + }, + "delete": { + "operationId": "worktree.remove", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + } + ], + "summary": "Remove worktree", + "description": "Remove a git worktree and delete its branch.", + "responses": { + "200": { + "description": "Worktree removed", + "content": { + "application/json": { + "schema": { + "type": "boolean" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" } + } } + } }, - "/session": { - "get": { - "operationId": "session.list", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - }, - "description": "Filter sessions by project directory" - }, - { - "in": "query", - "name": "roots", - "schema": { - "type": "boolean" - }, - "description": "Only return root sessions (no parentID)" - }, - { - "in": "query", - "name": "start", - "schema": { - "type": "number" - }, - "description": "Filter sessions updated on or after this timestamp (milliseconds since epoch)" - }, - { - "in": "query", - "name": "search", - "schema": { - "type": "string" - }, - "description": "Filter sessions by title (case-insensitive)" - }, - { - "in": "query", - "name": "limit", - "schema": { - "type": "number" - }, - "description": "Maximum number of sessions to return" - } - ], - "summary": "List sessions", - "description": "Get a list of all OpenCode sessions, sorted by most recently updated.", - "responses": { - "200": { - "description": "List of sessions", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Session" - } - } - } - } - } + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorktreeRemoveInput" + } + } + } + } + } + }, + "/experimental/worktree/reset": { + "post": { + "operationId": "worktree.reset", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + } + ], + "summary": "Reset worktree", + "description": "Reset a worktree branch to the primary default branch.", + "responses": { + "200": { + "description": "Worktree reset", + "content": { + "application/json": { + "schema": { + "type": "boolean" } - }, - "post": { - "operationId": "session.create", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - } - ], - "summary": "Create session", - "description": "Create a new OpenCode session for interacting with AI assistants and managing conversations.", - "responses": { - "200": { - "description": "Successfully created session", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Session" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "parentID": { - "type": "string", - "pattern": "^ses.*" - }, - "title": { - "type": "string" - }, - "permission": { - "$ref": "#/components/schemas/PermissionRuleset" - } - } - } - } - } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" } + } } + } }, - "/session/status": { - "get": { - "operationId": "session.status", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - } - ], - "summary": "Get session status", - "description": "Retrieve the current status of all sessions, including active, idle, and completed states.", - "responses": { - "200": { - "description": "Get session status", - "content": { - "application/json": { - "schema": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": { - "$ref": "#/components/schemas/SessionStatus" - } - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - } + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WorktreeResetInput" + } + } + } + } + } + }, + "/experimental/resource": { + "get": { + "operationId": "experimental.resource.list", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + } + ], + "summary": "Get MCP resources", + "description": "Get all available MCP resources from connected servers. Optionally filter by name.", + "responses": { + "200": { + "description": "MCP resources", + "content": { + "application/json": { + "schema": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "$ref": "#/components/schemas/McpResource" + } + } + } + } + } + } + } + }, + "/session": { + "get": { + "operationId": "session.list", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + }, + "description": "Filter sessions by project directory" + }, + { + "in": "query", + "name": "roots", + "schema": { + "type": "boolean" + }, + "description": "Only return root sessions (no parentID)" + }, + { + "in": "query", + "name": "start", + "schema": { + "type": "number" + }, + "description": "Filter sessions updated on or after this timestamp (milliseconds since epoch)" + }, + { + "in": "query", + "name": "search", + "schema": { + "type": "string" + }, + "description": "Filter sessions by title (case-insensitive)" + }, + { + "in": "query", + "name": "limit", + "schema": { + "type": "number" + }, + "description": "Maximum number of sessions to return" + } + ], + "summary": "List sessions", + "description": "Get a list of all OpenCode sessions, sorted by most recently updated.", + "responses": { + "200": { + "description": "List of sessions", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Session" + } + } + } + } + } + } + }, + "post": { + "operationId": "session.create", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + } + ], + "summary": "Create session", + "description": "Create a new OpenCode session for interacting with AI assistants and managing conversations.", + "responses": { + "200": { + "description": "Successfully created session", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Session" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" } + } } + } }, - "/session/{sessionID}": { - "get": { - "operationId": "session.get", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - }, - { - "in": "path", - "name": "sessionID", - "schema": { - "type": "string", - "pattern": "^ses.*" - }, - "required": true - } - ], - "summary": "Get session", - "description": "Retrieve detailed information about a specific OpenCode session.", - "tags": [ - "Session" - ], - "responses": { - "200": { - "description": "Get session", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Session" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NotFoundError" - } - } - } - } + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "parentID": { + "type": "string", + "pattern": "^ses.*" + }, + "title": { + "type": "string" + }, + "permission": { + "$ref": "#/components/schemas/PermissionRuleset" + } } - }, - "delete": { - "operationId": "session.delete", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - }, - { - "in": "path", - "name": "sessionID", - "schema": { - "type": "string", - "pattern": "^ses.*" - }, - "required": true - } - ], - "summary": "Delete session", - "description": "Delete a session and permanently remove all associated data, including messages and history.", - "responses": { - "200": { - "description": "Successfully deleted session", - "content": { - "application/json": { - "schema": { - "type": "boolean" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NotFoundError" - } - } - } - } + } + } + } + } + } + }, + "/session/status": { + "get": { + "operationId": "session.status", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + } + ], + "summary": "Get session status", + "description": "Retrieve the current status of all sessions, including active, idle, and completed states.", + "responses": { + "200": { + "description": "Get session status", + "content": { + "application/json": { + "schema": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "$ref": "#/components/schemas/SessionStatus" + } } - }, - "patch": { - "operationId": "session.update", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - }, - { - "in": "path", - "name": "sessionID", - "schema": { - "type": "string" - }, - "required": true - } - ], - "summary": "Update session", - "description": "Update properties of an existing session, such as title or other metadata.", - "responses": { - "200": { - "description": "Successfully updated session", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Session" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NotFoundError" - } - } - } - } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "title": { - "type": "string" - }, - "time": { - "type": "object", - "properties": { - "archived": { - "type": "number" - } - } - } - } - } - } - } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" } + } } - }, - "/session/{sessionID}/children": { - "get": { - "operationId": "session.children", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - }, - { - "in": "path", - "name": "sessionID", - "schema": { - "type": "string", - "pattern": "^ses.*" - }, - "required": true - } - ], - "summary": "Get session children", - "tags": [ - "Session" - ], - "description": "Retrieve all child sessions that were forked from the specified parent session.", - "responses": { - "200": { - "description": "List of children", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Session" - } - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NotFoundError" - } - } - } - } + } + } + } + }, + "/session/{sessionID}": { + "get": { + "operationId": "session.get", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "sessionID", + "schema": { + "type": "string", + "pattern": "^ses.*" + }, + "required": true + } + ], + "summary": "Get session", + "description": "Retrieve detailed information about a specific OpenCode session.", + "tags": ["Session"], + "responses": { + "200": { + "description": "Get session", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Session" } + } } - }, - "/session/{sessionID}/todo": { - "get": { - "operationId": "session.todo", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - }, - { - "in": "path", - "name": "sessionID", - "schema": { - "type": "string" - }, - "required": true, - "description": "Session ID" - } - ], - "summary": "Get session todos", - "description": "Retrieve the todo list associated with a specific session, showing tasks and action items.", - "responses": { - "200": { - "description": "Todo list", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Todo" - } - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NotFoundError" - } - } - } - } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" } + } } - }, - "/session/{sessionID}/init": { - "post": { - "operationId": "session.init", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - }, - { - "in": "path", - "name": "sessionID", - "schema": { - "type": "string" - }, - "required": true, - "description": "Session ID" - } - ], - "summary": "Initialize session", - "description": "Analyze the current application and create an AGENTS.md file with project-specific agent configurations.", - "responses": { - "200": { - "description": "200", - "content": { - "application/json": { - "schema": { - "type": "boolean" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NotFoundError" - } - } - } - } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "modelID": { - "type": "string" - }, - "providerID": { - "type": "string" - }, - "messageID": { - "type": "string", - "pattern": "^msg.*" - } - }, - "required": [ - "modelID", - "providerID", - "messageID" - ] - } - } - } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundError" } + } } - }, - "/session/{sessionID}/fork": { - "post": { - "operationId": "session.fork", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - }, - { - "in": "path", - "name": "sessionID", - "schema": { - "type": "string", - "pattern": "^ses.*" - }, - "required": true - } - ], - "summary": "Fork session", - "description": "Create a new session by forking an existing session at a specific message point.", - "responses": { - "200": { - "description": "200", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Session" - } - } - } - } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "messageID": { - "type": "string", - "pattern": "^msg.*" - } - } - } - } - } + } + } + }, + "delete": { + "operationId": "session.delete", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "sessionID", + "schema": { + "type": "string", + "pattern": "^ses.*" + }, + "required": true + } + ], + "summary": "Delete session", + "description": "Delete a session and permanently remove all associated data, including messages and history.", + "responses": { + "200": { + "description": "Successfully deleted session", + "content": { + "application/json": { + "schema": { + "type": "boolean" } + } } - }, - "/session/{sessionID}/abort": { - "post": { - "operationId": "session.abort", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - }, - { - "in": "path", - "name": "sessionID", - "schema": { - "type": "string" - }, - "required": true - } - ], - "summary": "Abort session", - "description": "Abort an active session and stop any ongoing AI processing or command execution.", - "responses": { - "200": { - "description": "Aborted session", - "content": { - "application/json": { - "schema": { - "type": "boolean" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NotFoundError" - } - } - } - } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" } + } } - }, - "/session/{sessionID}/share": { - "post": { - "operationId": "session.share", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - }, - { - "in": "path", - "name": "sessionID", - "schema": { - "type": "string" - }, - "required": true - } - ], - "summary": "Share session", - "description": "Create a shareable link for a session, allowing others to view the conversation.", - "responses": { - "200": { - "description": "Successfully shared session", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Session" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NotFoundError" - } - } - } - } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundError" } - }, - "delete": { - "operationId": "session.unshare", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - }, - { - "in": "path", - "name": "sessionID", - "schema": { - "type": "string", - "pattern": "^ses.*" - }, - "required": true - } - ], - "summary": "Unshare session", - "description": "Remove the shareable link for a session, making it private again.", - "responses": { - "200": { - "description": "Successfully unshared session", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Session" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NotFoundError" - } - } - } - } + } + } + } + } + }, + "patch": { + "operationId": "session.update", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "sessionID", + "schema": { + "type": "string" + }, + "required": true + } + ], + "summary": "Update session", + "description": "Update properties of an existing session, such as title or other metadata.", + "responses": { + "200": { + "description": "Successfully updated session", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Session" } + } } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundError" + } + } + } + } }, - "/session/{sessionID}/diff": { - "get": { - "operationId": "session.diff", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - }, - { - "in": "path", - "name": "sessionID", - "schema": { - "type": "string", - "pattern": "^ses.*" - }, - "required": true - }, - { - "in": "query", - "name": "messageID", - "schema": { - "type": "string", - "pattern": "^msg.*" - } - } - ], - "summary": "Get message diff", - "description": "Get the file changes (diff) that resulted from a specific user message in the session.", - "responses": { - "200": { - "description": "Successfully retrieved diff", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/FileDiff" - } - } - } - } + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "time": { + "type": "object", + "properties": { + "archived": { + "type": "number" + } } + } } + } } - }, - "/session/{sessionID}/summarize": { - "post": { - "operationId": "session.summarize", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - }, - { - "in": "path", - "name": "sessionID", - "schema": { - "type": "string" - }, - "required": true, - "description": "Session ID" - } - ], - "summary": "Summarize session", - "description": "Generate a concise summary of the session using AI compaction to preserve key information.", - "responses": { - "200": { - "description": "Summarized session", - "content": { - "application/json": { - "schema": { - "type": "boolean" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NotFoundError" - } - } - } - } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "providerID": { - "type": "string" - }, - "modelID": { - "type": "string" - }, - "auto": { - "default": false, - "type": "boolean" - } - }, - "required": [ - "providerID", - "modelID" - ] - } - } - } + } + } + } + }, + "/session/{sessionID}/children": { + "get": { + "operationId": "session.children", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "sessionID", + "schema": { + "type": "string", + "pattern": "^ses.*" + }, + "required": true + } + ], + "summary": "Get session children", + "tags": ["Session"], + "description": "Retrieve all child sessions that were forked from the specified parent session.", + "responses": { + "200": { + "description": "List of children", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Session" + } + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundError" + } + } + } + } + } + } + }, + "/session/{sessionID}/todo": { + "get": { + "operationId": "session.todo", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "sessionID", + "schema": { + "type": "string" + }, + "required": true, + "description": "Session ID" + } + ], + "summary": "Get session todos", + "description": "Retrieve the todo list associated with a specific session, showing tasks and action items.", + "responses": { + "200": { + "description": "Todo list", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Todo" + } + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundError" + } + } + } + } + } + } + }, + "/session/{sessionID}/init": { + "post": { + "operationId": "session.init", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "sessionID", + "schema": { + "type": "string" + }, + "required": true, + "description": "Session ID" + } + ], + "summary": "Initialize session", + "description": "Analyze the current application and create an AGENTS.md file with project-specific agent configurations.", + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "type": "boolean" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundError" } + } } + } }, - "/session/{sessionID}/message": { - "get": { - "operationId": "session.messages", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - }, - { - "in": "path", - "name": "sessionID", - "schema": { - "type": "string" - }, - "required": true, - "description": "Session ID" - }, - { - "in": "query", - "name": "limit", - "schema": { - "type": "number" - } - } - ], - "summary": "Get session messages", - "description": "Retrieve all messages in a session, including user prompts and AI responses.", - "responses": { - "200": { - "description": "List of messages", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "type": "object", - "properties": { - "info": { - "$ref": "#/components/schemas/Message" - }, - "parts": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Part" - } - } - }, - "required": [ - "info", - "parts" - ] - } - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NotFoundError" - } - } - } - } + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "modelID": { + "type": "string" + }, + "providerID": { + "type": "string" + }, + "messageID": { + "type": "string", + "pattern": "^msg.*" + } + }, + "required": ["modelID", "providerID", "messageID"] + } + } + } + } + } + }, + "/session/{sessionID}/fork": { + "post": { + "operationId": "session.fork", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "sessionID", + "schema": { + "type": "string", + "pattern": "^ses.*" + }, + "required": true + } + ], + "summary": "Fork session", + "description": "Create a new session by forking an existing session at a specific message point.", + "responses": { + "200": { + "description": "200", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Session" } - }, - "post": { - "operationId": "session.prompt", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - }, - { - "in": "path", - "name": "sessionID", - "schema": { - "type": "string" - }, - "required": true, - "description": "Session ID" - } - ], - "summary": "Send message", - "description": "Create and send a new message to a session, streaming the AI response.", - "responses": { - "200": { - "description": "Created message", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "info": { - "$ref": "#/components/schemas/AssistantMessage" - }, - "parts": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Part" - } - } - }, - "required": [ - "info", - "parts" - ] - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NotFoundError" - } - } - } - } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "messageID": { - "type": "string", - "pattern": "^msg.*" - }, - "model": { - "type": "object", - "properties": { - "providerID": { - "type": "string" - }, - "modelID": { - "type": "string" - } - }, - "required": [ - "providerID", - "modelID" - ] - }, - "agent": { - "type": "string" - }, - "noReply": { - "type": "boolean" - }, - "tools": { - "description": "@deprecated tools and permissions have been merged, you can set permissions on the session itself now", - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": { - "type": "boolean" - } - }, - "system": { - "type": "string" - }, - "variant": { - "type": "string" - }, - "parts": { - "type": "array", - "items": { - "anyOf": [ - { - "$ref": "#/components/schemas/TextPartInput" - }, - { - "$ref": "#/components/schemas/FilePartInput" - }, - { - "$ref": "#/components/schemas/AgentPartInput" - }, - { - "$ref": "#/components/schemas/SubtaskPartInput" - } - ] - } - } - }, - "required": [ - "parts" - ] - } - } - } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "messageID": { + "type": "string", + "pattern": "^msg.*" + } + } + } + } + } + } + } + }, + "/session/{sessionID}/abort": { + "post": { + "operationId": "session.abort", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "sessionID", + "schema": { + "type": "string" + }, + "required": true + } + ], + "summary": "Abort session", + "description": "Abort an active session and stop any ongoing AI processing or command execution.", + "responses": { + "200": { + "description": "Aborted session", + "content": { + "application/json": { + "schema": { + "type": "boolean" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundError" + } + } + } + } + } + } + }, + "/session/{sessionID}/share": { + "post": { + "operationId": "session.share", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "sessionID", + "schema": { + "type": "string" + }, + "required": true + } + ], + "summary": "Share session", + "description": "Create a shareable link for a session, allowing others to view the conversation.", + "responses": { + "200": { + "description": "Successfully shared session", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Session" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundError" + } + } + } + } + } + }, + "delete": { + "operationId": "session.unshare", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "sessionID", + "schema": { + "type": "string", + "pattern": "^ses.*" + }, + "required": true + } + ], + "summary": "Unshare session", + "description": "Remove the shareable link for a session, making it private again.", + "responses": { + "200": { + "description": "Successfully unshared session", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Session" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundError" + } + } + } + } + } + } + }, + "/session/{sessionID}/diff": { + "get": { + "operationId": "session.diff", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "sessionID", + "schema": { + "type": "string", + "pattern": "^ses.*" + }, + "required": true + }, + { + "in": "query", + "name": "messageID", + "schema": { + "type": "string", + "pattern": "^msg.*" + } + } + ], + "summary": "Get message diff", + "description": "Get the file changes (diff) that resulted from a specific user message in the session.", + "responses": { + "200": { + "description": "Successfully retrieved diff", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FileDiff" + } + } + } + } + } + } + } + }, + "/session/{sessionID}/summarize": { + "post": { + "operationId": "session.summarize", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "sessionID", + "schema": { + "type": "string" + }, + "required": true, + "description": "Session ID" + } + ], + "summary": "Summarize session", + "description": "Generate a concise summary of the session using AI compaction to preserve key information.", + "responses": { + "200": { + "description": "Summarized session", + "content": { + "application/json": { + "schema": { + "type": "boolean" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" } + } } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundError" + } + } + } + } }, - "/session/{sessionID}/message/{messageID}": { - "get": { - "operationId": "session.message", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - }, - { - "in": "path", - "name": "sessionID", - "schema": { - "type": "string" - }, - "required": true, - "description": "Session ID" - }, - { - "in": "path", - "name": "messageID", - "schema": { - "type": "string" - }, - "required": true, - "description": "Message ID" - } - ], - "summary": "Get message", - "description": "Retrieve a specific message from a session by its message ID.", - "responses": { - "200": { - "description": "Message", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "info": { - "$ref": "#/components/schemas/Message" - }, - "parts": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Part" - } - } - }, - "required": [ - "info", - "parts" - ] - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "providerID": { + "type": "string" + }, + "modelID": { + "type": "string" + }, + "auto": { + "default": false, + "type": "boolean" + } + }, + "required": ["providerID", "modelID"] + } + } + } + } + } + }, + "/session/{sessionID}/message": { + "get": { + "operationId": "session.messages", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "sessionID", + "schema": { + "type": "string" + }, + "required": true, + "description": "Session ID" + }, + { + "in": "query", + "name": "limit", + "schema": { + "type": "number" + } + } + ], + "summary": "Get session messages", + "description": "Retrieve all messages in a session, including user prompts and AI responses.", + "responses": { + "200": { + "description": "List of messages", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "info": { + "$ref": "#/components/schemas/Message" + }, + "parts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Part" } + } }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NotFoundError" - } - } - } - } + "required": ["info", "parts"] + } + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundError" + } + } + } + } + } + }, + "post": { + "operationId": "session.prompt", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "sessionID", + "schema": { + "type": "string" + }, + "required": true, + "description": "Session ID" + } + ], + "summary": "Send message", + "description": "Create and send a new message to a session, streaming the AI response.", + "responses": { + "200": { + "description": "Created message", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "info": { + "$ref": "#/components/schemas/AssistantMessage" + }, + "parts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Part" + } + } + }, + "required": ["info", "parts"] + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundError" } + } } + } }, - "/session/{sessionID}/message/{messageID}/part/{partID}": { - "delete": { - "operationId": "part.delete", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "messageID": { + "type": "string", + "pattern": "^msg.*" + }, + "model": { + "type": "object", + "properties": { + "providerID": { + "type": "string" + }, + "modelID": { + "type": "string" + } }, - { - "in": "path", - "name": "sessionID", - "schema": { - "type": "string" + "required": ["providerID", "modelID"] + }, + "agent": { + "type": "string" + }, + "noReply": { + "type": "boolean" + }, + "tools": { + "description": "@deprecated tools and permissions have been merged, you can set permissions on the session itself now", + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "boolean" + } + }, + "system": { + "type": "string" + }, + "variant": { + "type": "string" + }, + "parts": { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/components/schemas/TextPartInput" }, - "required": true, - "description": "Session ID" - }, - { - "in": "path", - "name": "messageID", - "schema": { - "type": "string" + { + "$ref": "#/components/schemas/FilePartInput" }, - "required": true, - "description": "Message ID" - }, - { - "in": "path", - "name": "partID", - "schema": { - "type": "string" + { + "$ref": "#/components/schemas/AgentPartInput" }, - "required": true, - "description": "Part ID" - } - ], - "description": "Delete a part from a message", - "responses": { - "200": { - "description": "Successfully deleted part", - "content": { - "application/json": { - "schema": { - "type": "boolean" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NotFoundError" - } - } - } - } - } - }, - "patch": { - "operationId": "part.update", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - }, - { - "in": "path", - "name": "sessionID", - "schema": { - "type": "string" - }, - "required": true, - "description": "Session ID" - }, - { - "in": "path", - "name": "messageID", - "schema": { - "type": "string" - }, - "required": true, - "description": "Message ID" - }, - { - "in": "path", - "name": "partID", - "schema": { - "type": "string" - }, - "required": true, - "description": "Part ID" - } - ], - "description": "Update a part in a message", - "responses": { - "200": { - "description": "Successfully updated part", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Part" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NotFoundError" - } - } + { + "$ref": "#/components/schemas/SubtaskPartInput" } + ] } + } }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Part" - } - } - } + "required": ["parts"] + } + } + } + } + } + }, + "/session/{sessionID}/message/{messageID}": { + "get": { + "operationId": "session.message", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "sessionID", + "schema": { + "type": "string" + }, + "required": true, + "description": "Session ID" + }, + { + "in": "path", + "name": "messageID", + "schema": { + "type": "string" + }, + "required": true, + "description": "Message ID" + } + ], + "summary": "Get message", + "description": "Retrieve a specific message from a session by its message ID.", + "responses": { + "200": { + "description": "Message", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "info": { + "$ref": "#/components/schemas/Message" + }, + "parts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Part" + } + } + }, + "required": ["info", "parts"] + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundError" + } + } + } + } + } + } + }, + "/session/{sessionID}/message/{messageID}/part/{partID}": { + "delete": { + "operationId": "part.delete", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "sessionID", + "schema": { + "type": "string" + }, + "required": true, + "description": "Session ID" + }, + { + "in": "path", + "name": "messageID", + "schema": { + "type": "string" + }, + "required": true, + "description": "Message ID" + }, + { + "in": "path", + "name": "partID", + "schema": { + "type": "string" + }, + "required": true, + "description": "Part ID" + } + ], + "description": "Delete a part from a message", + "responses": { + "200": { + "description": "Successfully deleted part", + "content": { + "application/json": { + "schema": { + "type": "boolean" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundError" } + } } + } + } + }, + "patch": { + "operationId": "part.update", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "sessionID", + "schema": { + "type": "string" + }, + "required": true, + "description": "Session ID" + }, + { + "in": "path", + "name": "messageID", + "schema": { + "type": "string" + }, + "required": true, + "description": "Message ID" + }, + { + "in": "path", + "name": "partID", + "schema": { + "type": "string" + }, + "required": true, + "description": "Part ID" + } + ], + "description": "Update a part in a message", + "responses": { + "200": { + "description": "Successfully updated part", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Part" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundError" + } + } + } + } }, - "/session/{sessionID}/prompt_async": { - "post": { - "operationId": "session.prompt_async", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - }, - { - "in": "path", - "name": "sessionID", - "schema": { - "type": "string" - }, - "required": true, - "description": "Session ID" - } - ], - "summary": "Send async message", - "description": "Create and send a new message to a session asynchronously, starting the session if needed and returning immediately.", - "responses": { - "204": { - "description": "Prompt accepted" - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NotFoundError" - } - } - } - } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "messageID": { - "type": "string", - "pattern": "^msg.*" - }, - "model": { - "type": "object", - "properties": { - "providerID": { - "type": "string" - }, - "modelID": { - "type": "string" - } - }, - "required": [ - "providerID", - "modelID" - ] - }, - "agent": { - "type": "string" - }, - "noReply": { - "type": "boolean" - }, - "tools": { - "description": "@deprecated tools and permissions have been merged, you can set permissions on the session itself now", - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": { - "type": "boolean" - } - }, - "system": { - "type": "string" - }, - "variant": { - "type": "string" - }, - "parts": { - "type": "array", - "items": { - "anyOf": [ - { - "$ref": "#/components/schemas/TextPartInput" - }, - { - "$ref": "#/components/schemas/FilePartInput" - }, - { - "$ref": "#/components/schemas/AgentPartInput" - }, - { - "$ref": "#/components/schemas/SubtaskPartInput" - } - ] - } - } - }, - "required": [ - "parts" - ] - } - } - } + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Part" + } + } + } + } + } + }, + "/session/{sessionID}/prompt_async": { + "post": { + "operationId": "session.prompt_async", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "sessionID", + "schema": { + "type": "string" + }, + "required": true, + "description": "Session ID" + } + ], + "summary": "Send async message", + "description": "Create and send a new message to a session asynchronously, starting the session if needed and returning immediately.", + "responses": { + "204": { + "description": "Prompt accepted" + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundError" } + } } + } }, - "/session/{sessionID}/command": { - "post": { - "operationId": "session.command", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "messageID": { + "type": "string", + "pattern": "^msg.*" + }, + "model": { + "type": "object", + "properties": { + "providerID": { + "type": "string" + }, + "modelID": { + "type": "string" + } }, - { - "in": "path", - "name": "sessionID", - "schema": { - "type": "string" + "required": ["providerID", "modelID"] + }, + "agent": { + "type": "string" + }, + "noReply": { + "type": "boolean" + }, + "tools": { + "description": "@deprecated tools and permissions have been merged, you can set permissions on the session itself now", + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "boolean" + } + }, + "system": { + "type": "string" + }, + "variant": { + "type": "string" + }, + "parts": { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/components/schemas/TextPartInput" }, - "required": true, - "description": "Session ID" - } - ], - "summary": "Send command", - "description": "Send a new command to a session for execution by the AI assistant.", - "responses": { - "200": { - "description": "Created message", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "info": { - "$ref": "#/components/schemas/AssistantMessage" - }, - "parts": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Part" - } - } - }, - "required": [ - "info", - "parts" - ] - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NotFoundError" - } - } + { + "$ref": "#/components/schemas/FilePartInput" + }, + { + "$ref": "#/components/schemas/AgentPartInput" + }, + { + "$ref": "#/components/schemas/SubtaskPartInput" } + ] } + } }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "messageID": { - "type": "string", - "pattern": "^msg.*" - }, - "agent": { - "type": "string" - }, - "model": { - "type": "string" - }, - "arguments": { - "type": "string" - }, - "command": { - "type": "string" - }, - "variant": { - "type": "string" - }, - "parts": { - "type": "array", - "items": { - "anyOf": [ - { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "type": { - "type": "string", - "const": "file" - }, - "mime": { - "type": "string" - }, - "filename": { - "type": "string" - }, - "url": { - "type": "string" - }, - "source": { - "$ref": "#/components/schemas/FilePartSource" - } - }, - "required": [ - "type", - "mime", - "url" - ] - } - ] - } - } - }, - "required": [ - "arguments", - "command" - ] - } - } - } + "required": ["parts"] + } + } + } + } + } + }, + "/session/{sessionID}/command": { + "post": { + "operationId": "session.command", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "sessionID", + "schema": { + "type": "string" + }, + "required": true, + "description": "Session ID" + } + ], + "summary": "Send command", + "description": "Send a new command to a session for execution by the AI assistant.", + "responses": { + "200": { + "description": "Created message", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "info": { + "$ref": "#/components/schemas/AssistantMessage" + }, + "parts": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Part" + } + } + }, + "required": ["info", "parts"] } + } } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundError" + } + } + } + } }, - "/session/{sessionID}/shell": { - "post": { - "operationId": "session.shell", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - }, - { - "in": "path", - "name": "sessionID", - "schema": { - "type": "string" - }, - "required": true, - "description": "Session ID" - } - ], - "summary": "Run shell command", - "description": "Execute a shell command within the session context and return the AI's response.", - "responses": { - "200": { - "description": "Created message", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AssistantMessage" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NotFoundError" - } + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "messageID": { + "type": "string", + "pattern": "^msg.*" + }, + "agent": { + "type": "string" + }, + "model": { + "type": "string" + }, + "arguments": { + "type": "string" + }, + "command": { + "type": "string" + }, + "variant": { + "type": "string" + }, + "parts": { + "type": "array", + "items": { + "anyOf": [ + { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "const": "file" + }, + "mime": { + "type": "string" + }, + "filename": { + "type": "string" + }, + "url": { + "type": "string" + }, + "source": { + "$ref": "#/components/schemas/FilePartSource" } + }, + "required": ["type", "mime", "url"] } + ] } + } }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "agent": { - "type": "string" - }, - "model": { - "type": "object", - "properties": { - "providerID": { - "type": "string" - }, - "modelID": { - "type": "string" - } - }, - "required": [ - "providerID", - "modelID" - ] - }, - "command": { - "type": "string" - } - }, - "required": [ - "agent", - "command" - ] - } - } - } + "required": ["arguments", "command"] + } + } + } + } + } + }, + "/session/{sessionID}/shell": { + "post": { + "operationId": "session.shell", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "sessionID", + "schema": { + "type": "string" + }, + "required": true, + "description": "Session ID" + } + ], + "summary": "Run shell command", + "description": "Execute a shell command within the session context and return the AI's response.", + "responses": { + "200": { + "description": "Created message", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AssistantMessage" } + } } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundError" + } + } + } + } }, - "/session/{sessionID}/revert": { - "post": { - "operationId": "session.revert", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - }, - { - "in": "path", - "name": "sessionID", - "schema": { - "type": "string" - }, - "required": true - } - ], - "summary": "Revert message", - "description": "Revert a specific message in a session, undoing its effects and restoring the previous state.", - "responses": { - "200": { - "description": "Updated session", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Session" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "agent": { + "type": "string" + }, + "model": { + "type": "object", + "properties": { + "providerID": { + "type": "string" + }, + "modelID": { + "type": "string" + } }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NotFoundError" - } - } - } - } + "required": ["providerID", "modelID"] + }, + "command": { + "type": "string" + } }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "messageID": { - "type": "string", - "pattern": "^msg.*" - }, - "partID": { - "type": "string", - "pattern": "^prt.*" - } - }, - "required": [ - "messageID" - ] - } - } - } + "required": ["agent", "command"] + } + } + } + } + } + }, + "/session/{sessionID}/revert": { + "post": { + "operationId": "session.revert", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "sessionID", + "schema": { + "type": "string" + }, + "required": true + } + ], + "summary": "Revert message", + "description": "Revert a specific message in a session, undoing its effects and restoring the previous state.", + "responses": { + "200": { + "description": "Updated session", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Session" } + } } - }, - "/session/{sessionID}/unrevert": { - "post": { - "operationId": "session.unrevert", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - }, - { - "in": "path", - "name": "sessionID", - "schema": { - "type": "string" - }, - "required": true - } - ], - "summary": "Restore reverted messages", - "description": "Restore all previously reverted messages in a session.", - "responses": { - "200": { - "description": "Updated session", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Session" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NotFoundError" - } - } - } - } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" } + } } - }, - "/session/{sessionID}/permissions/{permissionID}": { - "post": { - "operationId": "permission.respond", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - }, - { - "in": "path", - "name": "sessionID", - "schema": { - "type": "string" - }, - "required": true - }, - { - "in": "path", - "name": "permissionID", - "schema": { - "type": "string" - }, - "required": true - } - ], - "summary": "Respond to permission", - "deprecated": true, - "description": "Approve or deny a permission request from the AI assistant.", - "responses": { - "200": { - "description": "Permission processed successfully", - "content": { - "application/json": { - "schema": { - "type": "boolean" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NotFoundError" - } - } - } - } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "response": { - "type": "string", - "enum": [ - "once", - "always", - "reject" - ] - } - }, - "required": [ - "response" - ] - } - } - } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundError" } + } } + } }, - "/permission/{requestID}/reply": { - "post": { - "operationId": "permission.reply", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - }, - { - "in": "path", - "name": "requestID", - "schema": { - "type": "string" - }, - "required": true - } - ], - "summary": "Respond to permission request", - "description": "Approve or deny a permission request from the AI assistant.", - "responses": { - "200": { - "description": "Permission processed successfully", - "content": { - "application/json": { - "schema": { - "type": "boolean" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NotFoundError" - } - } - } - } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "reply": { - "type": "string", - "enum": [ - "once", - "always", - "reject" - ] - }, - "message": { - "type": "string" - } - }, - "required": [ - "reply" - ] - } - } - } + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "messageID": { + "type": "string", + "pattern": "^msg.*" + }, + "partID": { + "type": "string", + "pattern": "^prt.*" + } + }, + "required": ["messageID"] + } + } + } + } + } + }, + "/session/{sessionID}/unrevert": { + "post": { + "operationId": "session.unrevert", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "sessionID", + "schema": { + "type": "string" + }, + "required": true + } + ], + "summary": "Restore reverted messages", + "description": "Restore all previously reverted messages in a session.", + "responses": { + "200": { + "description": "Updated session", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Session" } + } } - }, - "/permission": { - "get": { - "operationId": "permission.list", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - } - ], - "summary": "List pending permissions", - "description": "Get all pending permission requests across all sessions.", - "responses": { - "200": { - "description": "List of pending permissions", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PermissionRequest" - } - } - } - } - } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" } + } } - }, - "/question": { - "get": { - "operationId": "question.list", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - } - ], - "summary": "List pending questions", - "description": "Get all pending question requests across all sessions.", - "responses": { - "200": { - "description": "List of pending questions", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/QuestionRequest" - } - } - } - } - } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundError" + } + } + } + } + } + } + }, + "/session/{sessionID}/permissions/{permissionID}": { + "post": { + "operationId": "permission.respond", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "sessionID", + "schema": { + "type": "string" + }, + "required": true + }, + { + "in": "path", + "name": "permissionID", + "schema": { + "type": "string" + }, + "required": true + } + ], + "summary": "Respond to permission", + "deprecated": true, + "description": "Approve or deny a permission request from the AI assistant.", + "responses": { + "200": { + "description": "Permission processed successfully", + "content": { + "application/json": { + "schema": { + "type": "boolean" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" } + } } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundError" + } + } + } + } }, - "/question/{requestID}/reply": { - "post": { - "operationId": "question.reply", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - }, - { - "in": "path", - "name": "requestID", - "schema": { - "type": "string" - }, - "required": true - } - ], - "summary": "Reply to question request", - "description": "Provide answers to a question request from the AI assistant.", - "responses": { - "200": { - "description": "Question answered successfully", - "content": { - "application/json": { - "schema": { - "type": "boolean" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NotFoundError" - } - } - } - } + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "response": { + "type": "string", + "enum": ["once", "always", "reject"] + } }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "answers": { - "description": "User answers in order of questions (each answer is an array of selected labels)", - "type": "array", - "items": { - "$ref": "#/components/schemas/QuestionAnswer" - } - } - }, - "required": [ - "answers" - ] - } - } - } + "required": ["response"] + } + } + } + } + } + }, + "/permission/{requestID}/reply": { + "post": { + "operationId": "permission.reply", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "requestID", + "schema": { + "type": "string" + }, + "required": true + } + ], + "summary": "Respond to permission request", + "description": "Approve or deny a permission request from the AI assistant.", + "responses": { + "200": { + "description": "Permission processed successfully", + "content": { + "application/json": { + "schema": { + "type": "boolean" } + } } - }, - "/question/{requestID}/reject": { - "post": { - "operationId": "question.reject", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - }, - { - "in": "path", - "name": "requestID", - "schema": { - "type": "string" - }, - "required": true - } - ], - "summary": "Reject question request", - "description": "Reject a question request from the AI assistant.", - "responses": { - "200": { - "description": "Question rejected successfully", - "content": { - "application/json": { - "schema": { - "type": "boolean" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NotFoundError" - } - } - } - } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" } + } } - }, - "/provider": { - "get": { - "operationId": "provider.list", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - } - ], - "summary": "List providers", - "description": "Get a list of all available AI providers, including both available and connected ones.", - "responses": { - "200": { - "description": "List of providers", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "all": { - "type": "array", - "items": { - "type": "object", - "properties": { - "api": { - "type": "string" - }, - "name": { - "type": "string" - }, - "env": { - "type": "array", - "items": { - "type": "string" - } - }, - "id": { - "type": "string" - }, - "npm": { - "type": "string" - }, - "models": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "family": { - "type": "string" - }, - "release_date": { - "type": "string" - }, - "attachment": { - "type": "boolean" - }, - "reasoning": { - "type": "boolean" - }, - "temperature": { - "type": "boolean" - }, - "tool_call": { - "type": "boolean" - }, - "interleaved": { - "anyOf": [ - { - "type": "boolean", - "const": true - }, - { - "type": "object", - "properties": { - "field": { - "type": "string", - "enum": [ - "reasoning_content", - "reasoning_details" - ] - } - }, - "required": [ - "field" - ], - "additionalProperties": false - } - ] - }, - "cost": { - "type": "object", - "properties": { - "input": { - "type": "number" - }, - "output": { - "type": "number" - }, - "cache_read": { - "type": "number" - }, - "cache_write": { - "type": "number" - }, - "context_over_200k": { - "type": "object", - "properties": { - "input": { - "type": "number" - }, - "output": { - "type": "number" - }, - "cache_read": { - "type": "number" - }, - "cache_write": { - "type": "number" - } - }, - "required": [ - "input", - "output" - ] - } - }, - "required": [ - "input", - "output" - ] - }, - "limit": { - "type": "object", - "properties": { - "context": { - "type": "number" - }, - "input": { - "type": "number" - }, - "output": { - "type": "number" - } - }, - "required": [ - "context", - "output" - ] - }, - "modalities": { - "type": "object", - "properties": { - "input": { - "type": "array", - "items": { - "type": "string", - "enum": [ - "text", - "audio", - "image", - "video", - "pdf" - ] - } - }, - "output": { - "type": "array", - "items": { - "type": "string", - "enum": [ - "text", - "audio", - "image", - "video", - "pdf" - ] - } - } - }, - "required": [ - "input", - "output" - ] - }, - "experimental": { - "type": "boolean" - }, - "status": { - "type": "string", - "enum": [ - "alpha", - "beta", - "deprecated" - ] - }, - "options": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": {} - }, - "headers": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": { - "type": "string" - } - }, - "provider": { - "type": "object", - "properties": { - "npm": { - "type": "string" - } - }, - "required": [ - "npm" - ] - }, - "variants": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": {} - } - } - }, - "required": [ - "id", - "name", - "release_date", - "attachment", - "reasoning", - "temperature", - "tool_call", - "limit", - "options" - ] - } - } - }, - "required": [ - "name", - "env", - "id", - "models" - ] - } - }, - "default": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": { - "type": "string" - } - }, - "connected": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "required": [ - "all", - "default", - "connected" - ] - } - } - } - } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundError" } + } } + } }, - "/provider/auth": { - "get": { - "operationId": "provider.auth", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - } - ], - "summary": "Get provider auth methods", - "description": "Retrieve available authentication methods for all AI providers.", - "responses": { - "200": { - "description": "Provider auth methods", - "content": { - "application/json": { - "schema": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ProviderAuthMethod" - } - } - } - } - } - } + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "reply": { + "type": "string", + "enum": ["once", "always", "reject"] + }, + "message": { + "type": "string" + } + }, + "required": ["reply"] + } + } + } + } + } + }, + "/permission": { + "get": { + "operationId": "permission.list", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + } + ], + "summary": "List pending permissions", + "description": "Get all pending permission requests across all sessions.", + "responses": { + "200": { + "description": "List of pending permissions", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PermissionRequest" + } + } + } + } + } + } + } + }, + "/question": { + "get": { + "operationId": "question.list", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + } + ], + "summary": "List pending questions", + "description": "Get all pending question requests across all sessions.", + "responses": { + "200": { + "description": "List of pending questions", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/QuestionRequest" + } + } + } + } + } + } + } + }, + "/question/{requestID}/reply": { + "post": { + "operationId": "question.reply", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "requestID", + "schema": { + "type": "string" + }, + "required": true + } + ], + "summary": "Reply to question request", + "description": "Provide answers to a question request from the AI assistant.", + "responses": { + "200": { + "description": "Question answered successfully", + "content": { + "application/json": { + "schema": { + "type": "boolean" } + } } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundError" + } + } + } + } }, - "/provider/{providerID}/oauth/authorize": { - "post": { - "operationId": "provider.oauth.authorize", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - }, - { - "in": "path", - "name": "providerID", - "schema": { - "type": "string" - }, - "required": true, - "description": "Provider ID" - } - ], - "summary": "OAuth authorize", - "description": "Initiate OAuth authorization for a specific AI provider to get an authorization URL.", - "responses": { - "200": { - "description": "Authorization URL and method", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProviderAuthAuthorization" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "answers": { + "description": "User answers in order of questions (each answer is an array of selected labels)", + "type": "array", + "items": { + "$ref": "#/components/schemas/QuestionAnswer" } + } }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "method": { - "description": "Auth method index", - "type": "number" - } - }, - "required": [ - "method" - ] - } - } - } + "required": ["answers"] + } + } + } + } + } + }, + "/question/{requestID}/reject": { + "post": { + "operationId": "question.reject", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "requestID", + "schema": { + "type": "string" + }, + "required": true + } + ], + "summary": "Reject question request", + "description": "Reject a question request from the AI assistant.", + "responses": { + "200": { + "description": "Question rejected successfully", + "content": { + "application/json": { + "schema": { + "type": "boolean" } + } } - }, - "/provider/{providerID}/oauth/callback": { - "post": { - "operationId": "provider.oauth.callback", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundError" + } + } + } + } + } + } + }, + "/provider": { + "get": { + "operationId": "provider.list", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + } + ], + "summary": "List providers", + "description": "Get a list of all available AI providers, including both available and connected ones.", + "responses": { + "200": { + "description": "List of providers", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "all": { + "type": "array", + "items": { + "type": "object", + "properties": { + "api": { "type": "string" - } - }, - { - "in": "path", - "name": "providerID", - "schema": { + }, + "name": { "type": "string" - }, - "required": true, - "description": "Provider ID" - } - ], - "summary": "OAuth callback", - "description": "Handle the OAuth callback from a provider after user authorization.", - "responses": { - "200": { - "description": "OAuth callback processed successfully", - "content": { - "application/json": { - "schema": { - "type": "boolean" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "method": { - "description": "Auth method index", - "type": "number" - }, - "code": { - "description": "OAuth authorization code", - "type": "string" - } - }, - "required": [ - "method" - ] + }, + "env": { + "type": "array", + "items": { + "type": "string" } - } - } - } - } - }, - "/find": { - "get": { - "operationId": "find.text", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { + }, + "id": { "type": "string" - } - }, - { - "in": "query", - "name": "pattern", - "schema": { + }, + "npm": { "type": "string" - }, - "required": true - } - ], - "summary": "Find text", - "description": "Search for text patterns across files in the project using ripgrep.", - "responses": { - "200": { - "description": "Matches", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "type": "object", - "properties": { - "path": { - "type": "object", - "properties": { - "text": { - "type": "string" - } - }, - "required": [ - "text" - ] - }, - "lines": { - "type": "object", - "properties": { - "text": { - "type": "string" - } - }, - "required": [ - "text" - ] - }, - "line_number": { - "type": "number" - }, - "absolute_offset": { - "type": "number" - }, - "submatches": { - "type": "array", - "items": { - "type": "object", - "properties": { - "match": { - "type": "object", - "properties": { - "text": { - "type": "string" - } - }, - "required": [ - "text" - ] - }, - "start": { - "type": "number" - }, - "end": { - "type": "number" - } - }, - "required": [ - "match", - "start", - "end" - ] - } - } + }, + "models": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "family": { + "type": "string" + }, + "release_date": { + "type": "string" + }, + "attachment": { + "type": "boolean" + }, + "reasoning": { + "type": "boolean" + }, + "temperature": { + "type": "boolean" + }, + "tool_call": { + "type": "boolean" + }, + "interleaved": { + "anyOf": [ + { + "type": "boolean", + "const": true + }, + { + "type": "object", + "properties": { + "field": { + "type": "string", + "enum": ["reasoning_content", "reasoning_details"] + } + }, + "required": ["field"], + "additionalProperties": false + } + ] + }, + "cost": { + "type": "object", + "properties": { + "input": { + "type": "number" + }, + "output": { + "type": "number" + }, + "cache_read": { + "type": "number" + }, + "cache_write": { + "type": "number" + }, + "context_over_200k": { + "type": "object", + "properties": { + "input": { + "type": "number" + }, + "output": { + "type": "number" + }, + "cache_read": { + "type": "number" }, - "required": [ - "path", - "lines", - "line_number", - "absolute_offset", - "submatches" - ] + "cache_write": { + "type": "number" + } + }, + "required": ["input", "output"] + } + }, + "required": ["input", "output"] + }, + "limit": { + "type": "object", + "properties": { + "context": { + "type": "number" + }, + "input": { + "type": "number" + }, + "output": { + "type": "number" + } + }, + "required": ["context", "output"] + }, + "modalities": { + "type": "object", + "properties": { + "input": { + "type": "array", + "items": { + "type": "string", + "enum": ["text", "audio", "image", "video", "pdf"] + } + }, + "output": { + "type": "array", + "items": { + "type": "string", + "enum": ["text", "audio", "image", "video", "pdf"] + } + } + }, + "required": ["input", "output"] + }, + "experimental": { + "type": "boolean" + }, + "status": { + "type": "string", + "enum": ["alpha", "beta", "deprecated"] + }, + "options": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "headers": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string" + } + }, + "provider": { + "type": "object", + "properties": { + "npm": { + "type": "string" } + }, + "required": ["npm"] + }, + "variants": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } } + }, + "required": [ + "id", + "name", + "release_date", + "attachment", + "reasoning", + "temperature", + "tool_call", + "limit", + "options" + ] } - } - } - } - } - }, - "/find/file": { - "get": { - "operationId": "find.files", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - }, - { - "in": "query", - "name": "query", - "schema": { - "type": "string" + } }, - "required": true + "required": ["name", "env", "id", "models"] + } }, - { - "in": "query", - "name": "dirs", - "schema": { - "type": "string", - "enum": [ - "true", - "false" - ] - } - }, - { - "in": "query", - "name": "type", - "schema": { - "type": "string", - "enum": [ - "file", - "directory" - ] - } + "default": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string" + } }, - { - "in": "query", - "name": "limit", - "schema": { - "type": "integer", - "minimum": 1, - "maximum": 200 - } - } - ], - "summary": "Find files", - "description": "Search for files or directories by name or pattern in the project directory.", - "responses": { - "200": { - "description": "File paths", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "type": "string" - } - } - } - } + "connected": { + "type": "array", + "items": { + "type": "string" + } } + }, + "required": ["all", "default", "connected"] } + } } - }, - "/find/symbol": { - "get": { - "operationId": "find.symbols", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - }, - { - "in": "query", - "name": "query", - "schema": { - "type": "string" - }, - "required": true - } - ], - "summary": "Find symbols", - "description": "Search for workspace symbols like functions, classes, and variables using LSP.", - "responses": { - "200": { - "description": "Symbols", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Symbol" - } - } - } - } - } - } + } + } + } + }, + "/provider/auth": { + "get": { + "operationId": "provider.auth", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" } - }, - "/file": { - "get": { - "operationId": "file.list", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - }, - { - "in": "query", - "name": "path", - "schema": { - "type": "string" - }, - "required": true - } - ], - "summary": "List files", - "description": "List files and directories in a specified path.", - "responses": { - "200": { - "description": "Files and directories", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/FileNode" - } - } - } - } + } + ], + "summary": "Get provider auth methods", + "description": "Retrieve available authentication methods for all AI providers.", + "responses": { + "200": { + "description": "Provider auth methods", + "content": { + "application/json": { + "schema": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ProviderAuthMethod" } + } } + } } - }, - "/file/content": { - "get": { - "operationId": "file.read", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - }, - { - "in": "query", - "name": "path", - "schema": { - "type": "string" - }, - "required": true - } - ], - "summary": "Read file", - "description": "Read the content of a specified file.", - "responses": { - "200": { - "description": "File content", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/FileContent" - } - } - } - } + } + } + } + }, + "/provider/{providerID}/oauth/authorize": { + "post": { + "operationId": "provider.oauth.authorize", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "providerID", + "schema": { + "type": "string" + }, + "required": true, + "description": "Provider ID" + } + ], + "summary": "OAuth authorize", + "description": "Initiate OAuth authorization for a specific AI provider to get an authorization URL.", + "responses": { + "200": { + "description": "Authorization URL and method", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProviderAuthAuthorization" } + } } - }, - "/file/status": { - "get": { - "operationId": "file.status", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - } - ], - "summary": "Get file status", - "description": "Get the git status of all files in the project.", - "responses": { - "200": { - "description": "File status", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/File" - } - } - } - } - } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" } + } } + } }, - "/mcp": { - "get": { - "operationId": "mcp.status", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - } - ], - "summary": "Get MCP status", - "description": "Get the status of all Model Context Protocol (MCP) servers.", - "responses": { - "200": { - "description": "MCP server status", - "content": { - "application/json": { - "schema": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": { - "$ref": "#/components/schemas/MCPStatus" - } - } - } - } - } - } - }, - "post": { - "operationId": "mcp.add", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - } - ], - "summary": "Add MCP server", - "description": "Dynamically add a new Model Context Protocol (MCP) server to the system.", - "responses": { - "200": { - "description": "MCP server added successfully", - "content": { - "application/json": { - "schema": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": { - "$ref": "#/components/schemas/MCPStatus" - } - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - } + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "method": { + "description": "Auth method index", + "type": "number" + } }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "config": { - "anyOf": [ - { - "$ref": "#/components/schemas/McpLocalConfig" - }, - { - "$ref": "#/components/schemas/McpRemoteConfig" - } - ] - } - }, - "required": [ - "name", - "config" - ] - } - } - } + "required": ["method"] + } + } + } + } + } + }, + "/provider/{providerID}/oauth/callback": { + "post": { + "operationId": "provider.oauth.callback", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "providerID", + "schema": { + "type": "string" + }, + "required": true, + "description": "Provider ID" + } + ], + "summary": "OAuth callback", + "description": "Handle the OAuth callback from a provider after user authorization.", + "responses": { + "200": { + "description": "OAuth callback processed successfully", + "content": { + "application/json": { + "schema": { + "type": "boolean" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" } + } } + } }, - "/mcp/{name}/auth": { - "post": { - "operationId": "mcp.auth.start", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "method": { + "description": "Auth method index", + "type": "number" + }, + "code": { + "description": "OAuth authorization code", + "type": "string" + } + }, + "required": ["method"] + } + } + } + } + } + }, + "/find": { + "get": { + "operationId": "find.text", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "pattern", + "schema": { + "type": "string" + }, + "required": true + } + ], + "summary": "Find text", + "description": "Search for text patterns across files in the project using ripgrep.", + "responses": { + "200": { + "description": "Matches", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "path": { + "type": "object", + "properties": { + "text": { "type": "string" - } - }, - { - "schema": { + } + }, + "required": ["text"] + }, + "lines": { + "type": "object", + "properties": { + "text": { "type": "string" + } }, - "in": "path", - "name": "name", - "required": true - } - ], - "summary": "Start MCP OAuth", - "description": "Start OAuth authentication flow for a Model Context Protocol (MCP) server.", - "responses": { - "200": { - "description": "OAuth flow started", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "authorizationUrl": { - "description": "URL to open in browser for authorization", - "type": "string" - } - }, - "required": [ - "authorizationUrl" - ] - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" + "required": ["text"] + }, + "line_number": { + "type": "number" + }, + "absolute_offset": { + "type": "number" + }, + "submatches": { + "type": "array", + "items": { + "type": "object", + "properties": { + "match": { + "type": "object", + "properties": { + "text": { + "type": "string" } + }, + "required": ["text"] + }, + "start": { + "type": "number" + }, + "end": { + "type": "number" } + }, + "required": ["match", "start", "end"] } + } }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NotFoundError" - } - } - } - } + "required": ["path", "lines", "line_number", "absolute_offset", "submatches"] + } } - }, - "delete": { - "operationId": "mcp.auth.remove", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - }, - { - "schema": { - "type": "string" - }, - "in": "path", - "name": "name", - "required": true - } - ], - "summary": "Remove MCP OAuth", - "description": "Remove OAuth credentials for an MCP server", - "responses": { - "200": { - "description": "OAuth credentials removed", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "success": { - "type": "boolean", - "const": true - } - }, - "required": [ - "success" - ] - } - } - } - }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NotFoundError" - } - } - } - } + } + } + } + } + } + }, + "/find/file": { + "get": { + "operationId": "find.files", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "query", + "schema": { + "type": "string" + }, + "required": true + }, + { + "in": "query", + "name": "dirs", + "schema": { + "type": "string", + "enum": ["true", "false"] + } + }, + { + "in": "query", + "name": "type", + "schema": { + "type": "string", + "enum": ["file", "directory"] + } + }, + { + "in": "query", + "name": "limit", + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 200 + } + } + ], + "summary": "Find files", + "description": "Search for files or directories by name or pattern in the project directory.", + "responses": { + "200": { + "description": "File paths", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "string" + } } + } } - }, - "/mcp/{name}/auth/callback": { - "post": { - "operationId": "mcp.auth.callback", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - }, - { - "schema": { - "type": "string" - }, - "in": "path", - "name": "name", - "required": true - } - ], - "summary": "Complete MCP OAuth", - "description": "Complete OAuth authentication for a Model Context Protocol (MCP) server using the authorization code.", - "responses": { - "200": { - "description": "OAuth authentication completed", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MCPStatus" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NotFoundError" - } - } - } - } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "code": { - "description": "Authorization code from OAuth callback", - "type": "string" - } - }, - "required": [ - "code" - ] - } - } - } + } + } + } + }, + "/find/symbol": { + "get": { + "operationId": "find.symbols", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "query", + "schema": { + "type": "string" + }, + "required": true + } + ], + "summary": "Find symbols", + "description": "Search for workspace symbols like functions, classes, and variables using LSP.", + "responses": { + "200": { + "description": "Symbols", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Symbol" + } } + } } - }, - "/mcp/{name}/auth/authenticate": { - "post": { - "operationId": "mcp.auth.authenticate", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - }, - { - "schema": { - "type": "string" - }, - "in": "path", - "name": "name", - "required": true - } - ], - "summary": "Authenticate MCP OAuth", - "description": "Start OAuth flow and wait for callback (opens browser)", - "responses": { - "200": { - "description": "OAuth authentication completed", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/MCPStatus" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NotFoundError" - } - } - } - } + } + } + } + }, + "/file": { + "get": { + "operationId": "file.list", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "summary": "List files", + "description": "List files and directories in a specified path.", + "responses": { + "200": { + "description": "Files and directories", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FileNode" + } } + } } - }, - "/mcp/{name}/connect": { - "post": { - "operationId": "mcp.connect", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - }, - { - "in": "path", - "name": "name", - "schema": { - "type": "string" - }, - "required": true - } - ], - "description": "Connect an MCP server", - "responses": { - "200": { - "description": "MCP server connected successfully", - "content": { - "application/json": { - "schema": { - "type": "boolean" - } - } - } - } + } + } + } + }, + "/file/content": { + "get": { + "operationId": "file.read", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + }, + { + "in": "query", + "name": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "summary": "Read file", + "description": "Read the content of a specified file.", + "responses": { + "200": { + "description": "File content", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileContent" } + } } - }, - "/mcp/{name}/disconnect": { - "post": { - "operationId": "mcp.disconnect", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - }, - { - "in": "path", - "name": "name", - "schema": { - "type": "string" - }, - "required": true - } - ], - "description": "Disconnect an MCP server", - "responses": { - "200": { - "description": "MCP server disconnected successfully", - "content": { - "application/json": { - "schema": { - "type": "boolean" - } - } - } - } + } + } + } + }, + "/file/status": { + "get": { + "operationId": "file.status", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + } + ], + "summary": "Get file status", + "description": "Get the git status of all files in the project.", + "responses": { + "200": { + "description": "File status", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/File" + } } + } } - }, - "/tui/append-prompt": { - "post": { - "operationId": "tui.appendPrompt", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - } - ], - "summary": "Append TUI prompt", - "description": "Append prompt to the TUI", - "responses": { - "200": { - "description": "Prompt processed successfully", - "content": { - "application/json": { - "schema": { - "type": "boolean" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "text": { - "type": "string" - } - }, - "required": [ - "text" - ] - } - } - } + } + } + } + }, + "/mcp": { + "get": { + "operationId": "mcp.status", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + } + ], + "summary": "Get MCP status", + "description": "Get the status of all Model Context Protocol (MCP) servers.", + "responses": { + "200": { + "description": "MCP server status", + "content": { + "application/json": { + "schema": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "$ref": "#/components/schemas/MCPStatus" + } } + } } - }, - "/tui/open-help": { - "post": { - "operationId": "tui.openHelp", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - } - ], - "summary": "Open help dialog", - "description": "Open the help dialog in the TUI to display user assistance information.", - "responses": { - "200": { - "description": "Help dialog opened successfully", - "content": { - "application/json": { - "schema": { - "type": "boolean" - } - } - } - } + } + } + }, + "post": { + "operationId": "mcp.add", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + } + ], + "summary": "Add MCP server", + "description": "Dynamically add a new Model Context Protocol (MCP) server to the system.", + "responses": { + "200": { + "description": "MCP server added successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "$ref": "#/components/schemas/MCPStatus" + } } + } } - }, - "/tui/open-sessions": { - "post": { - "operationId": "tui.openSessions", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - } - ], - "summary": "Open sessions dialog", - "description": "Open the session dialog", - "responses": { - "200": { - "description": "Session dialog opened successfully", - "content": { - "application/json": { - "schema": { - "type": "boolean" - } - } - } - } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" } + } } + } }, - "/tui/open-themes": { - "post": { - "operationId": "tui.openThemes", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - } - ], - "summary": "Open themes dialog", - "description": "Open the theme dialog", - "responses": { - "200": { - "description": "Theme dialog opened successfully", - "content": { - "application/json": { - "schema": { - "type": "boolean" - } - } - } - } + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "config": { + "anyOf": [ + { + "$ref": "#/components/schemas/McpLocalConfig" + }, + { + "$ref": "#/components/schemas/McpRemoteConfig" + } + ] + } + }, + "required": ["name", "config"] + } + } + } + } + } + }, + "/mcp/{name}/auth": { + "post": { + "operationId": "mcp.auth.start", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + }, + { + "schema": { + "type": "string" + }, + "in": "path", + "name": "name", + "required": true + } + ], + "summary": "Start MCP OAuth", + "description": "Start OAuth authentication flow for a Model Context Protocol (MCP) server.", + "responses": { + "200": { + "description": "OAuth flow started", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "authorizationUrl": { + "description": "URL to open in browser for authorization", + "type": "string" + } + }, + "required": ["authorizationUrl"] } + } } - }, - "/tui/open-models": { - "post": { - "operationId": "tui.openModels", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - } - ], - "summary": "Open models dialog", - "description": "Open the model dialog", - "responses": { - "200": { - "description": "Model dialog opened successfully", - "content": { - "application/json": { - "schema": { - "type": "boolean" - } - } - } - } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" } + } } - }, - "/tui/submit-prompt": { - "post": { - "operationId": "tui.submitPrompt", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - } - ], - "summary": "Submit TUI prompt", - "description": "Submit the prompt", - "responses": { - "200": { - "description": "Prompt submitted successfully", - "content": { - "application/json": { - "schema": { - "type": "boolean" - } - } - } - } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundError" } + } } - }, - "/tui/clear-prompt": { - "post": { - "operationId": "tui.clearPrompt", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - } - ], - "summary": "Clear TUI prompt", - "description": "Clear the prompt", - "responses": { - "200": { - "description": "Prompt cleared successfully", - "content": { - "application/json": { - "schema": { - "type": "boolean" - } - } - } + } + } + }, + "delete": { + "operationId": "mcp.auth.remove", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + }, + { + "schema": { + "type": "string" + }, + "in": "path", + "name": "name", + "required": true + } + ], + "summary": "Remove MCP OAuth", + "description": "Remove OAuth credentials for an MCP server", + "responses": { + "200": { + "description": "OAuth credentials removed", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "const": true } + }, + "required": ["success"] } + } } - }, - "/tui/execute-command": { - "post": { - "operationId": "tui.executeCommand", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - } - ], - "summary": "Execute TUI command", - "description": "Execute a TUI command (e.g. agent_cycle)", - "responses": { - "200": { - "description": "Command executed successfully", - "content": { - "application/json": { - "schema": { - "type": "boolean" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "command": { - "type": "string" - } - }, - "required": [ - "command" - ] - } - } - } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundError" } + } } - }, - "/tui/show-toast": { - "post": { - "operationId": "tui.showToast", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - } - ], - "summary": "Show TUI toast", - "description": "Show a toast notification in the TUI", - "responses": { - "200": { - "description": "Toast notification shown successfully", - "content": { - "application/json": { - "schema": { - "type": "boolean" - } - } - } - } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "title": { - "type": "string" - }, - "message": { - "type": "string" - }, - "variant": { - "type": "string", - "enum": [ - "info", - "success", - "warning", - "error" - ] - }, - "duration": { - "description": "Duration in milliseconds", - "default": 5000, - "type": "number" - } - }, - "required": [ - "message", - "variant" - ] - } - } - } + } + } + } + }, + "/mcp/{name}/auth/callback": { + "post": { + "operationId": "mcp.auth.callback", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + }, + { + "schema": { + "type": "string" + }, + "in": "path", + "name": "name", + "required": true + } + ], + "summary": "Complete MCP OAuth", + "description": "Complete OAuth authentication for a Model Context Protocol (MCP) server using the authorization code.", + "responses": { + "200": { + "description": "OAuth authentication completed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MCPStatus" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundError" } + } } + } }, - "/tui/publish": { - "post": { - "operationId": "tui.publish", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - } - ], - "summary": "Publish TUI event", - "description": "Publish a TUI event", - "responses": { - "200": { - "description": "Event published successfully", - "content": { - "application/json": { - "schema": { - "type": "boolean" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - } + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "code": { + "description": "Authorization code from OAuth callback", + "type": "string" + } }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "anyOf": [ - { - "$ref": "#/components/schemas/Event.tui.prompt.append" - }, - { - "$ref": "#/components/schemas/Event.tui.command.execute" - }, - { - "$ref": "#/components/schemas/Event.tui.toast.show" - }, - { - "$ref": "#/components/schemas/Event.tui.session.select" - } - ] - } - } - } - } + "required": ["code"] + } } - }, - "/tui/select-session": { - "post": { - "operationId": "tui.selectSession", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - } - ], - "summary": "Select session", - "description": "Navigate the TUI to display the specified session.", - "responses": { - "200": { - "description": "Session selected successfully", - "content": { - "application/json": { - "schema": { - "type": "boolean" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - }, - "404": { - "description": "Not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/NotFoundError" - } - } - } - } - }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "sessionID": { - "description": "Session ID to navigate to", - "type": "string", - "pattern": "^ses" - } - }, - "required": [ - "sessionID" - ] - } - } - } + } + } + } + }, + "/mcp/{name}/auth/authenticate": { + "post": { + "operationId": "mcp.auth.authenticate", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + }, + { + "schema": { + "type": "string" + }, + "in": "path", + "name": "name", + "required": true + } + ], + "summary": "Authenticate MCP OAuth", + "description": "Start OAuth flow and wait for callback (opens browser)", + "responses": { + "200": { + "description": "OAuth authentication completed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MCPStatus" } + } } - }, - "/tui/control/next": { - "get": { - "operationId": "tui.control.next", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - } - ], - "summary": "Get next TUI request", - "description": "Retrieve the next TUI (Terminal User Interface) request from the queue for processing.", - "responses": { - "200": { - "description": "Next TUI request", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "path": { - "type": "string" - }, - "body": {} - }, - "required": [ - "path", - "body" - ] - } - } - } - } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" } + } } - }, - "/tui/control/response": { - "post": { - "operationId": "tui.control.response", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - } - ], - "summary": "Submit TUI response", - "description": "Submit a response to the TUI request queue to complete a pending request.", - "responses": { - "200": { - "description": "Response submitted successfully", - "content": { - "application/json": { - "schema": { - "type": "boolean" - } - } - } - } - }, - "requestBody": { - "content": { - "application/json": { - "schema": {} - } - } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundError" } + } } - }, - "/instance/dispose": { - "post": { - "operationId": "instance.dispose", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - } - ], - "summary": "Dispose instance", - "description": "Clean up and dispose the current OpenCode instance, releasing all resources.", - "responses": { - "200": { - "description": "Instance disposed", - "content": { - "application/json": { - "schema": { - "type": "boolean" - } - } - } - } + } + } + } + }, + "/mcp/{name}/connect": { + "post": { + "operationId": "mcp.connect", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "name", + "schema": { + "type": "string" + }, + "required": true + } + ], + "description": "Connect an MCP server", + "responses": { + "200": { + "description": "MCP server connected successfully", + "content": { + "application/json": { + "schema": { + "type": "boolean" } + } } - }, - "/path": { - "get": { - "operationId": "path.get", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - } - ], - "summary": "Get paths", - "description": "Retrieve the current working directory and related path information for the OpenCode instance.", - "responses": { - "200": { - "description": "Path", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Path" - } - } - } - } + } + } + } + }, + "/mcp/{name}/disconnect": { + "post": { + "operationId": "mcp.disconnect", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "name", + "schema": { + "type": "string" + }, + "required": true + } + ], + "description": "Disconnect an MCP server", + "responses": { + "200": { + "description": "MCP server disconnected successfully", + "content": { + "application/json": { + "schema": { + "type": "boolean" } + } } - }, - "/vcs": { - "get": { - "operationId": "vcs.get", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - } - ], - "summary": "Get VCS info", - "description": "Retrieve version control system (VCS) information for the current project, such as git branch.", - "responses": { - "200": { - "description": "VCS info", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/VcsInfo" - } - } - } - } + } + } + } + }, + "/tui/append-prompt": { + "post": { + "operationId": "tui.appendPrompt", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + } + ], + "summary": "Append TUI prompt", + "description": "Append prompt to the TUI", + "responses": { + "200": { + "description": "Prompt processed successfully", + "content": { + "application/json": { + "schema": { + "type": "boolean" } + } } - }, - "/command": { - "get": { - "operationId": "command.list", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - } - ], - "summary": "List commands", - "description": "Get a list of all available commands in the OpenCode system.", - "responses": { - "200": { - "description": "List of commands", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Command" - } - } - } - } - } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" } + } } + } }, - "/log": { - "post": { - "operationId": "app.log", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - } - ], - "summary": "Write log", - "description": "Write a log entry to the server logs with specified level and metadata.", - "responses": { - "200": { - "description": "Log entry written successfully", - "content": { - "application/json": { - "schema": { - "type": "boolean" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - } + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "text": { + "type": "string" + } }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "service": { - "description": "Service name for the log entry", - "type": "string" - }, - "level": { - "description": "Log level", - "type": "string", - "enum": [ - "debug", - "info", - "error", - "warn" - ] - }, - "message": { - "description": "Log message", - "type": "string" - }, - "extra": { - "description": "Additional metadata for the log entry", - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": {} - } - }, - "required": [ - "service", - "level", - "message" - ] - } - } - } + "required": ["text"] + } + } + } + } + } + }, + "/tui/open-help": { + "post": { + "operationId": "tui.openHelp", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + } + ], + "summary": "Open help dialog", + "description": "Open the help dialog in the TUI to display user assistance information.", + "responses": { + "200": { + "description": "Help dialog opened successfully", + "content": { + "application/json": { + "schema": { + "type": "boolean" } + } } - }, - "/agent": { - "get": { - "operationId": "app.agents", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - } - ], - "summary": "List agents", - "description": "Get a list of all available AI agents in the OpenCode system.", - "responses": { - "200": { - "description": "List of agents", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Agent" - } - } - } - } - } + } + } + } + }, + "/tui/open-sessions": { + "post": { + "operationId": "tui.openSessions", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + } + ], + "summary": "Open sessions dialog", + "description": "Open the session dialog", + "responses": { + "200": { + "description": "Session dialog opened successfully", + "content": { + "application/json": { + "schema": { + "type": "boolean" } + } } - }, - "/skill": { - "get": { - "operationId": "app.skills", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - } - ], - "summary": "List skills", - "description": "Get a list of all available skills in the OpenCode system.", - "responses": { - "200": { - "description": "List of skills", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "description": { - "type": "string" - }, - "location": { - "type": "string" - } - }, - "required": [ - "name", - "description", - "location" - ] - } - } - } - } - } + } + } + } + }, + "/tui/open-themes": { + "post": { + "operationId": "tui.openThemes", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + } + ], + "summary": "Open themes dialog", + "description": "Open the theme dialog", + "responses": { + "200": { + "description": "Theme dialog opened successfully", + "content": { + "application/json": { + "schema": { + "type": "boolean" } + } } - }, - "/lsp": { - "get": { - "operationId": "lsp.status", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - } - ], - "summary": "Get LSP status", - "description": "Get LSP server status", - "responses": { - "200": { - "description": "LSP server status", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/LSPStatus" - } - } - } - } - } + } + } + } + }, + "/tui/open-models": { + "post": { + "operationId": "tui.openModels", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + } + ], + "summary": "Open models dialog", + "description": "Open the model dialog", + "responses": { + "200": { + "description": "Model dialog opened successfully", + "content": { + "application/json": { + "schema": { + "type": "boolean" } + } } - }, - "/formatter": { - "get": { - "operationId": "formatter.status", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - } - ], - "summary": "Get formatter status", - "description": "Get formatter status", - "responses": { - "200": { - "description": "Formatter status", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/FormatterStatus" - } - } - } - } - } + } + } + } + }, + "/tui/submit-prompt": { + "post": { + "operationId": "tui.submitPrompt", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + } + ], + "summary": "Submit TUI prompt", + "description": "Submit the prompt", + "responses": { + "200": { + "description": "Prompt submitted successfully", + "content": { + "application/json": { + "schema": { + "type": "boolean" + } + } + } + } + } + } + }, + "/tui/clear-prompt": { + "post": { + "operationId": "tui.clearPrompt", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + } + ], + "summary": "Clear TUI prompt", + "description": "Clear the prompt", + "responses": { + "200": { + "description": "Prompt cleared successfully", + "content": { + "application/json": { + "schema": { + "type": "boolean" } + } } + } + } + } + }, + "/tui/execute-command": { + "post": { + "operationId": "tui.executeCommand", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + } + ], + "summary": "Execute TUI command", + "description": "Execute a TUI command (e.g. agent_cycle)", + "responses": { + "200": { + "description": "Command executed successfully", + "content": { + "application/json": { + "schema": { + "type": "boolean" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } + } }, - "/auth/{providerID}": { - "put": { - "operationId": "auth.set", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - }, - { - "in": "path", - "name": "providerID", - "schema": { - "type": "string" - }, - "required": true - } - ], - "summary": "Set auth credentials", - "description": "Set authentication credentials", - "responses": { - "200": { - "description": "Successfully set authentication credentials", - "content": { - "application/json": { - "schema": { - "type": "boolean" - } - } - } - }, - "400": { - "description": "Bad request", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/BadRequestError" - } - } - } - } + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "command": { + "type": "string" + } }, - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Auth" - } - } - } + "required": ["command"] + } + } + } + } + } + }, + "/tui/show-toast": { + "post": { + "operationId": "tui.showToast", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + } + ], + "summary": "Show TUI toast", + "description": "Show a toast notification in the TUI", + "responses": { + "200": { + "description": "Toast notification shown successfully", + "content": { + "application/json": { + "schema": { + "type": "boolean" } + } } + } }, - "/event": { - "get": { - "operationId": "event.subscribe", - "parameters": [ - { - "in": "query", - "name": "directory", - "schema": { - "type": "string" - } - } - ], - "summary": "Subscribe to events", - "description": "Get events", - "responses": { - "200": { - "description": "Event stream", - "content": { - "text/event-stream": { - "schema": { - "$ref": "#/components/schemas/Event" - } - } - } - } + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "message": { + "type": "string" + }, + "variant": { + "type": "string", + "enum": ["info", "success", "warning", "error"] + }, + "duration": { + "description": "Duration in milliseconds", + "default": 5000, + "type": "number" + } + }, + "required": ["message", "variant"] + } + } + } + } + } + }, + "/tui/publish": { + "post": { + "operationId": "tui.publish", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + } + ], + "summary": "Publish TUI event", + "description": "Publish a TUI event", + "responses": { + "200": { + "description": "Event published successfully", + "content": { + "application/json": { + "schema": { + "type": "boolean" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "$ref": "#/components/schemas/Event.tui.prompt.append" + }, + { + "$ref": "#/components/schemas/Event.tui.command.execute" + }, + { + "$ref": "#/components/schemas/Event.tui.toast.show" + }, + { + "$ref": "#/components/schemas/Event.tui.session.select" + } + ] + } } + } } + } }, - "components": { - "schemas": { - "Event.server.connected": { + "/tui/select-session": { + "post": { + "operationId": "tui.selectSession", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + } + ], + "summary": "Select session", + "description": "Navigate the TUI to display the specified session.", + "responses": { + "200": { + "description": "Session selected successfully", + "content": { + "application/json": { + "schema": { + "type": "boolean" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundError" + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { "type": "object", "properties": { - "type": { - "type": "string", - "const": "server.connected" + "sessionID": { + "description": "Session ID to navigate to", + "type": "string", + "pattern": "^ses" + } + }, + "required": ["sessionID"] + } + } + } + } + } + }, + "/tui/control/next": { + "get": { + "operationId": "tui.control.next", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + } + ], + "summary": "Get next TUI request", + "description": "Retrieve the next TUI (Terminal User Interface) request from the queue for processing.", + "responses": { + "200": { + "description": "Next TUI request", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "path": { + "type": "string" }, - "properties": { - "type": "object", - "properties": {} - } - }, - "required": [ - "type", - "properties" - ] - }, - "Event.global.disposed": { + "body": {} + }, + "required": ["path", "body"] + } + } + } + } + } + } + }, + "/tui/control/response": { + "post": { + "operationId": "tui.control.response", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + } + ], + "summary": "Submit TUI response", + "description": "Submit a response to the TUI request queue to complete a pending request.", + "responses": { + "200": { + "description": "Response submitted successfully", + "content": { + "application/json": { + "schema": { + "type": "boolean" + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": {} + } + } + } + } + }, + "/instance/dispose": { + "post": { + "operationId": "instance.dispose", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + } + ], + "summary": "Dispose instance", + "description": "Clean up and dispose the current OpenCode instance, releasing all resources.", + "responses": { + "200": { + "description": "Instance disposed", + "content": { + "application/json": { + "schema": { + "type": "boolean" + } + } + } + } + } + } + }, + "/path": { + "get": { + "operationId": "path.get", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + } + ], + "summary": "Get paths", + "description": "Retrieve the current working directory and related path information for the OpenCode instance.", + "responses": { + "200": { + "description": "Path", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Path" + } + } + } + } + } + } + }, + "/vcs": { + "get": { + "operationId": "vcs.get", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + } + ], + "summary": "Get VCS info", + "description": "Retrieve version control system (VCS) information for the current project, such as git branch.", + "responses": { + "200": { + "description": "VCS info", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VcsInfo" + } + } + } + } + } + } + }, + "/command": { + "get": { + "operationId": "command.list", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + } + ], + "summary": "List commands", + "description": "Get a list of all available commands in the OpenCode system.", + "responses": { + "200": { + "description": "List of commands", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Command" + } + } + } + } + } + } + } + }, + "/log": { + "post": { + "operationId": "app.log", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + } + ], + "summary": "Write log", + "description": "Write a log entry to the server logs with specified level and metadata.", + "responses": { + "200": { + "description": "Log entry written successfully", + "content": { + "application/json": { + "schema": { + "type": "boolean" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { "type": "object", "properties": { - "type": { - "type": "string", - "const": "global.disposed" - }, + "service": { + "description": "Service name for the log entry", + "type": "string" + }, + "level": { + "description": "Log level", + "type": "string", + "enum": ["debug", "info", "error", "warn"] + }, + "message": { + "description": "Log message", + "type": "string" + }, + "extra": { + "description": "Additional metadata for the log entry", + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "required": ["service", "level", "message"] + } + } + } + } + } + }, + "/agent": { + "get": { + "operationId": "app.agents", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + } + ], + "summary": "List agents", + "description": "Get a list of all available AI agents in the OpenCode system.", + "responses": { + "200": { + "description": "List of agents", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Agent" + } + } + } + } + } + } + } + }, + "/skill": { + "get": { + "operationId": "app.skills", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + } + ], + "summary": "List skills", + "description": "Get a list of all available skills in the OpenCode system.", + "responses": { + "200": { + "description": "List of skills", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", "properties": { - "type": "object", - "properties": {} - } - }, - "required": [ - "type", - "properties" - ] - }, - "Event.tui.prompt.append": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "tui.prompt.append" + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "location": { + "type": "string" + } }, - "properties": { - "type": "object", - "properties": { - "text": { - "type": "string" - } - }, - "required": [ - "text" - ] - } - }, - "required": [ - "type", - "properties" + "required": ["name", "description", "location"] + } + } + } + } + } + } + } + }, + "/lsp": { + "get": { + "operationId": "lsp.status", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + } + ], + "summary": "Get LSP status", + "description": "Get LSP server status", + "responses": { + "200": { + "description": "LSP server status", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/LSPStatus" + } + } + } + } + } + } + } + }, + "/formatter": { + "get": { + "operationId": "formatter.status", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + } + ], + "summary": "Get formatter status", + "description": "Get formatter status", + "responses": { + "200": { + "description": "Formatter status", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FormatterStatus" + } + } + } + } + } + } + } + }, + "/auth/{providerID}": { + "put": { + "operationId": "auth.set", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "providerID", + "schema": { + "type": "string" + }, + "required": true + } + ], + "summary": "Set auth credentials", + "description": "Set authentication credentials", + "responses": { + "200": { + "description": "Successfully set authentication credentials", + "content": { + "application/json": { + "schema": { + "type": "boolean" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } + } + }, + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Auth" + } + } + } + } + } + }, + "/event": { + "get": { + "operationId": "event.subscribe", + "parameters": [ + { + "in": "query", + "name": "directory", + "schema": { + "type": "string" + } + } + ], + "summary": "Subscribe to events", + "description": "Get events", + "responses": { + "200": { + "description": "Event stream", + "content": { + "text/event-stream": { + "schema": { + "$ref": "#/components/schemas/Event" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "Event.server.connected": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "server.connected" + }, + "properties": { + "type": "object", + "properties": {} + } + }, + "required": ["type", "properties"] + }, + "Event.global.disposed": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "global.disposed" + }, + "properties": { + "type": "object", + "properties": {} + } + }, + "required": ["type", "properties"] + }, + "Event.tui.prompt.append": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "tui.prompt.append" + }, + "properties": { + "type": "object", + "properties": { + "text": { + "type": "string" + } + }, + "required": ["text"] + } + }, + "required": ["type", "properties"] + }, + "Event.tui.command.execute": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "tui.command.execute" + }, + "properties": { + "type": "object", + "properties": { + "command": { + "anyOf": [ + { + "type": "string", + "enum": [ + "session.list", + "session.new", + "session.share", + "session.interrupt", + "session.compact", + "session.page.up", + "session.page.down", + "session.line.up", + "session.line.down", + "session.half.page.up", + "session.half.page.down", + "session.first", + "session.last", + "prompt.clear", + "prompt.submit", + "agent.cycle" + ] + }, + { + "type": "string" + } ] + } }, - "Event.tui.command.execute": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "tui.command.execute" - }, - "properties": { - "type": "object", - "properties": { - "command": { - "anyOf": [ - { - "type": "string", - "enum": [ - "session.list", - "session.new", - "session.share", - "session.interrupt", - "session.compact", - "session.page.up", - "session.page.down", - "session.line.up", - "session.line.down", - "session.half.page.up", - "session.half.page.down", - "session.first", - "session.last", - "prompt.clear", - "prompt.submit", - "agent.cycle" - ] - }, - { - "type": "string" - } - ] - } - }, - "required": [ - "command" - ] - } - }, - "required": [ - "type", - "properties" - ] + "required": ["command"] + } + }, + "required": ["type", "properties"] + }, + "Event.tui.toast.show": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "tui.toast.show" + }, + "properties": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "message": { + "type": "string" + }, + "variant": { + "type": "string", + "enum": ["info", "success", "warning", "error"] + }, + "duration": { + "description": "Duration in milliseconds", + "default": 5000, + "type": "number" + } + }, + "required": ["message", "variant"] + } + }, + "required": ["type", "properties"] + }, + "Event.tui.session.select": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "tui.session.select" + }, + "properties": { + "type": "object", + "properties": { + "sessionID": { + "description": "Session ID to navigate to", + "type": "string", + "pattern": "^ses" + } }, - "Event.tui.toast.show": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "tui.toast.show" - }, - "properties": { - "type": "object", - "properties": { - "title": { - "type": "string" - }, - "message": { - "type": "string" - }, - "variant": { - "type": "string", - "enum": [ - "info", - "success", - "warning", - "error" - ] - }, - "duration": { - "description": "Duration in milliseconds", - "default": 5000, - "type": "number" - } - }, - "required": [ - "message", - "variant" - ] - } - }, - "required": [ - "type", - "properties" + "required": ["sessionID"] + } + }, + "required": ["type", "properties"] + }, + "Event.installation.updated": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "installation.updated" + }, + "properties": { + "type": "object", + "properties": { + "version": { + "type": "string" + } + }, + "required": ["version"] + } + }, + "required": ["type", "properties"] + }, + "Event.installation.update-available": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "installation.update-available" + }, + "properties": { + "type": "object", + "properties": { + "version": { + "type": "string" + } + }, + "required": ["version"] + } + }, + "required": ["type", "properties"] + }, + "Project": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "worktree": { + "type": "string" + }, + "vcs": { + "type": "string", + "const": "git" + }, + "name": { + "type": "string" + }, + "icon": { + "type": "object", + "properties": { + "url": { + "type": "string" + }, + "override": { + "type": "string" + }, + "color": { + "type": "string" + } + } + }, + "commands": { + "type": "object", + "properties": { + "start": { + "description": "Startup script to run when creating a new workspace (worktree)", + "type": "string" + } + } + }, + "time": { + "type": "object", + "properties": { + "created": { + "type": "number" + }, + "updated": { + "type": "number" + }, + "initialized": { + "type": "number" + } + }, + "required": ["created", "updated"] + }, + "sandboxes": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": ["id", "worktree", "time", "sandboxes"] + }, + "Event.project.updated": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "project.updated" + }, + "properties": { + "$ref": "#/components/schemas/Project" + } + }, + "required": ["type", "properties"] + }, + "Event.server.instance.disposed": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "server.instance.disposed" + }, + "properties": { + "type": "object", + "properties": { + "directory": { + "type": "string" + } + }, + "required": ["directory"] + } + }, + "required": ["type", "properties"] + }, + "Event.file.edited": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "file.edited" + }, + "properties": { + "type": "object", + "properties": { + "file": { + "type": "string" + } + }, + "required": ["file"] + } + }, + "required": ["type", "properties"] + }, + "Event.worktree.ready": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "worktree.ready" + }, + "properties": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "branch": { + "type": "string" + } + }, + "required": ["name", "branch"] + } + }, + "required": ["type", "properties"] + }, + "Event.worktree.failed": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "worktree.failed" + }, + "properties": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": ["message"] + } + }, + "required": ["type", "properties"] + }, + "Event.lsp.client.diagnostics": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "lsp.client.diagnostics" + }, + "properties": { + "type": "object", + "properties": { + "serverID": { + "type": "string" + }, + "path": { + "type": "string" + } + }, + "required": ["serverID", "path"] + } + }, + "required": ["type", "properties"] + }, + "PermissionRequest": { + "type": "object", + "properties": { + "id": { + "type": "string", + "pattern": "^per.*" + }, + "sessionID": { + "type": "string", + "pattern": "^ses.*" + }, + "permission": { + "type": "string" + }, + "patterns": { + "type": "array", + "items": { + "type": "string" + } + }, + "metadata": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "always": { + "type": "array", + "items": { + "type": "string" + } + }, + "tool": { + "type": "object", + "properties": { + "messageID": { + "type": "string" + }, + "callID": { + "type": "string" + } + }, + "required": ["messageID", "callID"] + } + }, + "required": ["id", "sessionID", "permission", "patterns", "metadata", "always"] + }, + "Event.permission.asked": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "permission.asked" + }, + "properties": { + "$ref": "#/components/schemas/PermissionRequest" + } + }, + "required": ["type", "properties"] + }, + "Event.permission.replied": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "permission.replied" + }, + "properties": { + "type": "object", + "properties": { + "sessionID": { + "type": "string" + }, + "requestID": { + "type": "string" + }, + "reply": { + "type": "string", + "enum": ["once", "always", "reject"] + } + }, + "required": ["sessionID", "requestID", "reply"] + } + }, + "required": ["type", "properties"] + }, + "SessionStatus": { + "anyOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "idle" + } + }, + "required": ["type"] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "retry" + }, + "attempt": { + "type": "number" + }, + "message": { + "type": "string" + }, + "next": { + "type": "number" + } + }, + "required": ["type", "attempt", "message", "next"] + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "busy" + } + }, + "required": ["type"] + } + ] + }, + "Event.session.status": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "session.status" + }, + "properties": { + "type": "object", + "properties": { + "sessionID": { + "type": "string" + }, + "status": { + "$ref": "#/components/schemas/SessionStatus" + } + }, + "required": ["sessionID", "status"] + } + }, + "required": ["type", "properties"] + }, + "Event.session.idle": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "session.idle" + }, + "properties": { + "type": "object", + "properties": { + "sessionID": { + "type": "string" + } + }, + "required": ["sessionID"] + } + }, + "required": ["type", "properties"] + }, + "QuestionOption": { + "type": "object", + "properties": { + "label": { + "description": "Display text (1-5 words, concise)", + "type": "string" + }, + "description": { + "description": "Explanation of choice", + "type": "string" + } + }, + "required": ["label", "description"] + }, + "QuestionInfo": { + "type": "object", + "properties": { + "question": { + "description": "Complete question", + "type": "string" + }, + "header": { + "description": "Very short label (max 30 chars)", + "type": "string" + }, + "options": { + "description": "Available choices", + "type": "array", + "items": { + "$ref": "#/components/schemas/QuestionOption" + } + }, + "multiple": { + "description": "Allow selecting multiple choices", + "type": "boolean" + }, + "custom": { + "description": "Allow typing a custom answer (default: true)", + "type": "boolean" + } + }, + "required": ["question", "header", "options"] + }, + "QuestionRequest": { + "type": "object", + "properties": { + "id": { + "type": "string", + "pattern": "^que.*" + }, + "sessionID": { + "type": "string", + "pattern": "^ses.*" + }, + "questions": { + "description": "Questions to ask", + "type": "array", + "items": { + "$ref": "#/components/schemas/QuestionInfo" + } + }, + "tool": { + "type": "object", + "properties": { + "messageID": { + "type": "string" + }, + "callID": { + "type": "string" + } + }, + "required": ["messageID", "callID"] + } + }, + "required": ["id", "sessionID", "questions"] + }, + "Event.question.asked": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "question.asked" + }, + "properties": { + "$ref": "#/components/schemas/QuestionRequest" + } + }, + "required": ["type", "properties"] + }, + "QuestionAnswer": { + "type": "array", + "items": { + "type": "string" + } + }, + "Event.question.replied": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "question.replied" + }, + "properties": { + "type": "object", + "properties": { + "sessionID": { + "type": "string" + }, + "requestID": { + "type": "string" + }, + "answers": { + "type": "array", + "items": { + "$ref": "#/components/schemas/QuestionAnswer" + } + } + }, + "required": ["sessionID", "requestID", "answers"] + } + }, + "required": ["type", "properties"] + }, + "Event.question.rejected": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "question.rejected" + }, + "properties": { + "type": "object", + "properties": { + "sessionID": { + "type": "string" + }, + "requestID": { + "type": "string" + } + }, + "required": ["sessionID", "requestID"] + } + }, + "required": ["type", "properties"] + }, + "Todo": { + "type": "object", + "properties": { + "content": { + "description": "Brief description of the task", + "type": "string" + }, + "status": { + "description": "Current status of the task: pending, in_progress, completed, cancelled", + "type": "string" + }, + "priority": { + "description": "Priority level of the task: high, medium, low", + "type": "string" + }, + "id": { + "description": "Unique identifier for the todo item", + "type": "string" + } + }, + "required": ["content", "status", "priority", "id"] + }, + "Event.todo.updated": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "todo.updated" + }, + "properties": { + "type": "object", + "properties": { + "sessionID": { + "type": "string" + }, + "todos": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Todo" + } + } + }, + "required": ["sessionID", "todos"] + } + }, + "required": ["type", "properties"] + }, + "Pty": { + "type": "object", + "properties": { + "id": { + "type": "string", + "pattern": "^pty.*" + }, + "title": { + "type": "string" + }, + "command": { + "type": "string" + }, + "args": { + "type": "array", + "items": { + "type": "string" + } + }, + "cwd": { + "type": "string" + }, + "status": { + "type": "string", + "enum": ["running", "exited"] + }, + "pid": { + "type": "number" + } + }, + "required": ["id", "title", "command", "args", "cwd", "status", "pid"] + }, + "Event.pty.created": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "pty.created" + }, + "properties": { + "type": "object", + "properties": { + "info": { + "$ref": "#/components/schemas/Pty" + } + }, + "required": ["info"] + } + }, + "required": ["type", "properties"] + }, + "Event.pty.updated": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "pty.updated" + }, + "properties": { + "type": "object", + "properties": { + "info": { + "$ref": "#/components/schemas/Pty" + } + }, + "required": ["info"] + } + }, + "required": ["type", "properties"] + }, + "Event.pty.exited": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "pty.exited" + }, + "properties": { + "type": "object", + "properties": { + "id": { + "type": "string", + "pattern": "^pty.*" + }, + "exitCode": { + "type": "number" + } + }, + "required": ["id", "exitCode"] + } + }, + "required": ["type", "properties"] + }, + "Event.pty.deleted": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "pty.deleted" + }, + "properties": { + "type": "object", + "properties": { + "id": { + "type": "string", + "pattern": "^pty.*" + } + }, + "required": ["id"] + } + }, + "required": ["type", "properties"] + }, + "Event.file.watcher.updated": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "file.watcher.updated" + }, + "properties": { + "type": "object", + "properties": { + "file": { + "type": "string" + }, + "event": { + "anyOf": [ + { + "type": "string", + "const": "add" + }, + { + "type": "string", + "const": "change" + }, + { + "type": "string", + "const": "unlink" + } ] + } + }, + "required": ["file", "event"] + } + }, + "required": ["type", "properties"] + }, + "Event.mcp.tools.changed": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "mcp.tools.changed" + }, + "properties": { + "type": "object", + "properties": { + "server": { + "type": "string" + } + }, + "required": ["server"] + } + }, + "required": ["type", "properties"] + }, + "Event.mcp.browser.open.failed": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "mcp.browser.open.failed" + }, + "properties": { + "type": "object", + "properties": { + "mcpName": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "required": ["mcpName", "url"] + } + }, + "required": ["type", "properties"] + }, + "Event.lsp.updated": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "lsp.updated" + }, + "properties": { + "type": "object", + "properties": {} + } + }, + "required": ["type", "properties"] + }, + "Event.vcs.branch.updated": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "vcs.branch.updated" + }, + "properties": { + "type": "object", + "properties": { + "branch": { + "type": "string" + } + } + } + }, + "required": ["type", "properties"] + }, + "Event.command.executed": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "command.executed" + }, + "properties": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "sessionID": { + "type": "string", + "pattern": "^ses.*" + }, + "arguments": { + "type": "string" + }, + "messageID": { + "type": "string", + "pattern": "^msg.*" + } }, - "Event.tui.session.select": { + "required": ["name", "sessionID", "arguments", "messageID"] + } + }, + "required": ["type", "properties"] + }, + "FileDiff": { + "type": "object", + "properties": { + "file": { + "type": "string" + }, + "before": { + "type": "string" + }, + "after": { + "type": "string" + }, + "additions": { + "type": "number" + }, + "deletions": { + "type": "number" + } + }, + "required": ["file", "before", "after", "additions", "deletions"] + }, + "UserMessage": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "sessionID": { + "type": "string" + }, + "role": { + "type": "string", + "const": "user" + }, + "time": { + "type": "object", + "properties": { + "created": { + "type": "number" + } + }, + "required": ["created"] + }, + "summary": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "body": { + "type": "string" + }, + "diffs": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FileDiff" + } + } + }, + "required": ["diffs"] + }, + "agent": { + "type": "string" + }, + "model": { + "type": "object", + "properties": { + "providerID": { + "type": "string" + }, + "modelID": { + "type": "string" + } + }, + "required": ["providerID", "modelID"] + }, + "system": { + "type": "string" + }, + "tools": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "boolean" + } + }, + "variant": { + "type": "string" + } + }, + "required": ["id", "sessionID", "role", "time", "agent", "model"] + }, + "ProviderAuthError": { + "type": "object", + "properties": { + "name": { + "type": "string", + "const": "ProviderAuthError" + }, + "data": { + "type": "object", + "properties": { + "providerID": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": ["providerID", "message"] + } + }, + "required": ["name", "data"] + }, + "UnknownError": { + "type": "object", + "properties": { + "name": { + "type": "string", + "const": "UnknownError" + }, + "data": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": ["message"] + } + }, + "required": ["name", "data"] + }, + "MessageOutputLengthError": { + "type": "object", + "properties": { + "name": { + "type": "string", + "const": "MessageOutputLengthError" + }, + "data": { + "type": "object", + "properties": {} + } + }, + "required": ["name", "data"] + }, + "MessageAbortedError": { + "type": "object", + "properties": { + "name": { + "type": "string", + "const": "MessageAbortedError" + }, + "data": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": ["message"] + } + }, + "required": ["name", "data"] + }, + "APIError": { + "type": "object", + "properties": { + "name": { + "type": "string", + "const": "APIError" + }, + "data": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "statusCode": { + "type": "number" + }, + "isRetryable": { + "type": "boolean" + }, + "responseHeaders": { "type": "object", - "properties": { - "type": { - "type": "string", - "const": "tui.session.select" - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "description": "Session ID to navigate to", - "type": "string", - "pattern": "^ses" - } - }, - "required": [ - "sessionID" - ] - } + "propertyNames": { + "type": "string" }, - "required": [ - "type", - "properties" - ] - }, - "Event.installation.updated": { + "additionalProperties": { + "type": "string" + } + }, + "responseBody": { + "type": "string" + }, + "metadata": { "type": "object", - "properties": { - "type": { - "type": "string", - "const": "installation.updated" - }, - "properties": { - "type": "object", - "properties": { - "version": { - "type": "string" - } - }, - "required": [ - "version" - ] - } + "propertyNames": { + "type": "string" }, - "required": [ - "type", - "properties" - ] + "additionalProperties": { + "type": "string" + } + } }, - "Event.installation.update-available": { + "required": ["message", "isRetryable"] + } + }, + "required": ["name", "data"] + }, + "AssistantMessage": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "sessionID": { + "type": "string" + }, + "role": { + "type": "string", + "const": "assistant" + }, + "time": { + "type": "object", + "properties": { + "created": { + "type": "number" + }, + "completed": { + "type": "number" + } + }, + "required": ["created"] + }, + "error": { + "anyOf": [ + { + "$ref": "#/components/schemas/ProviderAuthError" + }, + { + "$ref": "#/components/schemas/UnknownError" + }, + { + "$ref": "#/components/schemas/MessageOutputLengthError" + }, + { + "$ref": "#/components/schemas/MessageAbortedError" + }, + { + "$ref": "#/components/schemas/APIError" + } + ] + }, + "parentID": { + "type": "string" + }, + "modelID": { + "type": "string" + }, + "providerID": { + "type": "string" + }, + "mode": { + "type": "string" + }, + "agent": { + "type": "string" + }, + "path": { + "type": "object", + "properties": { + "cwd": { + "type": "string" + }, + "root": { + "type": "string" + } + }, + "required": ["cwd", "root"] + }, + "summary": { + "type": "boolean" + }, + "cost": { + "type": "number" + }, + "tokens": { + "type": "object", + "properties": { + "input": { + "type": "number" + }, + "output": { + "type": "number" + }, + "reasoning": { + "type": "number" + }, + "cache": { "type": "object", "properties": { - "type": { - "type": "string", - "const": "installation.update-available" - }, - "properties": { - "type": "object", - "properties": { - "version": { - "type": "string" - } - }, - "required": [ - "version" - ] - } - }, - "required": [ - "type", - "properties" - ] - }, - "Project": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "worktree": { - "type": "string" - }, - "vcs": { - "type": "string", - "const": "git" - }, - "name": { - "type": "string" - }, - "icon": { - "type": "object", - "properties": { - "url": { - "type": "string" - }, - "override": { - "type": "string" - }, - "color": { - "type": "string" - } - } - }, - "commands": { - "type": "object", - "properties": { - "start": { - "description": "Startup script to run when creating a new workspace (worktree)", - "type": "string" - } - } - }, - "time": { - "type": "object", - "properties": { - "created": { - "type": "number" - }, - "updated": { - "type": "number" - }, - "initialized": { - "type": "number" - } - }, - "required": [ - "created", - "updated" - ] - }, - "sandboxes": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "required": [ - "id", - "worktree", - "time", - "sandboxes" - ] - }, - "Event.project.updated": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "project.updated" - }, - "properties": { - "$ref": "#/components/schemas/Project" - } - }, - "required": [ - "type", - "properties" - ] - }, - "Event.server.instance.disposed": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "server.instance.disposed" - }, - "properties": { - "type": "object", - "properties": { - "directory": { - "type": "string" - } - }, - "required": [ - "directory" - ] - } - }, - "required": [ - "type", - "properties" - ] - }, - "Event.file.edited": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "file.edited" - }, - "properties": { - "type": "object", - "properties": { - "file": { - "type": "string" - } - }, - "required": [ - "file" - ] - } - }, - "required": [ - "type", - "properties" - ] - }, - "Event.worktree.ready": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "worktree.ready" - }, - "properties": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "branch": { - "type": "string" - } - }, - "required": [ - "name", - "branch" - ] - } - }, - "required": [ - "type", - "properties" - ] - }, - "Event.worktree.failed": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "worktree.failed" - }, - "properties": { - "type": "object", - "properties": { - "message": { - "type": "string" - } - }, - "required": [ - "message" - ] - } - }, - "required": [ - "type", - "properties" - ] - }, - "Event.lsp.client.diagnostics": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "lsp.client.diagnostics" - }, - "properties": { - "type": "object", - "properties": { - "serverID": { - "type": "string" - }, - "path": { - "type": "string" - } - }, - "required": [ - "serverID", - "path" - ] - } - }, - "required": [ - "type", - "properties" - ] - }, - "PermissionRequest": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^per.*" - }, - "sessionID": { - "type": "string", - "pattern": "^ses.*" - }, - "permission": { - "type": "string" - }, - "patterns": { - "type": "array", - "items": { - "type": "string" - } - }, - "metadata": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": {} - }, - "always": { - "type": "array", - "items": { - "type": "string" - } - }, - "tool": { - "type": "object", - "properties": { - "messageID": { - "type": "string" - }, - "callID": { - "type": "string" - } - }, - "required": [ - "messageID", - "callID" - ] - } - }, - "required": [ - "id", - "sessionID", - "permission", - "patterns", - "metadata", - "always" - ] - }, - "Event.permission.asked": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "permission.asked" - }, - "properties": { - "$ref": "#/components/schemas/PermissionRequest" - } - }, - "required": [ - "type", - "properties" - ] - }, - "Event.permission.replied": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "permission.replied" - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string" - }, - "requestID": { - "type": "string" - }, - "reply": { - "type": "string", - "enum": [ - "once", - "always", - "reject" - ] - } - }, - "required": [ - "sessionID", - "requestID", - "reply" - ] - } - }, - "required": [ - "type", - "properties" - ] - }, - "SessionStatus": { - "anyOf": [ - { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "idle" - } - }, - "required": [ - "type" - ] - }, - { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "retry" - }, - "attempt": { - "type": "number" - }, - "message": { - "type": "string" - }, - "next": { - "type": "number" - } - }, - "required": [ - "type", - "attempt", - "message", - "next" - ] - }, - { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "busy" - } - }, - "required": [ - "type" - ] - } - ] - }, - "Event.session.status": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "session.status" - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string" - }, - "status": { - "$ref": "#/components/schemas/SessionStatus" - } - }, - "required": [ - "sessionID", - "status" - ] - } - }, - "required": [ - "type", - "properties" - ] - }, - "Event.session.idle": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "session.idle" - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string" - } - }, - "required": [ - "sessionID" - ] - } - }, - "required": [ - "type", - "properties" - ] - }, - "QuestionOption": { - "type": "object", - "properties": { - "label": { - "description": "Display text (1-5 words, concise)", - "type": "string" - }, - "description": { - "description": "Explanation of choice", - "type": "string" - } - }, - "required": [ - "label", - "description" - ] - }, - "QuestionInfo": { - "type": "object", - "properties": { - "question": { - "description": "Complete question", - "type": "string" - }, - "header": { - "description": "Very short label (max 30 chars)", - "type": "string" - }, - "options": { - "description": "Available choices", - "type": "array", - "items": { - "$ref": "#/components/schemas/QuestionOption" - } - }, - "multiple": { - "description": "Allow selecting multiple choices", - "type": "boolean" - }, - "custom": { - "description": "Allow typing a custom answer (default: true)", - "type": "boolean" - } - }, - "required": [ - "question", - "header", - "options" - ] - }, - "QuestionRequest": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^que.*" - }, - "sessionID": { - "type": "string", - "pattern": "^ses.*" - }, - "questions": { - "description": "Questions to ask", - "type": "array", - "items": { - "$ref": "#/components/schemas/QuestionInfo" - } - }, - "tool": { - "type": "object", - "properties": { - "messageID": { - "type": "string" - }, - "callID": { - "type": "string" - } - }, - "required": [ - "messageID", - "callID" - ] - } - }, - "required": [ - "id", - "sessionID", - "questions" - ] - }, - "Event.question.asked": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "question.asked" - }, - "properties": { - "$ref": "#/components/schemas/QuestionRequest" - } - }, - "required": [ - "type", - "properties" - ] - }, - "QuestionAnswer": { - "type": "array", - "items": { - "type": "string" - } - }, - "Event.question.replied": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "question.replied" - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string" - }, - "requestID": { - "type": "string" - }, - "answers": { - "type": "array", - "items": { - "$ref": "#/components/schemas/QuestionAnswer" - } - } - }, - "required": [ - "sessionID", - "requestID", - "answers" - ] - } - }, - "required": [ - "type", - "properties" - ] - }, - "Event.question.rejected": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "question.rejected" - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string" - }, - "requestID": { - "type": "string" - } - }, - "required": [ - "sessionID", - "requestID" - ] - } - }, - "required": [ - "type", - "properties" - ] - }, - "Todo": { - "type": "object", - "properties": { - "content": { - "description": "Brief description of the task", - "type": "string" - }, - "status": { - "description": "Current status of the task: pending, in_progress, completed, cancelled", - "type": "string" - }, - "priority": { - "description": "Priority level of the task: high, medium, low", - "type": "string" - }, - "id": { - "description": "Unique identifier for the todo item", - "type": "string" - } - }, - "required": [ - "content", - "status", - "priority", - "id" - ] - }, - "Event.todo.updated": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "todo.updated" - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string" - }, - "todos": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Todo" - } - } - }, - "required": [ - "sessionID", - "todos" - ] - } - }, - "required": [ - "type", - "properties" - ] - }, - "Pty": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^pty.*" - }, - "title": { - "type": "string" - }, - "command": { - "type": "string" - }, - "args": { - "type": "array", - "items": { - "type": "string" - } - }, - "cwd": { - "type": "string" - }, - "status": { - "type": "string", - "enum": [ - "running", - "exited" - ] - }, - "pid": { - "type": "number" - } - }, - "required": [ - "id", - "title", - "command", - "args", - "cwd", - "status", - "pid" - ] - }, - "Event.pty.created": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "pty.created" - }, - "properties": { - "type": "object", - "properties": { - "info": { - "$ref": "#/components/schemas/Pty" - } - }, - "required": [ - "info" - ] - } - }, - "required": [ - "type", - "properties" - ] - }, - "Event.pty.updated": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "pty.updated" - }, - "properties": { - "type": "object", - "properties": { - "info": { - "$ref": "#/components/schemas/Pty" - } - }, - "required": [ - "info" - ] - } - }, - "required": [ - "type", - "properties" - ] - }, - "Event.pty.exited": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "pty.exited" - }, - "properties": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^pty.*" - }, - "exitCode": { - "type": "number" - } - }, - "required": [ - "id", - "exitCode" - ] - } - }, - "required": [ - "type", - "properties" - ] - }, - "Event.pty.deleted": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "pty.deleted" - }, - "properties": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^pty.*" - } - }, - "required": [ - "id" - ] - } - }, - "required": [ - "type", - "properties" - ] - }, - "Event.file.watcher.updated": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "file.watcher.updated" - }, - "properties": { - "type": "object", - "properties": { - "file": { - "type": "string" - }, - "event": { - "anyOf": [ - { - "type": "string", - "const": "add" - }, - { - "type": "string", - "const": "change" - }, - { - "type": "string", - "const": "unlink" - } - ] - } - }, - "required": [ - "file", - "event" - ] - } - }, - "required": [ - "type", - "properties" - ] - }, - "Event.mcp.tools.changed": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "mcp.tools.changed" - }, - "properties": { - "type": "object", - "properties": { - "server": { - "type": "string" - } - }, - "required": [ - "server" - ] - } - }, - "required": [ - "type", - "properties" - ] - }, - "Event.mcp.browser.open.failed": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "mcp.browser.open.failed" - }, - "properties": { - "type": "object", - "properties": { - "mcpName": { - "type": "string" - }, - "url": { - "type": "string" - } - }, - "required": [ - "mcpName", - "url" - ] - } - }, - "required": [ - "type", - "properties" - ] - }, - "Event.lsp.updated": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "lsp.updated" - }, - "properties": { - "type": "object", - "properties": {} - } - }, - "required": [ - "type", - "properties" - ] - }, - "Event.vcs.branch.updated": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "vcs.branch.updated" - }, - "properties": { - "type": "object", - "properties": { - "branch": { - "type": "string" - } - } - } - }, - "required": [ - "type", - "properties" - ] - }, - "Event.command.executed": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "command.executed" - }, - "properties": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "sessionID": { - "type": "string", - "pattern": "^ses.*" - }, - "arguments": { - "type": "string" - }, - "messageID": { - "type": "string", - "pattern": "^msg.*" - } - }, - "required": [ - "name", - "sessionID", - "arguments", - "messageID" - ] - } - }, - "required": [ - "type", - "properties" - ] - }, - "FileDiff": { - "type": "object", - "properties": { - "file": { - "type": "string" - }, - "before": { - "type": "string" - }, - "after": { - "type": "string" - }, - "additions": { - "type": "number" - }, - "deletions": { - "type": "number" - } - }, - "required": [ - "file", - "before", - "after", - "additions", - "deletions" - ] - }, - "UserMessage": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "sessionID": { - "type": "string" - }, - "role": { - "type": "string", - "const": "user" - }, - "time": { - "type": "object", - "properties": { - "created": { - "type": "number" - } - }, - "required": [ - "created" - ] - }, - "summary": { - "type": "object", - "properties": { - "title": { - "type": "string" - }, - "body": { - "type": "string" - }, - "diffs": { - "type": "array", - "items": { - "$ref": "#/components/schemas/FileDiff" - } - } - }, - "required": [ - "diffs" - ] - }, - "agent": { - "type": "string" - }, - "model": { - "type": "object", - "properties": { - "providerID": { - "type": "string" - }, - "modelID": { - "type": "string" - } - }, - "required": [ - "providerID", - "modelID" - ] - }, - "system": { - "type": "string" - }, - "tools": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": { - "type": "boolean" - } - }, - "variant": { - "type": "string" - } - }, - "required": [ - "id", - "sessionID", - "role", - "time", - "agent", - "model" - ] - }, - "ProviderAuthError": { - "type": "object", - "properties": { - "name": { - "type": "string", - "const": "ProviderAuthError" - }, - "data": { - "type": "object", - "properties": { - "providerID": { - "type": "string" - }, - "message": { - "type": "string" - } - }, - "required": [ - "providerID", - "message" - ] - } - }, - "required": [ - "name", - "data" - ] - }, - "UnknownError": { - "type": "object", - "properties": { - "name": { - "type": "string", - "const": "UnknownError" - }, - "data": { - "type": "object", - "properties": { - "message": { - "type": "string" - } - }, - "required": [ - "message" - ] - } - }, - "required": [ - "name", - "data" - ] - }, - "MessageOutputLengthError": { - "type": "object", - "properties": { - "name": { - "type": "string", - "const": "MessageOutputLengthError" - }, - "data": { - "type": "object", - "properties": {} - } - }, - "required": [ - "name", - "data" - ] - }, - "MessageAbortedError": { - "type": "object", - "properties": { - "name": { - "type": "string", - "const": "MessageAbortedError" - }, - "data": { - "type": "object", - "properties": { - "message": { - "type": "string" - } - }, - "required": [ - "message" - ] - } - }, - "required": [ - "name", - "data" - ] - }, - "APIError": { - "type": "object", - "properties": { - "name": { - "type": "string", - "const": "APIError" - }, - "data": { - "type": "object", - "properties": { - "message": { - "type": "string" - }, - "statusCode": { - "type": "number" - }, - "isRetryable": { - "type": "boolean" - }, - "responseHeaders": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": { - "type": "string" - } - }, - "responseBody": { - "type": "string" - }, - "metadata": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": { - "type": "string" - } - } - }, - "required": [ - "message", - "isRetryable" - ] - } - }, - "required": [ - "name", - "data" - ] - }, - "AssistantMessage": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "sessionID": { - "type": "string" - }, - "role": { - "type": "string", - "const": "assistant" - }, - "time": { - "type": "object", - "properties": { - "created": { - "type": "number" - }, - "completed": { - "type": "number" - } - }, - "required": [ - "created" - ] - }, - "error": { - "anyOf": [ - { - "$ref": "#/components/schemas/ProviderAuthError" - }, - { - "$ref": "#/components/schemas/UnknownError" - }, - { - "$ref": "#/components/schemas/MessageOutputLengthError" - }, - { - "$ref": "#/components/schemas/MessageAbortedError" - }, - { - "$ref": "#/components/schemas/APIError" - } - ] - }, - "parentID": { - "type": "string" - }, - "modelID": { - "type": "string" - }, - "providerID": { - "type": "string" - }, - "mode": { - "type": "string" - }, - "agent": { - "type": "string" - }, - "path": { - "type": "object", - "properties": { - "cwd": { - "type": "string" - }, - "root": { - "type": "string" - } - }, - "required": [ - "cwd", - "root" - ] - }, - "summary": { - "type": "boolean" - }, - "cost": { - "type": "number" - }, - "tokens": { - "type": "object", - "properties": { - "input": { - "type": "number" - }, - "output": { - "type": "number" - }, - "reasoning": { - "type": "number" - }, - "cache": { - "type": "object", - "properties": { - "read": { - "type": "number" - }, - "write": { - "type": "number" - } - }, - "required": [ - "read", - "write" - ] - } - }, - "required": [ - "input", - "output", - "reasoning", - "cache" - ] - }, - "finish": { - "type": "string" - } - }, - "required": [ - "id", - "sessionID", - "role", - "time", - "parentID", - "modelID", - "providerID", - "mode", - "agent", - "path", - "cost", - "tokens" - ] - }, - "Message": { - "anyOf": [ - { - "$ref": "#/components/schemas/UserMessage" - }, - { - "$ref": "#/components/schemas/AssistantMessage" - } - ] - }, - "Event.message.updated": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "message.updated" - }, - "properties": { - "type": "object", - "properties": { - "info": { - "$ref": "#/components/schemas/Message" - } - }, - "required": [ - "info" - ] - } - }, - "required": [ - "type", - "properties" - ] - }, - "Event.message.removed": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "message.removed" - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string" - }, - "messageID": { - "type": "string" - } - }, - "required": [ - "sessionID", - "messageID" - ] - } - }, - "required": [ - "type", - "properties" - ] - }, - "TextPart": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "sessionID": { - "type": "string" - }, - "messageID": { - "type": "string" - }, - "type": { - "type": "string", - "const": "text" - }, - "text": { - "type": "string" - }, - "synthetic": { - "type": "boolean" - }, - "ignored": { - "type": "boolean" - }, - "time": { - "type": "object", - "properties": { - "start": { - "type": "number" - }, - "end": { - "type": "number" - } - }, - "required": [ - "start" - ] - }, - "metadata": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": {} - } - }, - "required": [ - "id", - "sessionID", - "messageID", - "type", - "text" - ] - }, - "ReasoningPart": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "sessionID": { - "type": "string" - }, - "messageID": { - "type": "string" - }, - "type": { - "type": "string", - "const": "reasoning" - }, - "text": { - "type": "string" - }, - "metadata": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": {} - }, - "time": { - "type": "object", - "properties": { - "start": { - "type": "number" - }, - "end": { - "type": "number" - } - }, - "required": [ - "start" - ] - } - }, - "required": [ - "id", - "sessionID", - "messageID", - "type", - "text", - "time" - ] - }, - "FilePartSourceText": { - "type": "object", - "properties": { - "value": { - "type": "string" - }, - "start": { - "type": "integer", - "minimum": -9007199254740991, - "maximum": 9007199254740991 - }, - "end": { - "type": "integer", - "minimum": -9007199254740991, - "maximum": 9007199254740991 - } - }, - "required": [ - "value", - "start", - "end" - ] - }, - "FileSource": { - "type": "object", - "properties": { - "text": { - "$ref": "#/components/schemas/FilePartSourceText" - }, - "type": { - "type": "string", - "const": "file" - }, - "path": { - "type": "string" - } - }, - "required": [ - "text", - "type", - "path" - ] - }, - "Range": { - "type": "object", - "properties": { - "start": { - "type": "object", - "properties": { - "line": { - "type": "number" - }, - "character": { - "type": "number" - } - }, - "required": [ - "line", - "character" - ] - }, - "end": { - "type": "object", - "properties": { - "line": { - "type": "number" - }, - "character": { - "type": "number" - } - }, - "required": [ - "line", - "character" - ] - } - }, - "required": [ - "start", - "end" - ] - }, - "SymbolSource": { - "type": "object", - "properties": { - "text": { - "$ref": "#/components/schemas/FilePartSourceText" - }, - "type": { - "type": "string", - "const": "symbol" - }, - "path": { - "type": "string" - }, - "range": { - "$ref": "#/components/schemas/Range" - }, - "name": { - "type": "string" - }, - "kind": { - "type": "integer", - "minimum": -9007199254740991, - "maximum": 9007199254740991 - } - }, - "required": [ - "text", - "type", - "path", - "range", - "name", - "kind" - ] - }, - "ResourceSource": { - "type": "object", - "properties": { - "text": { - "$ref": "#/components/schemas/FilePartSourceText" - }, - "type": { - "type": "string", - "const": "resource" - }, - "clientName": { - "type": "string" - }, - "uri": { - "type": "string" - } - }, - "required": [ - "text", - "type", - "clientName", - "uri" - ] - }, - "FilePartSource": { - "anyOf": [ - { - "$ref": "#/components/schemas/FileSource" - }, - { - "$ref": "#/components/schemas/SymbolSource" - }, - { - "$ref": "#/components/schemas/ResourceSource" - } - ] - }, - "FilePart": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "sessionID": { - "type": "string" - }, - "messageID": { - "type": "string" - }, - "type": { - "type": "string", - "const": "file" - }, - "mime": { - "type": "string" - }, - "filename": { - "type": "string" - }, - "url": { - "type": "string" - }, - "source": { - "$ref": "#/components/schemas/FilePartSource" - } - }, - "required": [ - "id", - "sessionID", - "messageID", - "type", - "mime", - "url" - ] - }, - "ToolStatePending": { - "type": "object", - "properties": { - "status": { - "type": "string", - "const": "pending" - }, - "input": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": {} - }, - "raw": { - "type": "string" - } - }, - "required": [ - "status", - "input", - "raw" - ] - }, - "ToolStateRunning": { - "type": "object", - "properties": { - "status": { - "type": "string", - "const": "running" - }, - "input": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": {} - }, - "title": { - "type": "string" - }, - "metadata": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": {} - }, - "time": { - "type": "object", - "properties": { - "start": { - "type": "number" - } - }, - "required": [ - "start" - ] - } - }, - "required": [ - "status", - "input", - "time" - ] - }, - "ToolStateCompleted": { - "type": "object", - "properties": { - "status": { - "type": "string", - "const": "completed" - }, - "input": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": {} - }, - "output": { - "type": "string" - }, - "title": { - "type": "string" - }, - "metadata": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": {} - }, - "time": { - "type": "object", - "properties": { - "start": { - "type": "number" - }, - "end": { - "type": "number" - }, - "compacted": { - "type": "number" - } - }, - "required": [ - "start", - "end" - ] - }, - "attachments": { - "type": "array", - "items": { - "$ref": "#/components/schemas/FilePart" - } - } - }, - "required": [ - "status", - "input", - "output", - "title", - "metadata", - "time" - ] - }, - "ToolStateError": { - "type": "object", - "properties": { - "status": { - "type": "string", - "const": "error" - }, - "input": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": {} - }, - "error": { - "type": "string" - }, - "metadata": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": {} - }, - "time": { - "type": "object", - "properties": { - "start": { - "type": "number" - }, - "end": { - "type": "number" - } - }, - "required": [ - "start", - "end" - ] - } - }, - "required": [ - "status", - "input", - "error", - "time" - ] - }, - "ToolState": { - "anyOf": [ - { - "$ref": "#/components/schemas/ToolStatePending" - }, - { - "$ref": "#/components/schemas/ToolStateRunning" - }, - { - "$ref": "#/components/schemas/ToolStateCompleted" - }, - { - "$ref": "#/components/schemas/ToolStateError" - } - ] - }, - "ToolPart": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "sessionID": { - "type": "string" - }, - "messageID": { - "type": "string" - }, - "type": { - "type": "string", - "const": "tool" - }, - "callID": { - "type": "string" - }, - "tool": { - "type": "string" - }, - "state": { - "$ref": "#/components/schemas/ToolState" - }, - "metadata": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": {} - } - }, - "required": [ - "id", - "sessionID", - "messageID", - "type", - "callID", - "tool", - "state" - ] - }, - "StepStartPart": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "sessionID": { - "type": "string" - }, - "messageID": { - "type": "string" - }, - "type": { - "type": "string", - "const": "step-start" - }, - "snapshot": { - "type": "string" - } - }, - "required": [ - "id", - "sessionID", - "messageID", - "type" - ] - }, - "StepFinishPart": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "sessionID": { - "type": "string" - }, - "messageID": { - "type": "string" - }, - "type": { - "type": "string", - "const": "step-finish" - }, - "reason": { - "type": "string" - }, - "snapshot": { - "type": "string" - }, - "cost": { - "type": "number" - }, - "tokens": { - "type": "object", - "properties": { - "input": { - "type": "number" - }, - "output": { - "type": "number" - }, - "reasoning": { - "type": "number" - }, - "cache": { - "type": "object", - "properties": { - "read": { - "type": "number" - }, - "write": { - "type": "number" - } - }, - "required": [ - "read", - "write" - ] - } - }, - "required": [ - "input", - "output", - "reasoning", - "cache" - ] - } - }, - "required": [ - "id", - "sessionID", - "messageID", - "type", - "reason", - "cost", - "tokens" - ] - }, - "SnapshotPart": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "sessionID": { - "type": "string" - }, - "messageID": { - "type": "string" - }, - "type": { - "type": "string", - "const": "snapshot" - }, - "snapshot": { - "type": "string" - } - }, - "required": [ - "id", - "sessionID", - "messageID", - "type", - "snapshot" - ] - }, - "PatchPart": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "sessionID": { - "type": "string" - }, - "messageID": { - "type": "string" - }, - "type": { - "type": "string", - "const": "patch" - }, - "hash": { - "type": "string" - }, - "files": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "required": [ - "id", - "sessionID", - "messageID", - "type", - "hash", - "files" - ] - }, - "AgentPart": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "sessionID": { - "type": "string" - }, - "messageID": { - "type": "string" - }, - "type": { - "type": "string", - "const": "agent" - }, - "name": { - "type": "string" - }, - "source": { - "type": "object", - "properties": { - "value": { - "type": "string" - }, - "start": { - "type": "integer", - "minimum": -9007199254740991, - "maximum": 9007199254740991 - }, - "end": { - "type": "integer", - "minimum": -9007199254740991, - "maximum": 9007199254740991 - } - }, - "required": [ - "value", - "start", - "end" - ] - } - }, - "required": [ - "id", - "sessionID", - "messageID", - "type", - "name" - ] - }, - "RetryPart": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "sessionID": { - "type": "string" - }, - "messageID": { - "type": "string" - }, - "type": { - "type": "string", - "const": "retry" - }, - "attempt": { - "type": "number" - }, - "error": { - "$ref": "#/components/schemas/APIError" - }, - "time": { - "type": "object", - "properties": { - "created": { - "type": "number" - } - }, - "required": [ - "created" - ] - } - }, - "required": [ - "id", - "sessionID", - "messageID", - "type", - "attempt", - "error", - "time" - ] - }, - "CompactionPart": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "sessionID": { - "type": "string" - }, - "messageID": { - "type": "string" - }, - "type": { - "type": "string", - "const": "compaction" - }, - "auto": { - "type": "boolean" - } - }, - "required": [ - "id", - "sessionID", - "messageID", - "type", - "auto" - ] - }, - "Part": { - "anyOf": [ - { - "$ref": "#/components/schemas/TextPart" - }, - { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "sessionID": { - "type": "string" - }, - "messageID": { - "type": "string" - }, - "type": { - "type": "string", - "const": "subtask" - }, - "prompt": { - "type": "string" - }, - "description": { - "type": "string" - }, - "agent": { - "type": "string" - }, - "model": { - "type": "object", - "properties": { - "providerID": { - "type": "string" - }, - "modelID": { - "type": "string" - } - }, - "required": [ - "providerID", - "modelID" - ] - }, - "command": { - "type": "string" - } - }, - "required": [ - "id", - "sessionID", - "messageID", - "type", - "prompt", - "description", - "agent" - ] - }, - { - "$ref": "#/components/schemas/ReasoningPart" - }, - { - "$ref": "#/components/schemas/FilePart" - }, - { - "$ref": "#/components/schemas/ToolPart" - }, - { - "$ref": "#/components/schemas/StepStartPart" - }, - { - "$ref": "#/components/schemas/StepFinishPart" - }, - { - "$ref": "#/components/schemas/SnapshotPart" - }, - { - "$ref": "#/components/schemas/PatchPart" - }, - { - "$ref": "#/components/schemas/AgentPart" - }, - { - "$ref": "#/components/schemas/RetryPart" - }, - { - "$ref": "#/components/schemas/CompactionPart" - } - ] - }, - "Event.message.part.updated": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "message.part.updated" - }, - "properties": { - "type": "object", - "properties": { - "part": { - "$ref": "#/components/schemas/Part" - }, - "delta": { - "type": "string" - } - }, - "required": [ - "part" - ] - } - }, - "required": [ - "type", - "properties" - ] - }, - "Event.message.part.removed": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "message.part.removed" - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string" - }, - "messageID": { - "type": "string" - }, - "partID": { - "type": "string" - } - }, - "required": [ - "sessionID", - "messageID", - "partID" - ] - } - }, - "required": [ - "type", - "properties" - ] - }, - "Event.session.compacted": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "session.compacted" - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string" - } - }, - "required": [ - "sessionID" - ] - } - }, - "required": [ - "type", - "properties" - ] - }, - "PermissionAction": { - "type": "string", - "enum": [ - "allow", - "deny", - "ask" - ] - }, - "PermissionRule": { - "type": "object", - "properties": { - "permission": { - "type": "string" - }, - "pattern": { - "type": "string" - }, - "action": { - "$ref": "#/components/schemas/PermissionAction" - } - }, - "required": [ - "permission", - "pattern", - "action" - ] - }, - "PermissionRuleset": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PermissionRule" - } - }, - "Session": { - "type": "object", - "properties": { - "id": { - "type": "string", - "pattern": "^ses.*" - }, - "slug": { - "type": "string" - }, - "projectID": { - "type": "string" - }, - "directory": { - "type": "string" - }, - "parentID": { - "type": "string", - "pattern": "^ses.*" - }, - "summary": { - "type": "object", - "properties": { - "additions": { - "type": "number" - }, - "deletions": { - "type": "number" - }, - "files": { - "type": "number" - }, - "diffs": { - "type": "array", - "items": { - "$ref": "#/components/schemas/FileDiff" - } - } - }, - "required": [ - "additions", - "deletions", - "files" - ] - }, - "share": { - "type": "object", - "properties": { - "url": { - "type": "string" - } - }, - "required": [ - "url" - ] - }, - "title": { - "type": "string" - }, - "version": { - "type": "string" - }, - "time": { - "type": "object", - "properties": { - "created": { - "type": "number" - }, - "updated": { - "type": "number" - }, - "compacting": { - "type": "number" - }, - "archived": { - "type": "number" - } - }, - "required": [ - "created", - "updated" - ] - }, - "permission": { - "$ref": "#/components/schemas/PermissionRuleset" - }, - "revert": { - "type": "object", - "properties": { - "messageID": { - "type": "string" - }, - "partID": { - "type": "string" - }, - "snapshot": { - "type": "string" - }, - "diff": { - "type": "string" - } - }, - "required": [ - "messageID" - ] - } - }, - "required": [ - "id", - "slug", - "projectID", - "directory", - "title", - "version", - "time" - ] - }, - "Event.session.created": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "session.created" - }, - "properties": { - "type": "object", - "properties": { - "info": { - "$ref": "#/components/schemas/Session" - } - }, - "required": [ - "info" - ] - } - }, - "required": [ - "type", - "properties" - ] - }, - "Event.session.updated": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "session.updated" - }, - "properties": { - "type": "object", - "properties": { - "info": { - "$ref": "#/components/schemas/Session" - } - }, - "required": [ - "info" - ] - } - }, - "required": [ - "type", - "properties" - ] - }, - "Event.session.deleted": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "session.deleted" - }, - "properties": { - "type": "object", - "properties": { - "info": { - "$ref": "#/components/schemas/Session" - } - }, - "required": [ - "info" - ] - } - }, - "required": [ - "type", - "properties" - ] - }, - "Event.session.diff": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "session.diff" - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string" - }, - "diff": { - "type": "array", - "items": { - "$ref": "#/components/schemas/FileDiff" - } - } - }, - "required": [ - "sessionID", - "diff" - ] - } - }, - "required": [ - "type", - "properties" - ] - }, - "Event.session.error": { - "type": "object", - "properties": { - "type": { - "type": "string", - "const": "session.error" - }, - "properties": { - "type": "object", - "properties": { - "sessionID": { - "type": "string" - }, - "error": { - "anyOf": [ - { - "$ref": "#/components/schemas/ProviderAuthError" - }, - { - "$ref": "#/components/schemas/UnknownError" - }, - { - "$ref": "#/components/schemas/MessageOutputLengthError" - }, - { - "$ref": "#/components/schemas/MessageAbortedError" - }, - { - "$ref": "#/components/schemas/APIError" - } - ] - } - } - } - }, - "required": [ - "type", - "properties" - ] - }, - "Event": { - "anyOf": [ - { - "$ref": "#/components/schemas/Event.server.connected" - }, - { - "$ref": "#/components/schemas/Event.global.disposed" - }, - { - "$ref": "#/components/schemas/Event.tui.prompt.append" - }, - { - "$ref": "#/components/schemas/Event.tui.command.execute" - }, - { - "$ref": "#/components/schemas/Event.tui.toast.show" - }, - { - "$ref": "#/components/schemas/Event.tui.session.select" - }, - { - "$ref": "#/components/schemas/Event.installation.updated" - }, - { - "$ref": "#/components/schemas/Event.installation.update-available" - }, - { - "$ref": "#/components/schemas/Event.project.updated" - }, - { - "$ref": "#/components/schemas/Event.server.instance.disposed" - }, - { - "$ref": "#/components/schemas/Event.file.edited" - }, - { - "$ref": "#/components/schemas/Event.worktree.ready" - }, - { - "$ref": "#/components/schemas/Event.worktree.failed" - }, - { - "$ref": "#/components/schemas/Event.lsp.client.diagnostics" - }, - { - "$ref": "#/components/schemas/Event.permission.asked" - }, - { - "$ref": "#/components/schemas/Event.permission.replied" - }, - { - "$ref": "#/components/schemas/Event.session.status" - }, - { - "$ref": "#/components/schemas/Event.session.idle" - }, - { - "$ref": "#/components/schemas/Event.question.asked" - }, - { - "$ref": "#/components/schemas/Event.question.replied" - }, - { - "$ref": "#/components/schemas/Event.question.rejected" - }, - { - "$ref": "#/components/schemas/Event.todo.updated" - }, - { - "$ref": "#/components/schemas/Event.pty.created" - }, - { - "$ref": "#/components/schemas/Event.pty.updated" - }, - { - "$ref": "#/components/schemas/Event.pty.exited" - }, - { - "$ref": "#/components/schemas/Event.pty.deleted" - }, - { - "$ref": "#/components/schemas/Event.file.watcher.updated" - }, - { - "$ref": "#/components/schemas/Event.mcp.tools.changed" - }, - { - "$ref": "#/components/schemas/Event.mcp.browser.open.failed" - }, - { - "$ref": "#/components/schemas/Event.lsp.updated" - }, - { - "$ref": "#/components/schemas/Event.vcs.branch.updated" - }, - { - "$ref": "#/components/schemas/Event.command.executed" - }, - { - "$ref": "#/components/schemas/Event.message.updated" - }, - { - "$ref": "#/components/schemas/Event.message.removed" - }, - { - "$ref": "#/components/schemas/Event.message.part.updated" - }, - { - "$ref": "#/components/schemas/Event.message.part.removed" - }, - { - "$ref": "#/components/schemas/Event.session.compacted" - }, - { - "$ref": "#/components/schemas/Event.session.created" - }, - { - "$ref": "#/components/schemas/Event.session.updated" - }, - { - "$ref": "#/components/schemas/Event.session.deleted" - }, - { - "$ref": "#/components/schemas/Event.session.diff" - }, - { - "$ref": "#/components/schemas/Event.session.error" - } - ] - }, - "GlobalEvent": { - "type": "object", - "properties": { - "directory": { - "type": "string" - }, - "payload": { - "$ref": "#/components/schemas/Event" - } - }, - "required": [ - "directory", - "payload" - ] - }, - "BadRequestError": { - "type": "object", - "properties": { - "data": {}, - "errors": { - "type": "array", - "items": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": {} - } - }, - "success": { - "type": "boolean", - "const": false - } - }, - "required": [ - "data", - "errors", - "success" - ] - }, - "NotFoundError": { - "type": "object", - "properties": { - "name": { - "type": "string", - "const": "NotFoundError" - }, - "data": { - "type": "object", - "properties": { - "message": { - "type": "string" - } - }, - "required": [ - "message" - ] - } - }, - "required": [ - "name", - "data" - ] - }, - "KeybindsConfig": { - "description": "Custom keybind configurations", - "type": "object", - "properties": { - "leader": { - "description": "Leader key for keybind combinations", - "default": "ctrl+x", - "type": "string" - }, - "app_exit": { - "description": "Exit the application", - "default": "ctrl+c,ctrl+d,q", - "type": "string" - }, - "editor_open": { - "description": "Open external editor", - "default": "e", - "type": "string" - }, - "theme_list": { - "description": "List available themes", - "default": "t", - "type": "string" - }, - "sidebar_toggle": { - "description": "Toggle sidebar", - "default": "b", - "type": "string" - }, - "scrollbar_toggle": { - "description": "Toggle session scrollbar", - "default": "none", - "type": "string" - }, - "username_toggle": { - "description": "Toggle username visibility", - "default": "none", - "type": "string" - }, - "status_view": { - "description": "View status", - "default": "s", - "type": "string" - }, - "session_export": { - "description": "Export session to editor", - "default": "x", - "type": "string" - }, - "session_new": { - "description": "Create a new session", - "default": "n", - "type": "string" - }, - "session_list": { - "description": "List all sessions", - "default": "l", - "type": "string" - }, - "session_timeline": { - "description": "Show session timeline", - "default": "g", - "type": "string" - }, - "session_fork": { - "description": "Fork session from message", - "default": "none", - "type": "string" - }, - "session_rename": { - "description": "Rename session", - "default": "ctrl+r", - "type": "string" - }, - "session_delete": { - "description": "Delete session", - "default": "ctrl+d", - "type": "string" - }, - "stash_delete": { - "description": "Delete stash entry", - "default": "ctrl+d", - "type": "string" - }, - "model_provider_list": { - "description": "Open provider list from model dialog", - "default": "ctrl+a", - "type": "string" - }, - "model_favorite_toggle": { - "description": "Toggle model favorite status", - "default": "ctrl+f", - "type": "string" - }, - "session_share": { - "description": "Share current session", - "default": "none", - "type": "string" - }, - "session_unshare": { - "description": "Unshare current session", - "default": "none", - "type": "string" - }, - "session_interrupt": { - "description": "Interrupt current session", - "default": "escape", - "type": "string" - }, - "session_compact": { - "description": "Compact the session", - "default": "c", - "type": "string" - }, - "messages_page_up": { - "description": "Scroll messages up by one page", - "default": "pageup,ctrl+alt+b", - "type": "string" - }, - "messages_page_down": { - "description": "Scroll messages down by one page", - "default": "pagedown,ctrl+alt+f", - "type": "string" - }, - "messages_line_up": { - "description": "Scroll messages up by one line", - "default": "ctrl+alt+y", - "type": "string" - }, - "messages_line_down": { - "description": "Scroll messages down by one line", - "default": "ctrl+alt+e", - "type": "string" - }, - "messages_half_page_up": { - "description": "Scroll messages up by half page", - "default": "ctrl+alt+u", - "type": "string" - }, - "messages_half_page_down": { - "description": "Scroll messages down by half page", - "default": "ctrl+alt+d", - "type": "string" - }, - "messages_first": { - "description": "Navigate to first message", - "default": "ctrl+g,home", - "type": "string" - }, - "messages_last": { - "description": "Navigate to last message", - "default": "ctrl+alt+g,end", - "type": "string" - }, - "messages_next": { - "description": "Navigate to next message", - "default": "none", - "type": "string" - }, - "messages_previous": { - "description": "Navigate to previous message", - "default": "none", - "type": "string" - }, - "messages_last_user": { - "description": "Navigate to last user message", - "default": "none", - "type": "string" - }, - "messages_copy": { - "description": "Copy message", - "default": "y", - "type": "string" - }, - "messages_undo": { - "description": "Undo message", - "default": "u", - "type": "string" - }, - "messages_redo": { - "description": "Redo message", - "default": "r", - "type": "string" - }, - "messages_toggle_conceal": { - "description": "Toggle code block concealment in messages", - "default": "h", - "type": "string" - }, - "tool_details": { - "description": "Toggle tool details visibility", - "default": "none", - "type": "string" - }, - "model_list": { - "description": "List available models", - "default": "m", - "type": "string" - }, - "model_cycle_recent": { - "description": "Next recently used model", - "default": "f2", - "type": "string" - }, - "model_cycle_recent_reverse": { - "description": "Previous recently used model", - "default": "shift+f2", - "type": "string" - }, - "model_cycle_favorite": { - "description": "Next favorite model", - "default": "none", - "type": "string" - }, - "model_cycle_favorite_reverse": { - "description": "Previous favorite model", - "default": "none", - "type": "string" - }, - "command_list": { - "description": "List available commands", - "default": "ctrl+p", - "type": "string" - }, - "agent_list": { - "description": "List agents", - "default": "a", - "type": "string" - }, - "agent_cycle": { - "description": "Next agent", - "default": "tab", - "type": "string" - }, - "agent_cycle_reverse": { - "description": "Previous agent", - "default": "shift+tab", - "type": "string" - }, - "variant_cycle": { - "description": "Cycle model variants", - "default": "ctrl+t", - "type": "string" - }, - "input_clear": { - "description": "Clear input field", - "default": "ctrl+c", - "type": "string" - }, - "input_paste": { - "description": "Paste from clipboard", - "default": "ctrl+v", - "type": "string" - }, - "input_submit": { - "description": "Submit input", - "default": "return", - "type": "string" - }, - "input_newline": { - "description": "Insert newline in input", - "default": "shift+return,ctrl+return,alt+return,ctrl+j", - "type": "string" - }, - "input_move_left": { - "description": "Move cursor left in input", - "default": "left,ctrl+b", - "type": "string" - }, - "input_move_right": { - "description": "Move cursor right in input", - "default": "right,ctrl+f", - "type": "string" - }, - "input_move_up": { - "description": "Move cursor up in input", - "default": "up", - "type": "string" - }, - "input_move_down": { - "description": "Move cursor down in input", - "default": "down", - "type": "string" - }, - "input_select_left": { - "description": "Select left in input", - "default": "shift+left", - "type": "string" - }, - "input_select_right": { - "description": "Select right in input", - "default": "shift+right", - "type": "string" - }, - "input_select_up": { - "description": "Select up in input", - "default": "shift+up", - "type": "string" - }, - "input_select_down": { - "description": "Select down in input", - "default": "shift+down", - "type": "string" - }, - "input_line_home": { - "description": "Move to start of line in input", - "default": "ctrl+a", - "type": "string" - }, - "input_line_end": { - "description": "Move to end of line in input", - "default": "ctrl+e", - "type": "string" - }, - "input_select_line_home": { - "description": "Select to start of line in input", - "default": "ctrl+shift+a", - "type": "string" - }, - "input_select_line_end": { - "description": "Select to end of line in input", - "default": "ctrl+shift+e", - "type": "string" - }, - "input_visual_line_home": { - "description": "Move to start of visual line in input", - "default": "alt+a", - "type": "string" - }, - "input_visual_line_end": { - "description": "Move to end of visual line in input", - "default": "alt+e", - "type": "string" - }, - "input_select_visual_line_home": { - "description": "Select to start of visual line in input", - "default": "alt+shift+a", - "type": "string" - }, - "input_select_visual_line_end": { - "description": "Select to end of visual line in input", - "default": "alt+shift+e", - "type": "string" - }, - "input_buffer_home": { - "description": "Move to start of buffer in input", - "default": "home", - "type": "string" - }, - "input_buffer_end": { - "description": "Move to end of buffer in input", - "default": "end", - "type": "string" - }, - "input_select_buffer_home": { - "description": "Select to start of buffer in input", - "default": "shift+home", - "type": "string" - }, - "input_select_buffer_end": { - "description": "Select to end of buffer in input", - "default": "shift+end", - "type": "string" - }, - "input_delete_line": { - "description": "Delete line in input", - "default": "ctrl+shift+d", - "type": "string" - }, - "input_delete_to_line_end": { - "description": "Delete to end of line in input", - "default": "ctrl+k", - "type": "string" - }, - "input_delete_to_line_start": { - "description": "Delete to start of line in input", - "default": "ctrl+u", - "type": "string" - }, - "input_backspace": { - "description": "Backspace in input", - "default": "backspace,shift+backspace", - "type": "string" - }, - "input_delete": { - "description": "Delete character in input", - "default": "ctrl+d,delete,shift+delete", - "type": "string" - }, - "input_undo": { - "description": "Undo in input", - "default": "ctrl+-,super+z", - "type": "string" - }, - "input_redo": { - "description": "Redo in input", - "default": "ctrl+.,super+shift+z", - "type": "string" - }, - "input_word_forward": { - "description": "Move word forward in input", - "default": "alt+f,alt+right,ctrl+right", - "type": "string" - }, - "input_word_backward": { - "description": "Move word backward in input", - "default": "alt+b,alt+left,ctrl+left", - "type": "string" - }, - "input_select_word_forward": { - "description": "Select word forward in input", - "default": "alt+shift+f,alt+shift+right", - "type": "string" - }, - "input_select_word_backward": { - "description": "Select word backward in input", - "default": "alt+shift+b,alt+shift+left", - "type": "string" - }, - "input_delete_word_forward": { - "description": "Delete word forward in input", - "default": "alt+d,alt+delete,ctrl+delete", - "type": "string" - }, - "input_delete_word_backward": { - "description": "Delete word backward in input", - "default": "ctrl+w,ctrl+backspace,alt+backspace", - "type": "string" - }, - "history_previous": { - "description": "Previous history item", - "default": "up", - "type": "string" - }, - "history_next": { - "description": "Next history item", - "default": "down", - "type": "string" - }, - "session_child_cycle": { - "description": "Next child session", - "default": "right", - "type": "string" - }, - "session_child_cycle_reverse": { - "description": "Previous child session", - "default": "left", - "type": "string" - }, - "session_parent": { - "description": "Go to parent session", - "default": "up", - "type": "string" - }, - "terminal_suspend": { - "description": "Suspend terminal", - "default": "ctrl+z", - "type": "string" - }, - "terminal_title_toggle": { - "description": "Toggle terminal title", - "default": "none", - "type": "string" - }, - "tips_toggle": { - "description": "Toggle tips on home screen", - "default": "h", - "type": "string" - } - }, - "additionalProperties": false - }, - "LogLevel": { - "description": "Log level", - "type": "string", - "enum": [ - "DEBUG", - "INFO", - "WARN", - "ERROR" - ] - }, - "ServerConfig": { - "description": "Server configuration for opencode serve and web commands", - "type": "object", - "properties": { - "port": { - "description": "Port to listen on", - "type": "integer", - "exclusiveMinimum": 0, - "maximum": 9007199254740991 - }, - "hostname": { - "description": "Hostname to listen on", - "type": "string" - }, - "mdns": { - "description": "Enable mDNS service discovery", - "type": "boolean" - }, - "cors": { - "description": "Additional domains to allow for CORS", - "type": "array", - "items": { - "type": "string" - } - } - }, - "additionalProperties": false - }, - "PermissionActionConfig": { - "type": "string", - "enum": [ - "ask", - "allow", - "deny" - ] - }, - "PermissionObjectConfig": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": { - "$ref": "#/components/schemas/PermissionActionConfig" - } - }, - "PermissionRuleConfig": { - "anyOf": [ - { - "$ref": "#/components/schemas/PermissionActionConfig" - }, - { - "$ref": "#/components/schemas/PermissionObjectConfig" - } - ] - }, - "PermissionConfig": { - "anyOf": [ - { - "type": "object", - "properties": { - "__originalKeys": { - "type": "array", - "items": { - "type": "string" - } - }, - "read": { - "$ref": "#/components/schemas/PermissionRuleConfig" - }, - "edit": { - "$ref": "#/components/schemas/PermissionRuleConfig" - }, - "glob": { - "$ref": "#/components/schemas/PermissionRuleConfig" - }, - "grep": { - "$ref": "#/components/schemas/PermissionRuleConfig" - }, - "list": { - "$ref": "#/components/schemas/PermissionRuleConfig" - }, - "bash": { - "$ref": "#/components/schemas/PermissionRuleConfig" - }, - "task": { - "$ref": "#/components/schemas/PermissionRuleConfig" - }, - "external_directory": { - "$ref": "#/components/schemas/PermissionRuleConfig" - }, - "todowrite": { - "$ref": "#/components/schemas/PermissionActionConfig" - }, - "todoread": { - "$ref": "#/components/schemas/PermissionActionConfig" - }, - "question": { - "$ref": "#/components/schemas/PermissionActionConfig" - }, - "webfetch": { - "$ref": "#/components/schemas/PermissionActionConfig" - }, - "websearch": { - "$ref": "#/components/schemas/PermissionActionConfig" - }, - "codesearch": { - "$ref": "#/components/schemas/PermissionActionConfig" - }, - "lsp": { - "$ref": "#/components/schemas/PermissionRuleConfig" - }, - "doom_loop": { - "$ref": "#/components/schemas/PermissionActionConfig" - } - }, - "additionalProperties": { - "$ref": "#/components/schemas/PermissionRuleConfig" - } - }, - { - "$ref": "#/components/schemas/PermissionActionConfig" - } - ] - }, - "AgentConfig": { - "type": "object", - "properties": { - "model": { - "type": "string" - }, - "temperature": { - "type": "number" - }, - "top_p": { - "type": "number" - }, - "prompt": { - "type": "string" - }, - "tools": { - "description": "@deprecated Use 'permission' field instead", - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": { - "type": "boolean" - } - }, - "disable": { - "type": "boolean" - }, - "description": { - "description": "Description of when to use the agent", - "type": "string" - }, - "mode": { - "type": "string", - "enum": [ - "subagent", - "primary", - "all" - ] - }, - "hidden": { - "description": "Hide this subagent from the @ autocomplete menu (default: false, only applies to mode: subagent)", - "type": "boolean" - }, - "options": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": {} - }, - "color": { - "description": "Hex color code for the agent (e.g., #FF5733)", - "type": "string", - "pattern": "^#[0-9a-fA-F]{6}$" - }, - "steps": { - "description": "Maximum number of agentic iterations before forcing text-only response", - "type": "integer", - "exclusiveMinimum": 0, - "maximum": 9007199254740991 - }, - "maxSteps": { - "description": "@deprecated Use 'steps' field instead.", - "type": "integer", - "exclusiveMinimum": 0, - "maximum": 9007199254740991 - }, - "permission": { - "$ref": "#/components/schemas/PermissionConfig" - } - }, - "additionalProperties": {} - }, - "ProviderConfig": { - "type": "object", - "properties": { - "api": { - "type": "string" - }, - "name": { - "type": "string" - }, - "env": { - "type": "array", - "items": { - "type": "string" - } - }, - "id": { - "type": "string" - }, - "npm": { - "type": "string" - }, - "models": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "family": { - "type": "string" - }, - "release_date": { - "type": "string" - }, - "attachment": { - "type": "boolean" - }, - "reasoning": { - "type": "boolean" - }, - "temperature": { - "type": "boolean" - }, - "tool_call": { - "type": "boolean" - }, - "interleaved": { - "anyOf": [ - { - "type": "boolean", - "const": true - }, - { - "type": "object", - "properties": { - "field": { - "type": "string", - "enum": [ - "reasoning_content", - "reasoning_details" - ] - } - }, - "required": [ - "field" - ], - "additionalProperties": false - } - ] - }, - "cost": { - "type": "object", - "properties": { - "input": { - "type": "number" - }, - "output": { - "type": "number" - }, - "cache_read": { - "type": "number" - }, - "cache_write": { - "type": "number" - }, - "context_over_200k": { - "type": "object", - "properties": { - "input": { - "type": "number" - }, - "output": { - "type": "number" - }, - "cache_read": { - "type": "number" - }, - "cache_write": { - "type": "number" - } - }, - "required": [ - "input", - "output" - ] - } - }, - "required": [ - "input", - "output" - ] - }, - "limit": { - "type": "object", - "properties": { - "context": { - "type": "number" - }, - "input": { - "type": "number" - }, - "output": { - "type": "number" - } - }, - "required": [ - "context", - "output" - ] - }, - "modalities": { - "type": "object", - "properties": { - "input": { - "type": "array", - "items": { - "type": "string", - "enum": [ - "text", - "audio", - "image", - "video", - "pdf" - ] - } - }, - "output": { - "type": "array", - "items": { - "type": "string", - "enum": [ - "text", - "audio", - "image", - "video", - "pdf" - ] - } - } - }, - "required": [ - "input", - "output" - ] - }, - "experimental": { - "type": "boolean" - }, - "status": { - "type": "string", - "enum": [ - "alpha", - "beta", - "deprecated" - ] - }, - "options": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": {} - }, - "headers": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": { - "type": "string" - } - }, - "provider": { - "type": "object", - "properties": { - "npm": { - "type": "string" - } - }, - "required": [ - "npm" - ] - }, - "variants": { - "description": "Variant-specific configuration", - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": { - "type": "object", - "properties": { - "disabled": { - "description": "Disable this variant for the model", - "type": "boolean" - } - }, - "additionalProperties": {} - } - } - } - } - }, - "whitelist": { - "type": "array", - "items": { - "type": "string" - } - }, - "blacklist": { - "type": "array", - "items": { - "type": "string" - } - }, - "options": { - "type": "object", - "properties": { - "apiKey": { - "type": "string" - }, - "baseURL": { - "type": "string" - }, - "enterpriseUrl": { - "description": "GitHub Enterprise URL for copilot authentication", - "type": "string" - }, - "setCacheKey": { - "description": "Enable promptCacheKey for this provider (default false)", - "type": "boolean" - }, - "timeout": { - "description": "Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout.", - "anyOf": [ - { - "description": "Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout.", - "type": "integer", - "exclusiveMinimum": 0, - "maximum": 9007199254740991 - }, - { - "description": "Disable timeout for this provider entirely.", - "type": "boolean", - "const": false - } - ] - } - }, - "additionalProperties": {} - } - }, - "additionalProperties": false - }, - "McpLocalConfig": { - "type": "object", - "properties": { - "type": { - "description": "Type of MCP server connection", - "type": "string", - "const": "local" - }, - "command": { - "description": "Command and arguments to run the MCP server", - "type": "array", - "items": { - "type": "string" - } - }, - "environment": { - "description": "Environment variables to set when running the MCP server", - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": { - "type": "string" - } - }, - "enabled": { - "description": "Enable or disable the MCP server on startup", - "type": "boolean" - }, - "timeout": { - "description": "Timeout in ms for MCP server requests. Defaults to 5000 (5 seconds) if not specified.", - "type": "integer", - "exclusiveMinimum": 0, - "maximum": 9007199254740991 - } - }, - "required": [ - "type", - "command" - ], - "additionalProperties": false - }, - "McpOAuthConfig": { - "type": "object", - "properties": { - "clientId": { - "description": "OAuth client ID. If not provided, dynamic client registration (RFC 7591) will be attempted.", - "type": "string" - }, - "clientSecret": { - "description": "OAuth client secret (if required by the authorization server)", - "type": "string" - }, - "scope": { - "description": "OAuth scopes to request during authorization", - "type": "string" - } - }, - "additionalProperties": false - }, - "McpRemoteConfig": { - "type": "object", - "properties": { - "type": { - "description": "Type of MCP server connection", - "type": "string", - "const": "remote" - }, - "url": { - "description": "URL of the remote MCP server", - "type": "string" - }, - "enabled": { - "description": "Enable or disable the MCP server on startup", - "type": "boolean" - }, - "headers": { - "description": "Headers to send with the request", - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": { - "type": "string" - } - }, - "oauth": { - "description": "OAuth authentication configuration for the MCP server. Set to false to disable OAuth auto-detection.", - "anyOf": [ - { - "$ref": "#/components/schemas/McpOAuthConfig" - }, - { - "type": "boolean", - "const": false - } - ] - }, - "timeout": { - "description": "Timeout in ms for MCP server requests. Defaults to 5000 (5 seconds) if not specified.", - "type": "integer", - "exclusiveMinimum": 0, - "maximum": 9007199254740991 - } - }, - "required": [ - "type", - "url" - ], - "additionalProperties": false - }, - "LayoutConfig": { - "description": "@deprecated Always uses stretch layout.", - "type": "string", - "enum": [ - "auto", - "stretch" - ] - }, - "Config": { - "type": "object", - "properties": { - "$schema": { - "description": "JSON schema reference for configuration validation", - "type": "string" - }, - "theme": { - "description": "Theme name to use for the interface", - "type": "string" - }, - "keybinds": { - "$ref": "#/components/schemas/KeybindsConfig" - }, - "logLevel": { - "$ref": "#/components/schemas/LogLevel" - }, - "tui": { - "description": "TUI specific settings", - "type": "object", - "properties": { - "scroll_speed": { - "description": "TUI scroll speed", - "type": "number", - "minimum": 0.001 - }, - "scroll_acceleration": { - "description": "Scroll acceleration settings", - "type": "object", - "properties": { - "enabled": { - "description": "Enable scroll acceleration", - "type": "boolean" - } - }, - "required": [ - "enabled" - ] - }, - "diff_style": { - "description": "Control diff rendering style: 'auto' adapts to terminal width, 'stacked' always shows single column", - "type": "string", - "enum": [ - "auto", - "stacked" - ] - } - } - }, - "server": { - "$ref": "#/components/schemas/ServerConfig" - }, - "command": { - "description": "Command configuration, see https://opencode.ai/docs/commands", - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": { - "type": "object", - "properties": { - "template": { - "type": "string" - }, - "description": { - "type": "string" - }, - "agent": { - "type": "string" - }, - "model": { - "type": "string" - }, - "subtask": { - "type": "boolean" - } - }, - "required": [ - "template" - ] - } - }, - "watcher": { - "type": "object", - "properties": { - "ignore": { - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "plugin": { - "type": "array", - "items": { - "type": "string" - } - }, - "snapshot": { - "type": "boolean" - }, - "share": { - "description": "Control sharing behavior:'manual' allows manual sharing via commands, 'auto' enables automatic sharing, 'disabled' disables all sharing", - "type": "string", - "enum": [ - "manual", - "auto", - "disabled" - ] - }, - "autoshare": { - "description": "@deprecated Use 'share' field instead. Share newly created sessions automatically", - "type": "boolean" - }, - "autoupdate": { - "description": "Automatically update to the latest version. Set to true to auto-update, false to disable, or 'notify' to show update notifications", - "anyOf": [ - { - "type": "boolean" - }, - { - "type": "string", - "const": "notify" - } - ] - }, - "disabled_providers": { - "description": "Disable providers that are loaded automatically", - "type": "array", - "items": { - "type": "string" - } - }, - "enabled_providers": { - "description": "When set, ONLY these providers will be enabled. All other providers will be ignored", - "type": "array", - "items": { - "type": "string" - } - }, - "model": { - "description": "Model to use in the format of provider/model, eg anthropic/claude-2", - "type": "string" - }, - "small_model": { - "description": "Small model to use for tasks like title generation in the format of provider/model", - "type": "string" - }, - "default_agent": { - "description": "Default agent to use when none is specified. Must be a primary agent. Falls back to 'build' if not set or if the specified agent is invalid.", - "type": "string" - }, - "username": { - "description": "Custom username to display in conversations instead of system username", - "type": "string" - }, - "mode": { - "description": "@deprecated Use `agent` field instead.", - "type": "object", - "properties": { - "build": { - "$ref": "#/components/schemas/AgentConfig" - }, - "plan": { - "$ref": "#/components/schemas/AgentConfig" - } - }, - "additionalProperties": { - "$ref": "#/components/schemas/AgentConfig" - } - }, - "agent": { - "description": "Agent configuration, see https://opencode.ai/docs/agents", - "type": "object", - "properties": { - "plan": { - "$ref": "#/components/schemas/AgentConfig" - }, - "build": { - "$ref": "#/components/schemas/AgentConfig" - }, - "general": { - "$ref": "#/components/schemas/AgentConfig" - }, - "explore": { - "$ref": "#/components/schemas/AgentConfig" - }, - "title": { - "$ref": "#/components/schemas/AgentConfig" - }, - "summary": { - "$ref": "#/components/schemas/AgentConfig" - }, - "compaction": { - "$ref": "#/components/schemas/AgentConfig" - } - }, - "additionalProperties": { - "$ref": "#/components/schemas/AgentConfig" - } - }, - "provider": { - "description": "Custom provider configurations and model overrides", - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": { - "$ref": "#/components/schemas/ProviderConfig" - } - }, - "mcp": { - "description": "MCP (Model Context Protocol) server configurations", - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": { - "anyOf": [ - { - "anyOf": [ - { - "$ref": "#/components/schemas/McpLocalConfig" - }, - { - "$ref": "#/components/schemas/McpRemoteConfig" - } - ] - }, - { - "type": "object", - "properties": { - "enabled": { - "type": "boolean" - } - }, - "required": [ - "enabled" - ], - "additionalProperties": false - } - ] - } - }, - "formatter": { - "anyOf": [ - { - "type": "boolean", - "const": false - }, - { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": { - "type": "object", - "properties": { - "disabled": { - "type": "boolean" - }, - "command": { - "type": "array", - "items": { - "type": "string" - } - }, - "environment": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": { - "type": "string" - } - }, - "extensions": { - "type": "array", - "items": { - "type": "string" - } - } - } - } - } - ] - }, - "lsp": { - "anyOf": [ - { - "type": "boolean", - "const": false - }, - { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": { - "anyOf": [ - { - "type": "object", - "properties": { - "disabled": { - "type": "boolean", - "const": true - } - }, - "required": [ - "disabled" - ] - }, - { - "type": "object", - "properties": { - "command": { - "type": "array", - "items": { - "type": "string" - } - }, - "extensions": { - "type": "array", - "items": { - "type": "string" - } - }, - "disabled": { - "type": "boolean" - }, - "env": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": { - "type": "string" - } - }, - "initialization": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": {} - } - }, - "required": [ - "command" - ] - } - ] - } - } - ] - }, - "instructions": { - "description": "Additional instruction files or patterns to include", - "type": "array", - "items": { - "type": "string" - } - }, - "layout": { - "$ref": "#/components/schemas/LayoutConfig" - }, - "permission": { - "$ref": "#/components/schemas/PermissionConfig" - }, - "tools": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": { - "type": "boolean" - } - }, - "enterprise": { - "type": "object", - "properties": { - "url": { - "description": "Enterprise URL", - "type": "string" - } - } - }, - "compaction": { - "type": "object", - "properties": { - "auto": { - "description": "Enable automatic compaction when context is full (default: true)", - "type": "boolean" - }, - "prune": { - "description": "Enable pruning of old tool outputs (default: true)", - "type": "boolean" - } - } - }, - "experimental": { - "type": "object", - "properties": { - "hook": { - "type": "object", - "properties": { - "file_edited": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": { - "type": "array", - "items": { - "type": "object", - "properties": { - "command": { - "type": "array", - "items": { - "type": "string" - } - }, - "environment": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": { - "type": "string" - } - } - }, - "required": [ - "command" - ] - } - } - }, - "session_completed": { - "type": "array", - "items": { - "type": "object", - "properties": { - "command": { - "type": "array", - "items": { - "type": "string" - } - }, - "environment": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": { - "type": "string" - } - } - }, - "required": [ - "command" - ] - } - } - } - }, - "chatMaxRetries": { - "description": "Number of retries for chat completions on failure", - "type": "number" - }, - "disable_paste_summary": { - "type": "boolean" - }, - "batch_tool": { - "description": "Enable the batch tool", - "type": "boolean" - }, - "openTelemetry": { - "description": "Enable OpenTelemetry spans for AI SDK calls (using the 'experimental_telemetry' flag)", - "type": "boolean" - }, - "primary_tools": { - "description": "Tools that should only be available to primary agents.", - "type": "array", - "items": { - "type": "string" - } - }, - "continue_loop_on_deny": { - "description": "Continue the agent loop when a tool call is denied", - "type": "boolean" - }, - "mcp_timeout": { - "description": "Timeout in milliseconds for model context protocol (MCP) requests", - "type": "integer", - "exclusiveMinimum": 0, - "maximum": 9007199254740991 - } - } - } - }, - "additionalProperties": false - }, - "Model": { + "read": { + "type": "number" + }, + "write": { + "type": "number" + } + }, + "required": ["read", "write"] + } + }, + "required": ["input", "output", "reasoning", "cache"] + }, + "finish": { + "type": "string" + } + }, + "required": [ + "id", + "sessionID", + "role", + "time", + "parentID", + "modelID", + "providerID", + "mode", + "agent", + "path", + "cost", + "tokens" + ] + }, + "Message": { + "anyOf": [ + { + "$ref": "#/components/schemas/UserMessage" + }, + { + "$ref": "#/components/schemas/AssistantMessage" + } + ] + }, + "Event.message.updated": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "message.updated" + }, + "properties": { + "type": "object", + "properties": { + "info": { + "$ref": "#/components/schemas/Message" + } + }, + "required": ["info"] + } + }, + "required": ["type", "properties"] + }, + "Event.message.removed": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "message.removed" + }, + "properties": { + "type": "object", + "properties": { + "sessionID": { + "type": "string" + }, + "messageID": { + "type": "string" + } + }, + "required": ["sessionID", "messageID"] + } + }, + "required": ["type", "properties"] + }, + "TextPart": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "sessionID": { + "type": "string" + }, + "messageID": { + "type": "string" + }, + "type": { + "type": "string", + "const": "text" + }, + "text": { + "type": "string" + }, + "synthetic": { + "type": "boolean" + }, + "ignored": { + "type": "boolean" + }, + "time": { + "type": "object", + "properties": { + "start": { + "type": "number" + }, + "end": { + "type": "number" + } + }, + "required": ["start"] + }, + "metadata": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "required": ["id", "sessionID", "messageID", "type", "text"] + }, + "ReasoningPart": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "sessionID": { + "type": "string" + }, + "messageID": { + "type": "string" + }, + "type": { + "type": "string", + "const": "reasoning" + }, + "text": { + "type": "string" + }, + "metadata": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "time": { + "type": "object", + "properties": { + "start": { + "type": "number" + }, + "end": { + "type": "number" + } + }, + "required": ["start"] + } + }, + "required": ["id", "sessionID", "messageID", "type", "text", "time"] + }, + "FilePartSourceText": { + "type": "object", + "properties": { + "value": { + "type": "string" + }, + "start": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + "end": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + } + }, + "required": ["value", "start", "end"] + }, + "FileSource": { + "type": "object", + "properties": { + "text": { + "$ref": "#/components/schemas/FilePartSourceText" + }, + "type": { + "type": "string", + "const": "file" + }, + "path": { + "type": "string" + } + }, + "required": ["text", "type", "path"] + }, + "Range": { + "type": "object", + "properties": { + "start": { + "type": "object", + "properties": { + "line": { + "type": "number" + }, + "character": { + "type": "number" + } + }, + "required": ["line", "character"] + }, + "end": { + "type": "object", + "properties": { + "line": { + "type": "number" + }, + "character": { + "type": "number" + } + }, + "required": ["line", "character"] + } + }, + "required": ["start", "end"] + }, + "SymbolSource": { + "type": "object", + "properties": { + "text": { + "$ref": "#/components/schemas/FilePartSourceText" + }, + "type": { + "type": "string", + "const": "symbol" + }, + "path": { + "type": "string" + }, + "range": { + "$ref": "#/components/schemas/Range" + }, + "name": { + "type": "string" + }, + "kind": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + } + }, + "required": ["text", "type", "path", "range", "name", "kind"] + }, + "ResourceSource": { + "type": "object", + "properties": { + "text": { + "$ref": "#/components/schemas/FilePartSourceText" + }, + "type": { + "type": "string", + "const": "resource" + }, + "clientName": { + "type": "string" + }, + "uri": { + "type": "string" + } + }, + "required": ["text", "type", "clientName", "uri"] + }, + "FilePartSource": { + "anyOf": [ + { + "$ref": "#/components/schemas/FileSource" + }, + { + "$ref": "#/components/schemas/SymbolSource" + }, + { + "$ref": "#/components/schemas/ResourceSource" + } + ] + }, + "FilePart": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "sessionID": { + "type": "string" + }, + "messageID": { + "type": "string" + }, + "type": { + "type": "string", + "const": "file" + }, + "mime": { + "type": "string" + }, + "filename": { + "type": "string" + }, + "url": { + "type": "string" + }, + "source": { + "$ref": "#/components/schemas/FilePartSource" + } + }, + "required": ["id", "sessionID", "messageID", "type", "mime", "url"] + }, + "ToolStatePending": { + "type": "object", + "properties": { + "status": { + "type": "string", + "const": "pending" + }, + "input": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "raw": { + "type": "string" + } + }, + "required": ["status", "input", "raw"] + }, + "ToolStateRunning": { + "type": "object", + "properties": { + "status": { + "type": "string", + "const": "running" + }, + "input": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "title": { + "type": "string" + }, + "metadata": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "time": { + "type": "object", + "properties": { + "start": { + "type": "number" + } + }, + "required": ["start"] + } + }, + "required": ["status", "input", "time"] + }, + "ToolStateCompleted": { + "type": "object", + "properties": { + "status": { + "type": "string", + "const": "completed" + }, + "input": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "output": { + "type": "string" + }, + "title": { + "type": "string" + }, + "metadata": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "time": { + "type": "object", + "properties": { + "start": { + "type": "number" + }, + "end": { + "type": "number" + }, + "compacted": { + "type": "number" + } + }, + "required": ["start", "end"] + }, + "attachments": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FilePart" + } + } + }, + "required": ["status", "input", "output", "title", "metadata", "time"] + }, + "ToolStateError": { + "type": "object", + "properties": { + "status": { + "type": "string", + "const": "error" + }, + "input": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "error": { + "type": "string" + }, + "metadata": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "time": { + "type": "object", + "properties": { + "start": { + "type": "number" + }, + "end": { + "type": "number" + } + }, + "required": ["start", "end"] + } + }, + "required": ["status", "input", "error", "time"] + }, + "ToolState": { + "anyOf": [ + { + "$ref": "#/components/schemas/ToolStatePending" + }, + { + "$ref": "#/components/schemas/ToolStateRunning" + }, + { + "$ref": "#/components/schemas/ToolStateCompleted" + }, + { + "$ref": "#/components/schemas/ToolStateError" + } + ] + }, + "ToolPart": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "sessionID": { + "type": "string" + }, + "messageID": { + "type": "string" + }, + "type": { + "type": "string", + "const": "tool" + }, + "callID": { + "type": "string" + }, + "tool": { + "type": "string" + }, + "state": { + "$ref": "#/components/schemas/ToolState" + }, + "metadata": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "required": ["id", "sessionID", "messageID", "type", "callID", "tool", "state"] + }, + "StepStartPart": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "sessionID": { + "type": "string" + }, + "messageID": { + "type": "string" + }, + "type": { + "type": "string", + "const": "step-start" + }, + "snapshot": { + "type": "string" + } + }, + "required": ["id", "sessionID", "messageID", "type"] + }, + "StepFinishPart": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "sessionID": { + "type": "string" + }, + "messageID": { + "type": "string" + }, + "type": { + "type": "string", + "const": "step-finish" + }, + "reason": { + "type": "string" + }, + "snapshot": { + "type": "string" + }, + "cost": { + "type": "number" + }, + "tokens": { + "type": "object", + "properties": { + "input": { + "type": "number" + }, + "output": { + "type": "number" + }, + "reasoning": { + "type": "number" + }, + "cache": { "type": "object", "properties": { - "id": { - "type": "string" - }, - "providerID": { - "type": "string" - }, - "api": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "url": { - "type": "string" - }, - "npm": { - "type": "string" - } - }, - "required": [ - "id", - "url", - "npm" - ] - }, - "name": { - "type": "string" - }, - "family": { - "type": "string" - }, - "capabilities": { - "type": "object", - "properties": { - "temperature": { - "type": "boolean" - }, - "reasoning": { - "type": "boolean" - }, - "attachment": { - "type": "boolean" - }, - "toolcall": { - "type": "boolean" - }, - "input": { - "type": "object", - "properties": { - "text": { - "type": "boolean" - }, - "audio": { - "type": "boolean" - }, - "image": { - "type": "boolean" - }, - "video": { - "type": "boolean" - }, - "pdf": { - "type": "boolean" - } - }, - "required": [ - "text", - "audio", - "image", - "video", - "pdf" - ] - }, - "output": { - "type": "object", - "properties": { - "text": { - "type": "boolean" - }, - "audio": { - "type": "boolean" - }, - "image": { - "type": "boolean" - }, - "video": { - "type": "boolean" - }, - "pdf": { - "type": "boolean" - } - }, - "required": [ - "text", - "audio", - "image", - "video", - "pdf" - ] - }, - "interleaved": { - "anyOf": [ - { - "type": "boolean" - }, - { - "type": "object", - "properties": { - "field": { - "type": "string", - "enum": [ - "reasoning_content", - "reasoning_details" - ] - } - }, - "required": [ - "field" - ] - } - ] - } - }, - "required": [ - "temperature", - "reasoning", - "attachment", - "toolcall", - "input", - "output", - "interleaved" - ] - }, - "cost": { - "type": "object", - "properties": { - "input": { - "type": "number" - }, - "output": { - "type": "number" - }, - "cache": { - "type": "object", - "properties": { - "read": { - "type": "number" - }, - "write": { - "type": "number" - } - }, - "required": [ - "read", - "write" - ] - }, - "experimentalOver200K": { - "type": "object", - "properties": { - "input": { - "type": "number" - }, - "output": { - "type": "number" - }, - "cache": { - "type": "object", - "properties": { - "read": { - "type": "number" - }, - "write": { - "type": "number" - } - }, - "required": [ - "read", - "write" - ] - } - }, - "required": [ - "input", - "output", - "cache" - ] - } - }, - "required": [ - "input", - "output", - "cache" - ] - }, - "limit": { - "type": "object", - "properties": { - "context": { - "type": "number" - }, - "input": { - "type": "number" - }, - "output": { - "type": "number" - } - }, - "required": [ - "context", - "output" - ] - }, - "status": { - "type": "string", - "enum": [ - "alpha", - "beta", - "deprecated", - "active" - ] - }, - "options": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": {} - }, - "headers": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": { - "type": "string" - } - }, - "release_date": { - "type": "string" - }, - "variants": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": {} - } - } - }, - "required": [ - "id", - "providerID", - "api", - "name", - "capabilities", - "cost", - "limit", - "status", - "options", - "headers", - "release_date" - ] - }, - "Provider": { + "read": { + "type": "number" + }, + "write": { + "type": "number" + } + }, + "required": ["read", "write"] + } + }, + "required": ["input", "output", "reasoning", "cache"] + } + }, + "required": ["id", "sessionID", "messageID", "type", "reason", "cost", "tokens"] + }, + "SnapshotPart": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "sessionID": { + "type": "string" + }, + "messageID": { + "type": "string" + }, + "type": { + "type": "string", + "const": "snapshot" + }, + "snapshot": { + "type": "string" + } + }, + "required": ["id", "sessionID", "messageID", "type", "snapshot"] + }, + "PatchPart": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "sessionID": { + "type": "string" + }, + "messageID": { + "type": "string" + }, + "type": { + "type": "string", + "const": "patch" + }, + "hash": { + "type": "string" + }, + "files": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": ["id", "sessionID", "messageID", "type", "hash", "files"] + }, + "AgentPart": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "sessionID": { + "type": "string" + }, + "messageID": { + "type": "string" + }, + "type": { + "type": "string", + "const": "agent" + }, + "name": { + "type": "string" + }, + "source": { + "type": "object", + "properties": { + "value": { + "type": "string" + }, + "start": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + "end": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + } + }, + "required": ["value", "start", "end"] + } + }, + "required": ["id", "sessionID", "messageID", "type", "name"] + }, + "RetryPart": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "sessionID": { + "type": "string" + }, + "messageID": { + "type": "string" + }, + "type": { + "type": "string", + "const": "retry" + }, + "attempt": { + "type": "number" + }, + "error": { + "$ref": "#/components/schemas/APIError" + }, + "time": { + "type": "object", + "properties": { + "created": { + "type": "number" + } + }, + "required": ["created"] + } + }, + "required": ["id", "sessionID", "messageID", "type", "attempt", "error", "time"] + }, + "CompactionPart": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "sessionID": { + "type": "string" + }, + "messageID": { + "type": "string" + }, + "type": { + "type": "string", + "const": "compaction" + }, + "auto": { + "type": "boolean" + } + }, + "required": ["id", "sessionID", "messageID", "type", "auto"] + }, + "Part": { + "anyOf": [ + { + "$ref": "#/components/schemas/TextPart" + }, + { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "sessionID": { + "type": "string" + }, + "messageID": { + "type": "string" + }, + "type": { + "type": "string", + "const": "subtask" + }, + "prompt": { + "type": "string" + }, + "description": { + "type": "string" + }, + "agent": { + "type": "string" + }, + "model": { "type": "object", "properties": { - "id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "source": { - "type": "string", - "enum": [ - "env", - "config", - "custom", - "api" - ] - }, - "env": { - "type": "array", - "items": { - "type": "string" - } - }, - "key": { - "type": "string" - }, - "options": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": {} - }, - "models": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": { - "$ref": "#/components/schemas/Model" - } - } - }, - "required": [ - "id", - "name", - "source", - "env", - "options", - "models" - ] - }, - "ToolIDs": { + "providerID": { + "type": "string" + }, + "modelID": { + "type": "string" + } + }, + "required": ["providerID", "modelID"] + }, + "command": { + "type": "string" + } + }, + "required": ["id", "sessionID", "messageID", "type", "prompt", "description", "agent"] + }, + { + "$ref": "#/components/schemas/ReasoningPart" + }, + { + "$ref": "#/components/schemas/FilePart" + }, + { + "$ref": "#/components/schemas/ToolPart" + }, + { + "$ref": "#/components/schemas/StepStartPart" + }, + { + "$ref": "#/components/schemas/StepFinishPart" + }, + { + "$ref": "#/components/schemas/SnapshotPart" + }, + { + "$ref": "#/components/schemas/PatchPart" + }, + { + "$ref": "#/components/schemas/AgentPart" + }, + { + "$ref": "#/components/schemas/RetryPart" + }, + { + "$ref": "#/components/schemas/CompactionPart" + } + ] + }, + "Event.message.part.updated": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "message.part.updated" + }, + "properties": { + "type": "object", + "properties": { + "part": { + "$ref": "#/components/schemas/Part" + }, + "delta": { + "type": "string" + } + }, + "required": ["part"] + } + }, + "required": ["type", "properties"] + }, + "Event.message.part.removed": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "message.part.removed" + }, + "properties": { + "type": "object", + "properties": { + "sessionID": { + "type": "string" + }, + "messageID": { + "type": "string" + }, + "partID": { + "type": "string" + } + }, + "required": ["sessionID", "messageID", "partID"] + } + }, + "required": ["type", "properties"] + }, + "Event.session.compacted": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "session.compacted" + }, + "properties": { + "type": "object", + "properties": { + "sessionID": { + "type": "string" + } + }, + "required": ["sessionID"] + } + }, + "required": ["type", "properties"] + }, + "PermissionAction": { + "type": "string", + "enum": ["allow", "deny", "ask"] + }, + "PermissionRule": { + "type": "object", + "properties": { + "permission": { + "type": "string" + }, + "pattern": { + "type": "string" + }, + "action": { + "$ref": "#/components/schemas/PermissionAction" + } + }, + "required": ["permission", "pattern", "action"] + }, + "PermissionRuleset": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PermissionRule" + } + }, + "Session": { + "type": "object", + "properties": { + "id": { + "type": "string", + "pattern": "^ses.*" + }, + "slug": { + "type": "string" + }, + "projectID": { + "type": "string" + }, + "directory": { + "type": "string" + }, + "parentID": { + "type": "string", + "pattern": "^ses.*" + }, + "summary": { + "type": "object", + "properties": { + "additions": { + "type": "number" + }, + "deletions": { + "type": "number" + }, + "files": { + "type": "number" + }, + "diffs": { "type": "array", "items": { - "type": "string" + "$ref": "#/components/schemas/FileDiff" } + } + }, + "required": ["additions", "deletions", "files"] + }, + "share": { + "type": "object", + "properties": { + "url": { + "type": "string" + } + }, + "required": ["url"] + }, + "title": { + "type": "string" + }, + "version": { + "type": "string" + }, + "time": { + "type": "object", + "properties": { + "created": { + "type": "number" + }, + "updated": { + "type": "number" + }, + "compacting": { + "type": "number" + }, + "archived": { + "type": "number" + } + }, + "required": ["created", "updated"] + }, + "permission": { + "$ref": "#/components/schemas/PermissionRuleset" + }, + "revert": { + "type": "object", + "properties": { + "messageID": { + "type": "string" + }, + "partID": { + "type": "string" + }, + "snapshot": { + "type": "string" + }, + "diff": { + "type": "string" + } + }, + "required": ["messageID"] + } + }, + "required": ["id", "slug", "projectID", "directory", "title", "version", "time"] + }, + "Event.session.created": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "session.created" + }, + "properties": { + "type": "object", + "properties": { + "info": { + "$ref": "#/components/schemas/Session" + } + }, + "required": ["info"] + } + }, + "required": ["type", "properties"] + }, + "Event.session.updated": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "session.updated" + }, + "properties": { + "type": "object", + "properties": { + "info": { + "$ref": "#/components/schemas/Session" + } + }, + "required": ["info"] + } + }, + "required": ["type", "properties"] + }, + "Event.session.deleted": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "session.deleted" + }, + "properties": { + "type": "object", + "properties": { + "info": { + "$ref": "#/components/schemas/Session" + } + }, + "required": ["info"] + } + }, + "required": ["type", "properties"] + }, + "Event.session.diff": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "session.diff" + }, + "properties": { + "type": "object", + "properties": { + "sessionID": { + "type": "string" + }, + "diff": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FileDiff" + } + } }, - "ToolListItem": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "description": { - "type": "string" - }, - "parameters": {} - }, - "required": [ - "id", - "description", - "parameters" + "required": ["sessionID", "diff"] + } + }, + "required": ["type", "properties"] + }, + "Event.session.error": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "session.error" + }, + "properties": { + "type": "object", + "properties": { + "sessionID": { + "type": "string" + }, + "error": { + "anyOf": [ + { + "$ref": "#/components/schemas/ProviderAuthError" + }, + { + "$ref": "#/components/schemas/UnknownError" + }, + { + "$ref": "#/components/schemas/MessageOutputLengthError" + }, + { + "$ref": "#/components/schemas/MessageAbortedError" + }, + { + "$ref": "#/components/schemas/APIError" + } ] - }, - "ToolList": { + } + } + } + }, + "required": ["type", "properties"] + }, + "Event": { + "anyOf": [ + { + "$ref": "#/components/schemas/Event.server.connected" + }, + { + "$ref": "#/components/schemas/Event.global.disposed" + }, + { + "$ref": "#/components/schemas/Event.tui.prompt.append" + }, + { + "$ref": "#/components/schemas/Event.tui.command.execute" + }, + { + "$ref": "#/components/schemas/Event.tui.toast.show" + }, + { + "$ref": "#/components/schemas/Event.tui.session.select" + }, + { + "$ref": "#/components/schemas/Event.installation.updated" + }, + { + "$ref": "#/components/schemas/Event.installation.update-available" + }, + { + "$ref": "#/components/schemas/Event.project.updated" + }, + { + "$ref": "#/components/schemas/Event.server.instance.disposed" + }, + { + "$ref": "#/components/schemas/Event.file.edited" + }, + { + "$ref": "#/components/schemas/Event.worktree.ready" + }, + { + "$ref": "#/components/schemas/Event.worktree.failed" + }, + { + "$ref": "#/components/schemas/Event.lsp.client.diagnostics" + }, + { + "$ref": "#/components/schemas/Event.permission.asked" + }, + { + "$ref": "#/components/schemas/Event.permission.replied" + }, + { + "$ref": "#/components/schemas/Event.session.status" + }, + { + "$ref": "#/components/schemas/Event.session.idle" + }, + { + "$ref": "#/components/schemas/Event.question.asked" + }, + { + "$ref": "#/components/schemas/Event.question.replied" + }, + { + "$ref": "#/components/schemas/Event.question.rejected" + }, + { + "$ref": "#/components/schemas/Event.todo.updated" + }, + { + "$ref": "#/components/schemas/Event.pty.created" + }, + { + "$ref": "#/components/schemas/Event.pty.updated" + }, + { + "$ref": "#/components/schemas/Event.pty.exited" + }, + { + "$ref": "#/components/schemas/Event.pty.deleted" + }, + { + "$ref": "#/components/schemas/Event.file.watcher.updated" + }, + { + "$ref": "#/components/schemas/Event.mcp.tools.changed" + }, + { + "$ref": "#/components/schemas/Event.mcp.browser.open.failed" + }, + { + "$ref": "#/components/schemas/Event.lsp.updated" + }, + { + "$ref": "#/components/schemas/Event.vcs.branch.updated" + }, + { + "$ref": "#/components/schemas/Event.command.executed" + }, + { + "$ref": "#/components/schemas/Event.message.updated" + }, + { + "$ref": "#/components/schemas/Event.message.removed" + }, + { + "$ref": "#/components/schemas/Event.message.part.updated" + }, + { + "$ref": "#/components/schemas/Event.message.part.removed" + }, + { + "$ref": "#/components/schemas/Event.session.compacted" + }, + { + "$ref": "#/components/schemas/Event.session.created" + }, + { + "$ref": "#/components/schemas/Event.session.updated" + }, + { + "$ref": "#/components/schemas/Event.session.deleted" + }, + { + "$ref": "#/components/schemas/Event.session.diff" + }, + { + "$ref": "#/components/schemas/Event.session.error" + } + ] + }, + "GlobalEvent": { + "type": "object", + "properties": { + "directory": { + "type": "string" + }, + "payload": { + "$ref": "#/components/schemas/Event" + } + }, + "required": ["directory", "payload"] + }, + "BadRequestError": { + "type": "object", + "properties": { + "data": {}, + "errors": { + "type": "array", + "items": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "success": { + "type": "boolean", + "const": false + } + }, + "required": ["data", "errors", "success"] + }, + "NotFoundError": { + "type": "object", + "properties": { + "name": { + "type": "string", + "const": "NotFoundError" + }, + "data": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": ["message"] + } + }, + "required": ["name", "data"] + }, + "KeybindsConfig": { + "description": "Custom keybind configurations", + "type": "object", + "properties": { + "leader": { + "description": "Leader key for keybind combinations", + "default": "ctrl+x", + "type": "string" + }, + "app_exit": { + "description": "Exit the application", + "default": "ctrl+c,ctrl+d,q", + "type": "string" + }, + "editor_open": { + "description": "Open external editor", + "default": "e", + "type": "string" + }, + "theme_list": { + "description": "List available themes", + "default": "t", + "type": "string" + }, + "sidebar_toggle": { + "description": "Toggle sidebar", + "default": "b", + "type": "string" + }, + "scrollbar_toggle": { + "description": "Toggle session scrollbar", + "default": "none", + "type": "string" + }, + "username_toggle": { + "description": "Toggle username visibility", + "default": "none", + "type": "string" + }, + "status_view": { + "description": "View status", + "default": "s", + "type": "string" + }, + "session_export": { + "description": "Export session to editor", + "default": "x", + "type": "string" + }, + "session_new": { + "description": "Create a new session", + "default": "n", + "type": "string" + }, + "session_list": { + "description": "List all sessions", + "default": "l", + "type": "string" + }, + "session_timeline": { + "description": "Show session timeline", + "default": "g", + "type": "string" + }, + "session_fork": { + "description": "Fork session from message", + "default": "none", + "type": "string" + }, + "session_rename": { + "description": "Rename session", + "default": "ctrl+r", + "type": "string" + }, + "session_delete": { + "description": "Delete session", + "default": "ctrl+d", + "type": "string" + }, + "stash_delete": { + "description": "Delete stash entry", + "default": "ctrl+d", + "type": "string" + }, + "model_provider_list": { + "description": "Open provider list from model dialog", + "default": "ctrl+a", + "type": "string" + }, + "model_favorite_toggle": { + "description": "Toggle model favorite status", + "default": "ctrl+f", + "type": "string" + }, + "session_share": { + "description": "Share current session", + "default": "none", + "type": "string" + }, + "session_unshare": { + "description": "Unshare current session", + "default": "none", + "type": "string" + }, + "session_interrupt": { + "description": "Interrupt current session", + "default": "escape", + "type": "string" + }, + "session_compact": { + "description": "Compact the session", + "default": "c", + "type": "string" + }, + "messages_page_up": { + "description": "Scroll messages up by one page", + "default": "pageup,ctrl+alt+b", + "type": "string" + }, + "messages_page_down": { + "description": "Scroll messages down by one page", + "default": "pagedown,ctrl+alt+f", + "type": "string" + }, + "messages_line_up": { + "description": "Scroll messages up by one line", + "default": "ctrl+alt+y", + "type": "string" + }, + "messages_line_down": { + "description": "Scroll messages down by one line", + "default": "ctrl+alt+e", + "type": "string" + }, + "messages_half_page_up": { + "description": "Scroll messages up by half page", + "default": "ctrl+alt+u", + "type": "string" + }, + "messages_half_page_down": { + "description": "Scroll messages down by half page", + "default": "ctrl+alt+d", + "type": "string" + }, + "messages_first": { + "description": "Navigate to first message", + "default": "ctrl+g,home", + "type": "string" + }, + "messages_last": { + "description": "Navigate to last message", + "default": "ctrl+alt+g,end", + "type": "string" + }, + "messages_next": { + "description": "Navigate to next message", + "default": "none", + "type": "string" + }, + "messages_previous": { + "description": "Navigate to previous message", + "default": "none", + "type": "string" + }, + "messages_last_user": { + "description": "Navigate to last user message", + "default": "none", + "type": "string" + }, + "messages_copy": { + "description": "Copy message", + "default": "y", + "type": "string" + }, + "messages_undo": { + "description": "Undo message", + "default": "u", + "type": "string" + }, + "messages_redo": { + "description": "Redo message", + "default": "r", + "type": "string" + }, + "messages_toggle_conceal": { + "description": "Toggle code block concealment in messages", + "default": "h", + "type": "string" + }, + "tool_details": { + "description": "Toggle tool details visibility", + "default": "none", + "type": "string" + }, + "model_list": { + "description": "List available models", + "default": "m", + "type": "string" + }, + "model_cycle_recent": { + "description": "Next recently used model", + "default": "f2", + "type": "string" + }, + "model_cycle_recent_reverse": { + "description": "Previous recently used model", + "default": "shift+f2", + "type": "string" + }, + "model_cycle_favorite": { + "description": "Next favorite model", + "default": "none", + "type": "string" + }, + "model_cycle_favorite_reverse": { + "description": "Previous favorite model", + "default": "none", + "type": "string" + }, + "command_list": { + "description": "List available commands", + "default": "ctrl+p", + "type": "string" + }, + "agent_list": { + "description": "List agents", + "default": "a", + "type": "string" + }, + "agent_cycle": { + "description": "Next agent", + "default": "tab", + "type": "string" + }, + "agent_cycle_reverse": { + "description": "Previous agent", + "default": "shift+tab", + "type": "string" + }, + "variant_cycle": { + "description": "Cycle model variants", + "default": "ctrl+t", + "type": "string" + }, + "input_clear": { + "description": "Clear input field", + "default": "ctrl+c", + "type": "string" + }, + "input_paste": { + "description": "Paste from clipboard", + "default": "ctrl+v", + "type": "string" + }, + "input_submit": { + "description": "Submit input", + "default": "return", + "type": "string" + }, + "input_newline": { + "description": "Insert newline in input", + "default": "shift+return,ctrl+return,alt+return,ctrl+j", + "type": "string" + }, + "input_move_left": { + "description": "Move cursor left in input", + "default": "left,ctrl+b", + "type": "string" + }, + "input_move_right": { + "description": "Move cursor right in input", + "default": "right,ctrl+f", + "type": "string" + }, + "input_move_up": { + "description": "Move cursor up in input", + "default": "up", + "type": "string" + }, + "input_move_down": { + "description": "Move cursor down in input", + "default": "down", + "type": "string" + }, + "input_select_left": { + "description": "Select left in input", + "default": "shift+left", + "type": "string" + }, + "input_select_right": { + "description": "Select right in input", + "default": "shift+right", + "type": "string" + }, + "input_select_up": { + "description": "Select up in input", + "default": "shift+up", + "type": "string" + }, + "input_select_down": { + "description": "Select down in input", + "default": "shift+down", + "type": "string" + }, + "input_line_home": { + "description": "Move to start of line in input", + "default": "ctrl+a", + "type": "string" + }, + "input_line_end": { + "description": "Move to end of line in input", + "default": "ctrl+e", + "type": "string" + }, + "input_select_line_home": { + "description": "Select to start of line in input", + "default": "ctrl+shift+a", + "type": "string" + }, + "input_select_line_end": { + "description": "Select to end of line in input", + "default": "ctrl+shift+e", + "type": "string" + }, + "input_visual_line_home": { + "description": "Move to start of visual line in input", + "default": "alt+a", + "type": "string" + }, + "input_visual_line_end": { + "description": "Move to end of visual line in input", + "default": "alt+e", + "type": "string" + }, + "input_select_visual_line_home": { + "description": "Select to start of visual line in input", + "default": "alt+shift+a", + "type": "string" + }, + "input_select_visual_line_end": { + "description": "Select to end of visual line in input", + "default": "alt+shift+e", + "type": "string" + }, + "input_buffer_home": { + "description": "Move to start of buffer in input", + "default": "home", + "type": "string" + }, + "input_buffer_end": { + "description": "Move to end of buffer in input", + "default": "end", + "type": "string" + }, + "input_select_buffer_home": { + "description": "Select to start of buffer in input", + "default": "shift+home", + "type": "string" + }, + "input_select_buffer_end": { + "description": "Select to end of buffer in input", + "default": "shift+end", + "type": "string" + }, + "input_delete_line": { + "description": "Delete line in input", + "default": "ctrl+shift+d", + "type": "string" + }, + "input_delete_to_line_end": { + "description": "Delete to end of line in input", + "default": "ctrl+k", + "type": "string" + }, + "input_delete_to_line_start": { + "description": "Delete to start of line in input", + "default": "ctrl+u", + "type": "string" + }, + "input_backspace": { + "description": "Backspace in input", + "default": "backspace,shift+backspace", + "type": "string" + }, + "input_delete": { + "description": "Delete character in input", + "default": "ctrl+d,delete,shift+delete", + "type": "string" + }, + "input_undo": { + "description": "Undo in input", + "default": "ctrl+-,super+z", + "type": "string" + }, + "input_redo": { + "description": "Redo in input", + "default": "ctrl+.,super+shift+z", + "type": "string" + }, + "input_word_forward": { + "description": "Move word forward in input", + "default": "alt+f,alt+right,ctrl+right", + "type": "string" + }, + "input_word_backward": { + "description": "Move word backward in input", + "default": "alt+b,alt+left,ctrl+left", + "type": "string" + }, + "input_select_word_forward": { + "description": "Select word forward in input", + "default": "alt+shift+f,alt+shift+right", + "type": "string" + }, + "input_select_word_backward": { + "description": "Select word backward in input", + "default": "alt+shift+b,alt+shift+left", + "type": "string" + }, + "input_delete_word_forward": { + "description": "Delete word forward in input", + "default": "alt+d,alt+delete,ctrl+delete", + "type": "string" + }, + "input_delete_word_backward": { + "description": "Delete word backward in input", + "default": "ctrl+w,ctrl+backspace,alt+backspace", + "type": "string" + }, + "history_previous": { + "description": "Previous history item", + "default": "up", + "type": "string" + }, + "history_next": { + "description": "Next history item", + "default": "down", + "type": "string" + }, + "session_child_cycle": { + "description": "Next child session", + "default": "right", + "type": "string" + }, + "session_child_cycle_reverse": { + "description": "Previous child session", + "default": "left", + "type": "string" + }, + "session_parent": { + "description": "Go to parent session", + "default": "up", + "type": "string" + }, + "terminal_suspend": { + "description": "Suspend terminal", + "default": "ctrl+z", + "type": "string" + }, + "terminal_title_toggle": { + "description": "Toggle terminal title", + "default": "none", + "type": "string" + }, + "tips_toggle": { + "description": "Toggle tips on home screen", + "default": "h", + "type": "string" + } + }, + "additionalProperties": false + }, + "LogLevel": { + "description": "Log level", + "type": "string", + "enum": ["DEBUG", "INFO", "WARN", "ERROR"] + }, + "ServerConfig": { + "description": "Server configuration for opencode serve and web commands", + "type": "object", + "properties": { + "port": { + "description": "Port to listen on", + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + "hostname": { + "description": "Hostname to listen on", + "type": "string" + }, + "mdns": { + "description": "Enable mDNS service discovery", + "type": "boolean" + }, + "cors": { + "description": "Additional domains to allow for CORS", + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + }, + "PermissionActionConfig": { + "type": "string", + "enum": ["ask", "allow", "deny"] + }, + "PermissionObjectConfig": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "$ref": "#/components/schemas/PermissionActionConfig" + } + }, + "PermissionRuleConfig": { + "anyOf": [ + { + "$ref": "#/components/schemas/PermissionActionConfig" + }, + { + "$ref": "#/components/schemas/PermissionObjectConfig" + } + ] + }, + "PermissionConfig": { + "anyOf": [ + { + "type": "object", + "properties": { + "__originalKeys": { "type": "array", "items": { - "$ref": "#/components/schemas/ToolListItem" + "type": "string" } + }, + "read": { + "$ref": "#/components/schemas/PermissionRuleConfig" + }, + "edit": { + "$ref": "#/components/schemas/PermissionRuleConfig" + }, + "glob": { + "$ref": "#/components/schemas/PermissionRuleConfig" + }, + "grep": { + "$ref": "#/components/schemas/PermissionRuleConfig" + }, + "list": { + "$ref": "#/components/schemas/PermissionRuleConfig" + }, + "bash": { + "$ref": "#/components/schemas/PermissionRuleConfig" + }, + "task": { + "$ref": "#/components/schemas/PermissionRuleConfig" + }, + "external_directory": { + "$ref": "#/components/schemas/PermissionRuleConfig" + }, + "todowrite": { + "$ref": "#/components/schemas/PermissionActionConfig" + }, + "todoread": { + "$ref": "#/components/schemas/PermissionActionConfig" + }, + "question": { + "$ref": "#/components/schemas/PermissionActionConfig" + }, + "webfetch": { + "$ref": "#/components/schemas/PermissionActionConfig" + }, + "websearch": { + "$ref": "#/components/schemas/PermissionActionConfig" + }, + "codesearch": { + "$ref": "#/components/schemas/PermissionActionConfig" + }, + "lsp": { + "$ref": "#/components/schemas/PermissionRuleConfig" + }, + "doom_loop": { + "$ref": "#/components/schemas/PermissionActionConfig" + } + }, + "additionalProperties": { + "$ref": "#/components/schemas/PermissionRuleConfig" + } + }, + { + "$ref": "#/components/schemas/PermissionActionConfig" + } + ] + }, + "AgentConfig": { + "type": "object", + "properties": { + "model": { + "type": "string" + }, + "temperature": { + "type": "number" + }, + "top_p": { + "type": "number" + }, + "prompt": { + "type": "string" + }, + "tools": { + "description": "@deprecated Use 'permission' field instead", + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "boolean" + } + }, + "disable": { + "type": "boolean" + }, + "description": { + "description": "Description of when to use the agent", + "type": "string" + }, + "mode": { + "type": "string", + "enum": ["subagent", "primary", "all"] + }, + "hidden": { + "description": "Hide this subagent from the @ autocomplete menu (default: false, only applies to mode: subagent)", + "type": "boolean" + }, + "options": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "color": { + "description": "Hex color code for the agent (e.g., #FF5733)", + "type": "string", + "pattern": "^#[0-9a-fA-F]{6}$" + }, + "steps": { + "description": "Maximum number of agentic iterations before forcing text-only response", + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + "maxSteps": { + "description": "@deprecated Use 'steps' field instead.", + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + "permission": { + "$ref": "#/components/schemas/PermissionConfig" + } + }, + "additionalProperties": {} + }, + "ProviderConfig": { + "type": "object", + "properties": { + "api": { + "type": "string" + }, + "name": { + "type": "string" + }, + "env": { + "type": "array", + "items": { + "type": "string" + } + }, + "id": { + "type": "string" + }, + "npm": { + "type": "string" + }, + "models": { + "type": "object", + "propertyNames": { + "type": "string" }, - "Worktree": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "branch": { - "type": "string" - }, - "directory": { - "type": "string" - } + "additionalProperties": { + "type": "object", + "properties": { + "id": { + "type": "string" }, - "required": [ - "name", - "branch", - "directory" - ] - }, - "WorktreeCreateInput": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "startCommand": { - "description": "Additional startup script to run after the project's start command", - "type": "string" - } - } - }, - "WorktreeRemoveInput": { - "type": "object", - "properties": { - "directory": { - "type": "string" - } + "name": { + "type": "string" }, - "required": [ - "directory" - ] - }, - "WorktreeResetInput": { - "type": "object", - "properties": { - "directory": { - "type": "string" - } + "family": { + "type": "string" }, - "required": [ - "directory" - ] - }, - "McpResource": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "uri": { - "type": "string" - }, - "description": { - "type": "string" - }, - "mimeType": { - "type": "string" - }, - "client": { - "type": "string" - } + "release_date": { + "type": "string" }, - "required": [ - "name", - "uri", - "client" - ] - }, - "TextPartInput": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "type": { - "type": "string", - "const": "text" - }, - "text": { - "type": "string" - }, - "synthetic": { - "type": "boolean" - }, - "ignored": { - "type": "boolean" - }, - "time": { - "type": "object", - "properties": { - "start": { - "type": "number" - }, - "end": { - "type": "number" - } - }, - "required": [ - "start" - ] - }, - "metadata": { - "type": "object", - "propertyNames": { - "type": "string" - }, - "additionalProperties": {} - } + "attachment": { + "type": "boolean" }, - "required": [ - "type", - "text" - ] - }, - "FilePartInput": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "type": { - "type": "string", - "const": "file" - }, - "mime": { - "type": "string" - }, - "filename": { - "type": "string" - }, - "url": { - "type": "string" - }, - "source": { - "$ref": "#/components/schemas/FilePartSource" - } + "reasoning": { + "type": "boolean" }, - "required": [ - "type", - "mime", - "url" - ] - }, - "AgentPartInput": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "type": { - "type": "string", - "const": "agent" - }, - "name": { - "type": "string" - }, - "source": { - "type": "object", - "properties": { - "value": { - "type": "string" - }, - "start": { - "type": "integer", - "minimum": -9007199254740991, - "maximum": 9007199254740991 - }, - "end": { - "type": "integer", - "minimum": -9007199254740991, - "maximum": 9007199254740991 - } - }, - "required": [ - "value", - "start", - "end" - ] - } + "temperature": { + "type": "boolean" }, - "required": [ - "type", - "name" - ] - }, - "SubtaskPartInput": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "type": { - "type": "string", - "const": "subtask" - }, - "prompt": { - "type": "string" - }, - "description": { - "type": "string" - }, - "agent": { - "type": "string" - }, - "model": { - "type": "object", - "properties": { - "providerID": { - "type": "string" - }, - "modelID": { - "type": "string" - } - }, - "required": [ - "providerID", - "modelID" - ] - }, - "command": { - "type": "string" - } + "tool_call": { + "type": "boolean" }, - "required": [ - "type", - "prompt", - "description", - "agent" - ] - }, - "ProviderAuthMethod": { - "type": "object", - "properties": { - "type": { - "anyOf": [ - { - "type": "string", - "const": "oauth" - }, - { - "type": "string", - "const": "api" - } - ] + "interleaved": { + "anyOf": [ + { + "type": "boolean", + "const": true }, - "label": { - "type": "string" + { + "type": "object", + "properties": { + "field": { + "type": "string", + "enum": ["reasoning_content", "reasoning_details"] + } + }, + "required": ["field"], + "additionalProperties": false } + ] }, - "required": [ - "type", - "label" - ] - }, - "ProviderAuthAuthorization": { - "type": "object", - "properties": { - "url": { - "type": "string" + "cost": { + "type": "object", + "properties": { + "input": { + "type": "number" }, - "method": { - "anyOf": [ - { - "type": "string", - "const": "auto" - }, - { - "type": "string", - "const": "code" - } - ] + "output": { + "type": "number" }, - "instructions": { - "type": "string" - } - }, - "required": [ - "url", - "method", - "instructions" - ] - }, - "Symbol": { - "type": "object", - "properties": { - "name": { - "type": "string" + "cache_read": { + "type": "number" }, - "kind": { - "type": "number" + "cache_write": { + "type": "number" }, - "location": { - "type": "object", - "properties": { - "uri": { - "type": "string" - }, - "range": { - "$ref": "#/components/schemas/Range" - } + "context_over_200k": { + "type": "object", + "properties": { + "input": { + "type": "number" + }, + "output": { + "type": "number" }, - "required": [ - "uri", - "range" - ] + "cache_read": { + "type": "number" + }, + "cache_write": { + "type": "number" + } + }, + "required": ["input", "output"] } + }, + "required": ["input", "output"] }, - "required": [ - "name", - "kind", - "location" - ] - }, - "FileNode": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "path": { - "type": "string" + "limit": { + "type": "object", + "properties": { + "context": { + "type": "number" }, - "absolute": { - "type": "string" - }, - "type": { - "type": "string", - "enum": [ - "file", - "directory" - ] + "input": { + "type": "number" }, - "ignored": { - "type": "boolean" + "output": { + "type": "number" } + }, + "required": ["context", "output"] }, - "required": [ - "name", - "path", - "absolute", - "type", - "ignored" - ] - }, - "FileContent": { - "type": "object", - "properties": { - "type": { + "modalities": { + "type": "object", + "properties": { + "input": { + "type": "array", + "items": { "type": "string", - "const": "text" - }, - "content": { - "type": "string" - }, - "diff": { - "type": "string" - }, - "patch": { - "type": "object", - "properties": { - "oldFileName": { - "type": "string" - }, - "newFileName": { - "type": "string" - }, - "oldHeader": { - "type": "string" - }, - "newHeader": { - "type": "string" - }, - "hunks": { - "type": "array", - "items": { - "type": "object", - "properties": { - "oldStart": { - "type": "number" - }, - "oldLines": { - "type": "number" - }, - "newStart": { - "type": "number" - }, - "newLines": { - "type": "number" - }, - "lines": { - "type": "array", - "items": { - "type": "string" - } - } - }, - "required": [ - "oldStart", - "oldLines", - "newStart", - "newLines", - "lines" - ] - } - }, - "index": { - "type": "string" - } - }, - "required": [ - "oldFileName", - "newFileName", - "hunks" - ] + "enum": ["text", "audio", "image", "video", "pdf"] + } }, - "encoding": { + "output": { + "type": "array", + "items": { "type": "string", - "const": "base64" - }, - "mimeType": { - "type": "string" + "enum": ["text", "audio", "image", "video", "pdf"] + } } + }, + "required": ["input", "output"] }, - "required": [ - "type", - "content" - ] - }, - "File": { - "type": "object", - "properties": { - "path": { - "type": "string" - }, - "added": { - "type": "integer", - "minimum": -9007199254740991, - "maximum": 9007199254740991 - }, - "removed": { - "type": "integer", - "minimum": -9007199254740991, - "maximum": 9007199254740991 - }, - "status": { - "type": "string", - "enum": [ - "added", - "deleted", - "modified" - ] - } + "experimental": { + "type": "boolean" }, - "required": [ - "path", - "added", - "removed", - "status" - ] - }, - "MCPStatusConnected": { - "type": "object", - "properties": { - "status": { - "type": "string", - "const": "connected" - } + "status": { + "type": "string", + "enum": ["alpha", "beta", "deprecated"] }, - "required": [ - "status" - ] - }, - "MCPStatusDisabled": { - "type": "object", - "properties": { - "status": { - "type": "string", - "const": "disabled" - } + "options": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} }, - "required": [ - "status" - ] - }, - "MCPStatusFailed": { - "type": "object", - "properties": { - "status": { - "type": "string", - "const": "failed" - }, - "error": { - "type": "string" - } + "headers": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string" + } }, - "required": [ - "status", - "error" - ] - }, - "MCPStatusNeedsAuth": { - "type": "object", - "properties": { - "status": { - "type": "string", - "const": "needs_auth" + "provider": { + "type": "object", + "properties": { + "npm": { + "type": "string" } + }, + "required": ["npm"] }, - "required": [ - "status" + "variants": { + "description": "Variant-specific configuration", + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "object", + "properties": { + "disabled": { + "description": "Disable this variant for the model", + "type": "boolean" + } + }, + "additionalProperties": {} + } + } + } + } + }, + "whitelist": { + "type": "array", + "items": { + "type": "string" + } + }, + "blacklist": { + "type": "array", + "items": { + "type": "string" + } + }, + "options": { + "type": "object", + "properties": { + "apiKey": { + "type": "string" + }, + "baseURL": { + "type": "string" + }, + "enterpriseUrl": { + "description": "GitHub Enterprise URL for copilot authentication", + "type": "string" + }, + "setCacheKey": { + "description": "Enable promptCacheKey for this provider (default false)", + "type": "boolean" + }, + "timeout": { + "description": "Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout.", + "anyOf": [ + { + "description": "Timeout in milliseconds for requests to this provider. Default is 300000 (5 minutes). Set to false to disable timeout.", + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + }, + { + "description": "Disable timeout for this provider entirely.", + "type": "boolean", + "const": false + } ] + } }, - "MCPStatusNeedsClientRegistration": { + "additionalProperties": {} + } + }, + "additionalProperties": false + }, + "McpLocalConfig": { + "type": "object", + "properties": { + "type": { + "description": "Type of MCP server connection", + "type": "string", + "const": "local" + }, + "command": { + "description": "Command and arguments to run the MCP server", + "type": "array", + "items": { + "type": "string" + } + }, + "environment": { + "description": "Environment variables to set when running the MCP server", + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string" + } + }, + "enabled": { + "description": "Enable or disable the MCP server on startup", + "type": "boolean" + }, + "timeout": { + "description": "Timeout in ms for MCP server requests. Defaults to 5000 (5 seconds) if not specified.", + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + } + }, + "required": ["type", "command"], + "additionalProperties": false + }, + "McpOAuthConfig": { + "type": "object", + "properties": { + "clientId": { + "description": "OAuth client ID. If not provided, dynamic client registration (RFC 7591) will be attempted.", + "type": "string" + }, + "clientSecret": { + "description": "OAuth client secret (if required by the authorization server)", + "type": "string" + }, + "scope": { + "description": "OAuth scopes to request during authorization", + "type": "string" + } + }, + "additionalProperties": false + }, + "McpRemoteConfig": { + "type": "object", + "properties": { + "type": { + "description": "Type of MCP server connection", + "type": "string", + "const": "remote" + }, + "url": { + "description": "URL of the remote MCP server", + "type": "string" + }, + "enabled": { + "description": "Enable or disable the MCP server on startup", + "type": "boolean" + }, + "headers": { + "description": "Headers to send with the request", + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string" + } + }, + "oauth": { + "description": "OAuth authentication configuration for the MCP server. Set to false to disable OAuth auto-detection.", + "anyOf": [ + { + "$ref": "#/components/schemas/McpOAuthConfig" + }, + { + "type": "boolean", + "const": false + } + ] + }, + "timeout": { + "description": "Timeout in ms for MCP server requests. Defaults to 5000 (5 seconds) if not specified.", + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + } + }, + "required": ["type", "url"], + "additionalProperties": false + }, + "LayoutConfig": { + "description": "@deprecated Always uses stretch layout.", + "type": "string", + "enum": ["auto", "stretch"] + }, + "Config": { + "type": "object", + "properties": { + "$schema": { + "description": "JSON schema reference for configuration validation", + "type": "string" + }, + "theme": { + "description": "Theme name to use for the interface", + "type": "string" + }, + "keybinds": { + "$ref": "#/components/schemas/KeybindsConfig" + }, + "logLevel": { + "$ref": "#/components/schemas/LogLevel" + }, + "tui": { + "description": "TUI specific settings", + "type": "object", + "properties": { + "scroll_speed": { + "description": "TUI scroll speed", + "type": "number", + "minimum": 0.001 + }, + "scroll_acceleration": { + "description": "Scroll acceleration settings", "type": "object", "properties": { - "status": { - "type": "string", - "const": "needs_client_registration" - }, - "error": { - "type": "string" - } - }, - "required": [ - "status", - "error" - ] + "enabled": { + "description": "Enable scroll acceleration", + "type": "boolean" + } + }, + "required": ["enabled"] + }, + "diff_style": { + "description": "Control diff rendering style: 'auto' adapts to terminal width, 'stacked' always shows single column", + "type": "string", + "enum": ["auto", "stacked"] + } + } + }, + "server": { + "$ref": "#/components/schemas/ServerConfig" + }, + "command": { + "description": "Command configuration, see https://opencode.ai/docs/commands", + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "object", + "properties": { + "template": { + "type": "string" + }, + "description": { + "type": "string" + }, + "agent": { + "type": "string" + }, + "model": { + "type": "string" + }, + "subtask": { + "type": "boolean" + } + }, + "required": ["template"] + } + }, + "watcher": { + "type": "object", + "properties": { + "ignore": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "plugin": { + "type": "array", + "items": { + "type": "string" + } + }, + "snapshot": { + "type": "boolean" + }, + "share": { + "description": "Control sharing behavior:'manual' allows manual sharing via commands, 'auto' enables automatic sharing, 'disabled' disables all sharing", + "type": "string", + "enum": ["manual", "auto", "disabled"] + }, + "autoshare": { + "description": "@deprecated Use 'share' field instead. Share newly created sessions automatically", + "type": "boolean" + }, + "autoupdate": { + "description": "Automatically update to the latest version. Set to true to auto-update, false to disable, or 'notify' to show update notifications", + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "string", + "const": "notify" + } + ] + }, + "disabled_providers": { + "description": "Disable providers that are loaded automatically", + "type": "array", + "items": { + "type": "string" + } + }, + "enabled_providers": { + "description": "When set, ONLY these providers will be enabled. All other providers will be ignored", + "type": "array", + "items": { + "type": "string" + } + }, + "model": { + "description": "Model to use in the format of provider/model, eg anthropic/claude-2", + "type": "string" + }, + "small_model": { + "description": "Small model to use for tasks like title generation in the format of provider/model", + "type": "string" + }, + "default_agent": { + "description": "Default agent to use when none is specified. Must be a primary agent. Falls back to 'build' if not set or if the specified agent is invalid.", + "type": "string" + }, + "username": { + "description": "Custom username to display in conversations instead of system username", + "type": "string" + }, + "mode": { + "description": "@deprecated Use `agent` field instead.", + "type": "object", + "properties": { + "build": { + "$ref": "#/components/schemas/AgentConfig" + }, + "plan": { + "$ref": "#/components/schemas/AgentConfig" + } + }, + "additionalProperties": { + "$ref": "#/components/schemas/AgentConfig" + } + }, + "agent": { + "description": "Agent configuration, see https://opencode.ai/docs/agents", + "type": "object", + "properties": { + "plan": { + "$ref": "#/components/schemas/AgentConfig" + }, + "build": { + "$ref": "#/components/schemas/AgentConfig" + }, + "general": { + "$ref": "#/components/schemas/AgentConfig" + }, + "explore": { + "$ref": "#/components/schemas/AgentConfig" + }, + "title": { + "$ref": "#/components/schemas/AgentConfig" + }, + "summary": { + "$ref": "#/components/schemas/AgentConfig" + }, + "compaction": { + "$ref": "#/components/schemas/AgentConfig" + } + }, + "additionalProperties": { + "$ref": "#/components/schemas/AgentConfig" + } + }, + "provider": { + "description": "Custom provider configurations and model overrides", + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "$ref": "#/components/schemas/ProviderConfig" + } + }, + "mcp": { + "description": "MCP (Model Context Protocol) server configurations", + "type": "object", + "propertyNames": { + "type": "string" }, - "MCPStatus": { - "anyOf": [ - { - "$ref": "#/components/schemas/MCPStatusConnected" - }, - { - "$ref": "#/components/schemas/MCPStatusDisabled" - }, - { - "$ref": "#/components/schemas/MCPStatusFailed" - }, + "additionalProperties": { + "anyOf": [ + { + "anyOf": [ { - "$ref": "#/components/schemas/MCPStatusNeedsAuth" + "$ref": "#/components/schemas/McpLocalConfig" }, { - "$ref": "#/components/schemas/MCPStatusNeedsClientRegistration" + "$ref": "#/components/schemas/McpRemoteConfig" } - ] - }, - "Path": { + ] + }, + { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + } + }, + "required": ["enabled"], + "additionalProperties": false + } + ] + } + }, + "formatter": { + "anyOf": [ + { + "type": "boolean", + "const": false + }, + { "type": "object", - "properties": { - "home": { - "type": "string" + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "object", + "properties": { + "disabled": { + "type": "boolean" }, - "state": { + "command": { + "type": "array", + "items": { "type": "string" + } }, - "config": { + "environment": { + "type": "object", + "propertyNames": { "type": "string" - }, - "worktree": { + }, + "additionalProperties": { "type": "string" + } }, - "directory": { + "extensions": { + "type": "array", + "items": { "type": "string" + } } - }, - "required": [ - "home", - "state", - "config", - "worktree", - "directory" - ] - }, - "VcsInfo": { + } + } + } + ] + }, + "lsp": { + "anyOf": [ + { + "type": "boolean", + "const": false + }, + { "type": "object", - "properties": { - "branch": { - "type": "string" - } + "propertyNames": { + "type": "string" }, - "required": [ - "branch" - ] - }, - "Command": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "description": { - "type": "string" - }, - "agent": { - "type": "string" - }, - "model": { - "type": "string" - }, - "mcp": { - "type": "boolean" - }, - "template": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "string" - } - ] - }, - "subtask": { - "type": "boolean" + "additionalProperties": { + "anyOf": [ + { + "type": "object", + "properties": { + "disabled": { + "type": "boolean", + "const": true + } + }, + "required": ["disabled"] }, - "hints": { - "type": "array", - "items": { + { + "type": "object", + "properties": { + "command": { + "type": "array", + "items": { + "type": "string" + } + }, + "extensions": { + "type": "array", + "items": { + "type": "string" + } + }, + "disabled": { + "type": "boolean" + }, + "env": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string" + } + }, + "initialization": { + "type": "object", + "propertyNames": { "type": "string" + }, + "additionalProperties": {} } + }, + "required": ["command"] } - }, - "required": [ - "name", - "template", - "hints" - ] - }, - "Agent": { + ] + } + } + ] + }, + "instructions": { + "description": "Additional instruction files or patterns to include", + "type": "array", + "items": { + "type": "string" + } + }, + "layout": { + "$ref": "#/components/schemas/LayoutConfig" + }, + "permission": { + "$ref": "#/components/schemas/PermissionConfig" + }, + "tools": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "boolean" + } + }, + "enterprise": { + "type": "object", + "properties": { + "url": { + "description": "Enterprise URL", + "type": "string" + } + } + }, + "compaction": { + "type": "object", + "properties": { + "auto": { + "description": "Enable automatic compaction when context is full (default: true)", + "type": "boolean" + }, + "prune": { + "description": "Enable pruning of old tool outputs (default: true)", + "type": "boolean" + } + } + }, + "experimental": { + "type": "object", + "properties": { + "hook": { "type": "object", "properties": { - "name": { - "type": "string" - }, - "description": { - "type": "string" - }, - "mode": { - "type": "string", - "enum": [ - "subagent", - "primary", - "all" - ] - }, - "native": { - "type": "boolean" - }, - "hidden": { - "type": "boolean" - }, - "topP": { - "type": "number" - }, - "temperature": { - "type": "number" - }, - "color": { - "type": "string" - }, - "permission": { - "$ref": "#/components/schemas/PermissionRuleset" - }, - "model": { + "file_edited": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "array", + "items": { "type": "object", "properties": { - "modelID": { - "type": "string" + "command": { + "type": "array", + "items": { + "type": "string" + } + }, + "environment": { + "type": "object", + "propertyNames": { + "type": "string" }, - "providerID": { - "type": "string" + "additionalProperties": { + "type": "string" } + } }, - "required": [ - "modelID", - "providerID" - ] - }, - "prompt": { - "type": "string" - }, - "options": { - "type": "object", - "propertyNames": { + "required": ["command"] + } + } + }, + "session_completed": { + "type": "array", + "items": { + "type": "object", + "properties": { + "command": { + "type": "array", + "items": { "type": "string" + } }, - "additionalProperties": {} - }, - "steps": { - "type": "integer", - "exclusiveMinimum": 0, - "maximum": 9007199254740991 - } - }, - "required": [ - "name", - "mode", - "permission", - "options" - ] - }, - "LSPStatus": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "root": { - "type": "string" - }, - "status": { - "anyOf": [ - { - "type": "string", - "const": "connected" - }, - { - "type": "string", - "const": "error" - } - ] - } - }, - "required": [ - "id", - "name", - "root", - "status" - ] - }, - "FormatterStatus": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "extensions": { - "type": "array", - "items": { + "environment": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { "type": "string" + } } - }, - "enabled": { - "type": "boolean" + }, + "required": ["command"] } - }, - "required": [ - "name", - "extensions", - "enabled" - ] - }, - "OAuth": { + } + } + }, + "chatMaxRetries": { + "description": "Number of retries for chat completions on failure", + "type": "number" + }, + "disable_paste_summary": { + "type": "boolean" + }, + "batch_tool": { + "description": "Enable the batch tool", + "type": "boolean" + }, + "openTelemetry": { + "description": "Enable OpenTelemetry spans for AI SDK calls (using the 'experimental_telemetry' flag)", + "type": "boolean" + }, + "primary_tools": { + "description": "Tools that should only be available to primary agents.", + "type": "array", + "items": { + "type": "string" + } + }, + "continue_loop_on_deny": { + "description": "Continue the agent loop when a tool call is denied", + "type": "boolean" + }, + "mcp_timeout": { + "description": "Timeout in milliseconds for model context protocol (MCP) requests", + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + } + } + } + }, + "additionalProperties": false + }, + "Model": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "providerID": { + "type": "string" + }, + "api": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "url": { + "type": "string" + }, + "npm": { + "type": "string" + } + }, + "required": ["id", "url", "npm"] + }, + "name": { + "type": "string" + }, + "family": { + "type": "string" + }, + "capabilities": { + "type": "object", + "properties": { + "temperature": { + "type": "boolean" + }, + "reasoning": { + "type": "boolean" + }, + "attachment": { + "type": "boolean" + }, + "toolcall": { + "type": "boolean" + }, + "input": { "type": "object", "properties": { - "type": { - "type": "string", - "const": "oauth" - }, - "refresh": { - "type": "string" - }, - "access": { - "type": "string" - }, - "expires": { - "type": "number" - }, - "accountId": { - "type": "string" - }, - "enterpriseUrl": { - "type": "string" - } - }, - "required": [ - "type", - "refresh", - "access", - "expires" - ] - }, - "ApiAuth": { + "text": { + "type": "boolean" + }, + "audio": { + "type": "boolean" + }, + "image": { + "type": "boolean" + }, + "video": { + "type": "boolean" + }, + "pdf": { + "type": "boolean" + } + }, + "required": ["text", "audio", "image", "video", "pdf"] + }, + "output": { "type": "object", "properties": { - "type": { + "text": { + "type": "boolean" + }, + "audio": { + "type": "boolean" + }, + "image": { + "type": "boolean" + }, + "video": { + "type": "boolean" + }, + "pdf": { + "type": "boolean" + } + }, + "required": ["text", "audio", "image", "video", "pdf"] + }, + "interleaved": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "object", + "properties": { + "field": { "type": "string", - "const": "api" + "enum": ["reasoning_content", "reasoning_details"] + } }, - "key": { - "type": "string" - } - }, - "required": [ - "type", - "key" + "required": ["field"] + } ] - }, - "WellKnownAuth": { + } + }, + "required": ["temperature", "reasoning", "attachment", "toolcall", "input", "output", "interleaved"] + }, + "cost": { + "type": "object", + "properties": { + "input": { + "type": "number" + }, + "output": { + "type": "number" + }, + "cache": { "type": "object", "properties": { - "type": { - "type": "string", - "const": "wellknown" + "read": { + "type": "number" + }, + "write": { + "type": "number" + } + }, + "required": ["read", "write"] + }, + "experimentalOver200K": { + "type": "object", + "properties": { + "input": { + "type": "number" + }, + "output": { + "type": "number" + }, + "cache": { + "type": "object", + "properties": { + "read": { + "type": "number" + }, + "write": { + "type": "number" + } + }, + "required": ["read", "write"] + } + }, + "required": ["input", "output", "cache"] + } + }, + "required": ["input", "output", "cache"] + }, + "limit": { + "type": "object", + "properties": { + "context": { + "type": "number" + }, + "input": { + "type": "number" + }, + "output": { + "type": "number" + } + }, + "required": ["context", "output"] + }, + "status": { + "type": "string", + "enum": ["alpha", "beta", "deprecated", "active"] + }, + "options": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "headers": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "string" + } + }, + "release_date": { + "type": "string" + }, + "variants": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + } + }, + "required": [ + "id", + "providerID", + "api", + "name", + "capabilities", + "cost", + "limit", + "status", + "options", + "headers", + "release_date" + ] + }, + "Provider": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "source": { + "type": "string", + "enum": ["env", "config", "custom", "api"] + }, + "env": { + "type": "array", + "items": { + "type": "string" + } + }, + "key": { + "type": "string" + }, + "options": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "models": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "$ref": "#/components/schemas/Model" + } + } + }, + "required": ["id", "name", "source", "env", "options", "models"] + }, + "ToolIDs": { + "type": "array", + "items": { + "type": "string" + } + }, + "ToolListItem": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "description": { + "type": "string" + }, + "parameters": {} + }, + "required": ["id", "description", "parameters"] + }, + "ToolList": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ToolListItem" + } + }, + "Worktree": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "branch": { + "type": "string" + }, + "directory": { + "type": "string" + } + }, + "required": ["name", "branch", "directory"] + }, + "WorktreeCreateInput": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "startCommand": { + "description": "Additional startup script to run after the project's start command", + "type": "string" + } + } + }, + "WorktreeRemoveInput": { + "type": "object", + "properties": { + "directory": { + "type": "string" + } + }, + "required": ["directory"] + }, + "WorktreeResetInput": { + "type": "object", + "properties": { + "directory": { + "type": "string" + } + }, + "required": ["directory"] + }, + "McpResource": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "uri": { + "type": "string" + }, + "description": { + "type": "string" + }, + "mimeType": { + "type": "string" + }, + "client": { + "type": "string" + } + }, + "required": ["name", "uri", "client"] + }, + "TextPartInput": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "const": "text" + }, + "text": { + "type": "string" + }, + "synthetic": { + "type": "boolean" + }, + "ignored": { + "type": "boolean" + }, + "time": { + "type": "object", + "properties": { + "start": { + "type": "number" + }, + "end": { + "type": "number" + } + }, + "required": ["start"] + }, + "metadata": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "required": ["type", "text"] + }, + "FilePartInput": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "const": "file" + }, + "mime": { + "type": "string" + }, + "filename": { + "type": "string" + }, + "url": { + "type": "string" + }, + "source": { + "$ref": "#/components/schemas/FilePartSource" + } + }, + "required": ["type", "mime", "url"] + }, + "AgentPartInput": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "const": "agent" + }, + "name": { + "type": "string" + }, + "source": { + "type": "object", + "properties": { + "value": { + "type": "string" + }, + "start": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + "end": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + } + }, + "required": ["value", "start", "end"] + } + }, + "required": ["type", "name"] + }, + "SubtaskPartInput": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "const": "subtask" + }, + "prompt": { + "type": "string" + }, + "description": { + "type": "string" + }, + "agent": { + "type": "string" + }, + "model": { + "type": "object", + "properties": { + "providerID": { + "type": "string" + }, + "modelID": { + "type": "string" + } + }, + "required": ["providerID", "modelID"] + }, + "command": { + "type": "string" + } + }, + "required": ["type", "prompt", "description", "agent"] + }, + "ProviderAuthMethod": { + "type": "object", + "properties": { + "type": { + "anyOf": [ + { + "type": "string", + "const": "oauth" + }, + { + "type": "string", + "const": "api" + } + ] + }, + "label": { + "type": "string" + } + }, + "required": ["type", "label"] + }, + "ProviderAuthAuthorization": { + "type": "object", + "properties": { + "url": { + "type": "string" + }, + "method": { + "anyOf": [ + { + "type": "string", + "const": "auto" + }, + { + "type": "string", + "const": "code" + } + ] + }, + "instructions": { + "type": "string" + } + }, + "required": ["url", "method", "instructions"] + }, + "Symbol": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "kind": { + "type": "number" + }, + "location": { + "type": "object", + "properties": { + "uri": { + "type": "string" + }, + "range": { + "$ref": "#/components/schemas/Range" + } + }, + "required": ["uri", "range"] + } + }, + "required": ["name", "kind", "location"] + }, + "FileNode": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "path": { + "type": "string" + }, + "absolute": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["file", "directory"] + }, + "ignored": { + "type": "boolean" + } + }, + "required": ["name", "path", "absolute", "type", "ignored"] + }, + "FileContent": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "text" + }, + "content": { + "type": "string" + }, + "diff": { + "type": "string" + }, + "patch": { + "type": "object", + "properties": { + "oldFileName": { + "type": "string" + }, + "newFileName": { + "type": "string" + }, + "oldHeader": { + "type": "string" + }, + "newHeader": { + "type": "string" + }, + "hunks": { + "type": "array", + "items": { + "type": "object", + "properties": { + "oldStart": { + "type": "number" }, - "key": { - "type": "string" + "oldLines": { + "type": "number" }, - "token": { - "type": "string" - } - }, - "required": [ - "type", - "key", - "token" - ] - }, - "Auth": { - "anyOf": [ - { - "$ref": "#/components/schemas/OAuth" + "newStart": { + "type": "number" }, - { - "$ref": "#/components/schemas/ApiAuth" + "newLines": { + "type": "number" }, - { - "$ref": "#/components/schemas/WellKnownAuth" + "lines": { + "type": "array", + "items": { + "type": "string" + } } - ] + }, + "required": ["oldStart", "oldLines", "newStart", "newLines", "lines"] + } + }, + "index": { + "type": "string" + } + }, + "required": ["oldFileName", "newFileName", "hunks"] + }, + "encoding": { + "type": "string", + "const": "base64" + }, + "mimeType": { + "type": "string" + } + }, + "required": ["type", "content"] + }, + "File": { + "type": "object", + "properties": { + "path": { + "type": "string" + }, + "added": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + "removed": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + "status": { + "type": "string", + "enum": ["added", "deleted", "modified"] + } + }, + "required": ["path", "added", "removed", "status"] + }, + "MCPStatusConnected": { + "type": "object", + "properties": { + "status": { + "type": "string", + "const": "connected" + } + }, + "required": ["status"] + }, + "MCPStatusDisabled": { + "type": "object", + "properties": { + "status": { + "type": "string", + "const": "disabled" + } + }, + "required": ["status"] + }, + "MCPStatusFailed": { + "type": "object", + "properties": { + "status": { + "type": "string", + "const": "failed" + }, + "error": { + "type": "string" + } + }, + "required": ["status", "error"] + }, + "MCPStatusNeedsAuth": { + "type": "object", + "properties": { + "status": { + "type": "string", + "const": "needs_auth" + } + }, + "required": ["status"] + }, + "MCPStatusNeedsClientRegistration": { + "type": "object", + "properties": { + "status": { + "type": "string", + "const": "needs_client_registration" + }, + "error": { + "type": "string" + } + }, + "required": ["status", "error"] + }, + "MCPStatus": { + "anyOf": [ + { + "$ref": "#/components/schemas/MCPStatusConnected" + }, + { + "$ref": "#/components/schemas/MCPStatusDisabled" + }, + { + "$ref": "#/components/schemas/MCPStatusFailed" + }, + { + "$ref": "#/components/schemas/MCPStatusNeedsAuth" + }, + { + "$ref": "#/components/schemas/MCPStatusNeedsClientRegistration" + } + ] + }, + "Path": { + "type": "object", + "properties": { + "home": { + "type": "string" + }, + "state": { + "type": "string" + }, + "config": { + "type": "string" + }, + "worktree": { + "type": "string" + }, + "directory": { + "type": "string" + } + }, + "required": ["home", "state", "config", "worktree", "directory"] + }, + "VcsInfo": { + "type": "object", + "properties": { + "branch": { + "type": "string" + } + }, + "required": ["branch"] + }, + "Command": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "agent": { + "type": "string" + }, + "model": { + "type": "string" + }, + "mcp": { + "type": "boolean" + }, + "template": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "string" + } + ] + }, + "subtask": { + "type": "boolean" + }, + "hints": { + "type": "array", + "items": { + "type": "string" } - } + } + }, + "required": ["name", "template", "hints"] + }, + "Agent": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "mode": { + "type": "string", + "enum": ["subagent", "primary", "all"] + }, + "native": { + "type": "boolean" + }, + "hidden": { + "type": "boolean" + }, + "topP": { + "type": "number" + }, + "temperature": { + "type": "number" + }, + "color": { + "type": "string" + }, + "permission": { + "$ref": "#/components/schemas/PermissionRuleset" + }, + "model": { + "type": "object", + "properties": { + "modelID": { + "type": "string" + }, + "providerID": { + "type": "string" + } + }, + "required": ["modelID", "providerID"] + }, + "prompt": { + "type": "string" + }, + "options": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "steps": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + } + }, + "required": ["name", "mode", "permission", "options"] + }, + "LSPStatus": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "root": { + "type": "string" + }, + "status": { + "anyOf": [ + { + "type": "string", + "const": "connected" + }, + { + "type": "string", + "const": "error" + } + ] + } + }, + "required": ["id", "name", "root", "status"] + }, + "FormatterStatus": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "extensions": { + "type": "array", + "items": { + "type": "string" + } + }, + "enabled": { + "type": "boolean" + } + }, + "required": ["name", "extensions", "enabled"] + }, + "OAuth": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "oauth" + }, + "refresh": { + "type": "string" + }, + "access": { + "type": "string" + }, + "expires": { + "type": "number" + }, + "accountId": { + "type": "string" + }, + "enterpriseUrl": { + "type": "string" + } + }, + "required": ["type", "refresh", "access", "expires"] + }, + "ApiAuth": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "api" + }, + "key": { + "type": "string" + } + }, + "required": ["type", "key"] + }, + "WellKnownAuth": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "wellknown" + }, + "key": { + "type": "string" + }, + "token": { + "type": "string" + } + }, + "required": ["type", "key", "token"] + }, + "Auth": { + "anyOf": [ + { + "$ref": "#/components/schemas/OAuth" + }, + { + "$ref": "#/components/schemas/ApiAuth" + }, + { + "$ref": "#/components/schemas/WellKnownAuth" + } + ] + } } + } } diff --git a/package-lock.json b/package-lock.json index 8b1fd4e6..6e3578be 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,53 +1,162 @@ { - "name": "claude-chat", - "version": "0.0.0", + "name": "opencodeui", + "version": "0.6.23", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "claude-chat", - "version": "0.0.0", - "dependencies": { - "@types/diff": "^7.0.2", + "name": "opencodeui", + "version": "0.6.23", + "hasInstallScript": true, + "license": "GPL-3.0-only", + "dependencies": { + "@codemirror/commands": "^6.10.3", + "@codemirror/search": "^6.7.0", + "@codemirror/state": "^6.6.0", + "@codemirror/view": "^6.41.1", + "@opencode-ai/sdk": "^1.16.0", + "@streamdown/math": "^1.0.2", + "@tauri-apps/api": "^2.10.1", + "@tauri-apps/plugin-dialog": "^2.6.0", + "@tauri-apps/plugin-fs": "^2.4.5", + "@tauri-apps/plugin-http": "^2.5.7", + "@tauri-apps/plugin-notification": "^2.3.3", + "@tauri-apps/plugin-opener": "^2.5.3", "@xterm/addon-fit": "^0.11.0", + "@xterm/addon-serialize": "^0.14.0", "@xterm/addon-web-links": "^0.12.0", "@xterm/addon-webgl": "^0.19.0", "@xterm/xterm": "^6.0.0", + "ajv": "^8.20.0", "clsx": "^2.1.1", - "diff": "^8.0.3", + "diff": "^8.0.4", + "i18next": "^26.0.1", + "i18next-browser-languagedetector": "^8.2.1", + "lucide-react": "^1.7.0", + "mermaid": "^11.13.0", + "motion": "^12.38.0", "react": "^19.2.0", "react-dom": "^19.2.0", - "react-markdown": "^10.1.0", - "react-virtuoso": "^4.18.1", - "remark-gfm": "^4.0.1", - "shiki": "^3.21.0", - "tailwind-merge": "^3.4.0" + "react-i18next": "^17.0.1", + "shiki": "^4.0.2", + "shiki-stream": "^0.1.4", + "streamdown": "^2.5.0" }, "devDependencies": { - "@eslint/js": "^9.39.1", - "@tailwindcss/vite": "^4.1.18", - "@types/node": "^24.10.1", + "@eslint/js": "^9.39.4", + "@tailwindcss/vite": "^4.2.2", + "@tauri-apps/cli": "^2.10.0", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.0", + "@types/diff": "^7.0.2", + "@types/node": "^25.5.0", "@types/react": "^19.2.5", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^5.1.1", - "eslint": "^9.39.1", + "@vitejs/plugin-react": "^6.0.1", + "eslint": "^9.39.4", + "eslint-config-prettier": "^10.1.8", "eslint-plugin-react-hooks": "^7.0.1", - "eslint-plugin-react-refresh": "^0.4.24", - "globals": "^16.5.0", - "tailwindcss": "^4.1.18", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.4.0", + "jsdom": "^29.0.1", + "material-icon-theme": "^5.32.0", + "prettier": "^3.6.2", + "tailwindcss": "^4.2.2", "typescript": "~5.9.3", - "typescript-eslint": "^8.46.4", - "vite": "^7.2.4" + "typescript-eslint": "^8.57.2", + "vite": "^8.0.3", + "vitest": "^4.1.2" + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", + "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", + "dev": true, + "license": "MIT" + }, + "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==", + "license": "MIT", + "dependencies": { + "package-manager-detector": "^1.3.0", + "tinyexec": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@asamuzakjp/css-color": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.0.1.tgz", + "integrity": "sha512-2SZFvqMyvboVV1d15lMf7XiI3m7SDqXUuKaTymJYLN6dSGadqp+fVojqJlVoMlbZnlTmu3S0TLwLTJpvBMO1Aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^3.1.1", + "@csstools/css-color-parser": "^4.0.2", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "lru-cache": "^11.2.6" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", + "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.0.4.tgz", + "integrity": "sha512-jXR6x4AcT3eIrS2fSNAwJpwirOkGcd+E7F7CP3zjdTqz9B/2huHOL8YJZBgekKwLML+u7qB/6P1LXQuMScsx0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.2.7" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", + "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" } }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@babel/code-frame": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz", - "integrity": "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==", + "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, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.28.5", + "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -56,9 +165,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.6.tgz", - "integrity": "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", "dev": true, "license": "MIT", "engines": { @@ -66,21 +175,21 @@ } }, "node_modules/@babel/core": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz", - "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/generator": "^7.28.6", - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helpers": "^7.28.6", - "@babel/parser": "^7.28.6", - "@babel/template": "^7.28.6", - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6", + "@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", @@ -97,14 +206,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.6.tgz", - "integrity": "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==", + "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, "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.6", - "@babel/types": "^7.28.6", + "@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" @@ -114,14 +223,14 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", - "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "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, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.28.6", - "@babel/helper-validator-option": "^7.27.1", + "@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" @@ -131,9 +240,9 @@ } }, "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "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, "license": "MIT", "engines": { @@ -141,29 +250,29 @@ } }, "node_modules/@babel/helper-module-imports": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", - "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "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, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", - "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "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, "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.28.6", - "@babel/helper-validator-identifier": "^7.28.5", - "@babel/traverse": "^7.28.6" + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -172,20 +281,10 @@ "@babel/core": "^7.0.0" } }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", - "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "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, "license": "MIT", "engines": { @@ -193,9 +292,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "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, "license": "MIT", "engines": { @@ -203,9 +302,9 @@ } }, "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "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, "license": "MIT", "engines": { @@ -213,27 +312,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", - "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz", - "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==", + "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, "license": "MIT", "dependencies": { - "@babel/types": "^7.28.6" + "@babel/types": "^7.29.7" }, "bin": { "parser": "bin/babel-parser.js" @@ -242,66 +341,43 @@ "node": ">=6.0.0" } }, - "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", - "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", - "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", - "dev": true, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/template": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", - "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "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, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/parser": "^7.28.6", - "@babel/types": "^7.28.6" + "@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.28.6", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.6.tgz", - "integrity": "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/generator": "^7.28.6", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.6", - "@babel/template": "^7.28.6", - "@babel/types": "^7.28.6", + "@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": { @@ -309,459 +385,274 @@ } }, "node_modules/@babel/types": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz", - "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", - "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", - "cpu": [ - "ppc64" - ], + "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==", + "license": "MIT" + }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" } }, - "node_modules/@esbuild/android-arm": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", - "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", - "cpu": [ - "arm" - ], - "dev": true, + "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==", + "license": "Apache-2.0" + }, + "node_modules/@codemirror/commands": { + "version": "6.10.3", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.3.tgz", + "integrity": "sha512-JFRiqhKu+bvSkDLI+rUhJwSxQxYb759W5GBezE8Uc8mHLqC9aV/9aTC7yJSqCtB3F00pylrLCwnyS91Ap5ej4Q==", "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.6.0", + "@codemirror/view": "^6.27.0", + "@lezer/common": "^1.1.0" } }, - "node_modules/@esbuild/android-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", - "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", - "cpu": [ - "arm64" - ], - "dev": true, + "node_modules/@codemirror/language": { + "version": "6.12.3", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.3.tgz", + "integrity": "sha512-QwCZW6Tt1siP37Jet9Tb02Zs81TQt6qQrZR2H+eGMcFsL1zMrk2/b9CLC7/9ieP1fjIUMgviLWMmgiHoJrj+ZA==", "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.23.0", + "@lezer/common": "^1.5.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" } }, - "node_modules/@esbuild/android-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", - "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", - "cpu": [ - "x64" - ], - "dev": true, + "node_modules/@codemirror/search": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.7.0.tgz", + "integrity": "sha512-ZvGm99wc/s2cITtMT15LFdn8aH/aS+V+DqyGq/N5ZlV5vWtH+nILvC2nw0zX7ByNoHHDZ2IxxdW38O0tc5nVHg==", "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.37.0", + "crelt": "^1.0.5" } }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", - "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", - "cpu": [ - "arm64" - ], - "dev": true, + "node_modules/@codemirror/state": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.6.0.tgz", + "integrity": "sha512-4nbvra5R5EtiCzr9BTHiTLc+MLXK2QGiAVYMyi8PkQd3SR+6ixar/Q/01Fa21TBIDOZXgeWV4WppsQolSreAPQ==", "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" + "dependencies": { + "@marijn/find-cluster-break": "^1.0.0" } }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", - "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", - "cpu": [ - "x64" - ], - "dev": true, + "node_modules/@codemirror/view": { + "version": "6.41.1", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.41.1.tgz", + "integrity": "sha512-ToDnWKbBnke+ZLrP6vgTTDScGi5H37YYuZGniQaBzxMVdtCxMrslsmtnOvbPZk4RX9bvkQqnWR/WS/35tJA0qg==", "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" + "dependencies": { + "@codemirror/state": "^6.6.0", + "crelt": "^1.0.6", + "style-mod": "^4.1.0", + "w3c-keyname": "^2.2.4" } }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", - "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", - "cpu": [ - "arm64" - ], + "node_modules/@csstools/color-helpers": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", + "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } ], + "license": "MIT-0", "engines": { - "node": ">=18" + "node": ">=20.19.0" } }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", - "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", - "cpu": [ - "x64" - ], + "node_modules/@csstools/css-calc": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.1.1.tgz", + "integrity": "sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } ], + "license": "MIT", "engines": { - "node": ">=18" + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" } }, - "node_modules/@esbuild/linux-arm": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", - "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", - "cpu": [ - "arm" - ], + "node_modules/@csstools/css-color-parser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.0.2.tgz", + "integrity": "sha512-0GEfbBLmTFf0dJlpsNU7zwxRIH0/BGEMuXLTCvFYxuL1tNhqzTbtnFICyJLTNK4a+RechKP75e7w42ClXSnJQw==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.0.2", + "@csstools/css-calc": "^3.1.1" + }, "engines": { - "node": ">=18" + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" } }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", - "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", - "cpu": [ - "arm64" - ], + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } ], + "license": "MIT", "engines": { - "node": ">=18" + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" } }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", - "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", - "cpu": [ - "ia32" - ], + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.2.tgz", + "integrity": "sha512-5GkLzz4prTIpoyeUiIu3iV6CSG3Plo7xRVOFPKI7FVEJ3mZ0A8SwK0XU3Gl7xAkiQ+mDyam+NNp875/C5y+jSA==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } ], - "engines": { - "node": ">=18" + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } } }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", - "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", - "cpu": [ - "loong64" - ], + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } ], + "license": "MIT", "engines": { - "node": ">=18" + "node": ">=20.19.0" } }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", - "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", - "cpu": [ - "mips64el" - ], + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", "dev": true, "license": "MIT", "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" } }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", - "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", - "cpu": [ - "ppc64" - ], + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", "dev": true, "license": "MIT", "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", - "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", - "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", - "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", - "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", - "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", - "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", - "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", - "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", - "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", - "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", - "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" + "dependencies": { + "tslib": "^2.4.0" } }, - "node_modules/@esbuild/win32-x64": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", - "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", - "cpu": [ - "x64" - ], + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", "dev": true, "license": "MIT", "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" + "dependencies": { + "tslib": "^2.4.0" } }, "node_modules/@eslint-community/eslint-utils": { @@ -807,15 +698,15 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.21.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", - "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", "dev": true, "license": "Apache-2.0", "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", - "minimatch": "^3.1.2" + "minimatch": "^3.1.5" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -848,20 +739,20 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", - "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", "dev": true, "license": "MIT", "dependencies": { - "ajv": "^6.12.4", + "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", - "minimatch": "^3.1.2", + "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" }, "engines": { @@ -871,6 +762,23 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/@eslint/eslintrc/node_modules/globals": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", @@ -884,10 +792,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, "node_modules/@eslint/js": { - "version": "9.39.2", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", - "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", "dev": true, "license": "MIT", "engines": { @@ -921,6 +836,24 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@exodus/bytes": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.0.tgz", + "integrity": "sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -973,6 +906,23 @@ "url": "https://github.com/sponsors/nzakas" } }, + "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==", + "license": "MIT" + }, + "node_modules/@iconify/utils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-3.1.0.tgz", + "integrity": "sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw==", + "license": "MIT", + "dependencies": { + "@antfu/install-pkg": "^1.1.0", + "@iconify/types": "^2.0.0", + "mlly": "^1.8.0" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -1023,31 +973,87 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.53", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz", - "integrity": "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==", - "dev": true, + "node_modules/@lezer/common": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.2.tgz", + "integrity": "sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==", "license": "MIT" }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.56.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.56.0.tgz", - "integrity": "sha512-LNKIPA5k8PF1+jAFomGe3qN3bbIgJe/IlpDBwuVjrDKrJhVWywgnJvflMt/zkbVNLFtF1+94SljYQS6e99klnw==", - "cpu": [ - "arm" - ], + "node_modules/@lezer/highlight": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz", + "integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.3.0" + } + }, + "node_modules/@lezer/lr": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.10.tgz", + "integrity": "sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@marijn/find-cluster-break": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", + "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "dependencies": { + "@chevrotain/types": "~11.1.1" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", "dev": true, "license": "MIT", "optional": true, - "os": [ - "android" - ] + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@opencode-ai/sdk": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@opencode-ai/sdk/-/sdk-1.16.0.tgz", + "integrity": "sha512-S4H2e9j4rdHs5BQOCjmVEdqdXmKwPFKjXPbPUaWiRJpAjBcZ/uIBpoZkmV+x9BLzc+vrE6WAffMZieQgukt4DA==", + "license": "MIT", + "dependencies": { + "cross-spawn": "7.0.6" + } }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.56.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.56.0.tgz", - "integrity": "sha512-lfbVUbelYqXlYiU/HApNMJzT1E87UPGvzveGg2h0ktUNlOCxKlWuJ9jtfvs1sKHdwU4fzY7Pl8sAl49/XaEk6Q==", + "node_modules/@oxc-project/types": { + "version": "0.133.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz", + "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", + "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==", "cpu": [ "arm64" ], @@ -1056,12 +1062,15 @@ "optional": true, "os": [ "android" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.56.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.56.0.tgz", - "integrity": "sha512-EgxD1ocWfhoD6xSOeEEwyE7tDvwTgZc8Bss7wCWe+uc7wO8G34HHCUH+Q6cHqJubxIAnQzAsyUsClt0yFLu06w==", + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz", + "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==", "cpu": [ "arm64" ], @@ -1070,12 +1079,15 @@ "optional": true, "os": [ "darwin" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.56.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.56.0.tgz", - "integrity": "sha512-1vXe1vcMOssb/hOF8iv52A7feWW2xnu+c8BV4t1F//m9QVLTfNVpEdja5ia762j/UEJe2Z1jAmEqZAK42tVW3g==", + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz", + "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==", "cpu": [ "x64" ], @@ -1084,54 +1096,32 @@ "optional": true, "os": [ "darwin" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.56.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.56.0.tgz", - "integrity": "sha512-bof7fbIlvqsyv/DtaXSck4VYQ9lPtoWNFCB/JY4snlFuJREXfZnm+Ej6yaCHfQvofJDXLDMTVxWscVSuQvVWUQ==", + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz", + "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==", "cpu": [ - "arm64" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.56.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.56.0.tgz", - "integrity": "sha512-KNa6lYHloW+7lTEkYGa37fpvPq+NKG/EHKM8+G/g9WDU7ls4sMqbVRV78J6LdNuVaeeK5WB9/9VAFbKxcbXKYg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.56.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.56.0.tgz", - "integrity": "sha512-E8jKK87uOvLrrLN28jnAAAChNq5LeCd2mGgZF+fGF5D507WlG/Noct3lP/QzQ6MrqJ5BCKNwI9ipADB6jyiq2A==", - "cpu": [ - "arm" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.56.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.56.0.tgz", - "integrity": "sha512-jQosa5FMYF5Z6prEpTCCmzCXz6eKr/tCBssSmQGEeozA9tkRUty/5Vx06ibaOP9RCrW1Pvb8yp3gvZhHwTDsJw==", + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz", + "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==", "cpu": [ "arm" ], @@ -1140,26 +1130,15 @@ "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.56.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.56.0.tgz", - "integrity": "sha512-uQVoKkrC1KGEV6udrdVahASIsaF8h7iLG0U0W+Xn14ucFwi6uS539PsAr24IEF9/FoDtzMeeJXJIBo5RkbNWvQ==", - "cpu": [ - "arm64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.56.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.56.0.tgz", - "integrity": "sha512-vLZ1yJKLxhQLFKTs42RwTwa6zkGln+bnXc8ueFGMYmBTLfNu58sl5/eXyxRa2RarTkJbXl8TKPgfS6V5ijNqEA==", + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz", + "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==", "cpu": [ "arm64" ], @@ -1168,54 +1147,32 @@ "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.56.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.56.0.tgz", - "integrity": "sha512-FWfHOCub564kSE3xJQLLIC/hbKqHSVxy8vY75/YHHzWvbJL7aYJkdgwD/xGfUlL5UV2SB7otapLrcCj2xnF1dg==", - "cpu": [ - "loong64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.56.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.56.0.tgz", - "integrity": "sha512-z1EkujxIh7nbrKL1lmIpqFTc/sr0u8Uk0zK/qIEFldbt6EDKWFk/pxFq3gYj4Bjn3aa9eEhYRlL3H8ZbPT1xvA==", + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz", + "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==", "cpu": [ - "loong64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.56.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.56.0.tgz", - "integrity": "sha512-iNFTluqgdoQC7AIE8Q34R3AuPrJGJirj5wMUErxj22deOcY7XwZRaqYmB6ZKFHoVGqRcRd0mqO+845jAibKCkw==", - "cpu": [ - "ppc64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.56.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.56.0.tgz", - "integrity": "sha512-MtMeFVlD2LIKjp2sE2xM2slq3Zxf9zwVuw0jemsxvh1QOpHSsSzfNOTH9uYW9i1MXFxUSMmLpeVeUzoNOKBaWg==", + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz", + "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==", "cpu": [ "ppc64" ], @@ -1224,40 +1181,15 @@ "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.56.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.56.0.tgz", - "integrity": "sha512-in+v6wiHdzzVhYKXIk5U74dEZHdKN9KH0Q4ANHOTvyXPG41bajYRsy7a8TPKbYPl34hU7PP7hMVHRvv/5aCSew==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.56.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.56.0.tgz", - "integrity": "sha512-yni2raKHB8m9NQpI9fPVwN754mn6dHQSbDTwxdr9SE0ks38DTjLMMBjrwvB5+mXrX+C0npX0CVeCUcvvvD8CNQ==", - "cpu": [ - "riscv64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.56.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.56.0.tgz", - "integrity": "sha512-zhLLJx9nQPu7wezbxt2ut+CI4YlXi68ndEve16tPc/iwoylWS9B3FxpLS2PkmfYgDQtosah07Mj9E0khc3Y+vQ==", + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz", + "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==", "cpu": [ "s390x" ], @@ -1266,12 +1198,15 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.56.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.56.0.tgz", - "integrity": "sha512-MVC6UDp16ZSH7x4rtuJPAEoE1RwS8N4oK9DLHy3FTEdFoUTCFVzMfJl/BVJ330C+hx8FfprA5Wqx4FhZXkj2Kw==", + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz", + "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==", "cpu": [ "x64" ], @@ -1280,12 +1215,15 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.56.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.56.0.tgz", - "integrity": "sha512-ZhGH1eA4Qv0lxaV00azCIS1ChedK0V32952Md3FtnxSqZTBTd6tgil4nZT5cU8B+SIw3PFYkvyR4FKo2oyZIHA==", + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz", + "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==", "cpu": [ "x64" ], @@ -1294,26 +1232,15 @@ "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.56.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.56.0.tgz", - "integrity": "sha512-O16XcmyDeFI9879pEcmtWvD/2nyxR9mF7Gs44lf1vGGx8Vg2DRNx11aVXBEqOQhWb92WN4z7fW/q4+2NYzCbBA==", - "cpu": [ - "x64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.56.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.56.0.tgz", - "integrity": "sha512-LhN/Reh+7F3RCgQIRbgw8ZMwUwyqJM+8pXNT6IIJAqm2IdKkzpCh/V9EdgOMBKuebIrzswqy4ATlrDgiOwbRcQ==", + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz", + "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==", "cpu": [ "arm64" ], @@ -1322,40 +1249,51 @@ "optional": true, "os": [ "openharmony" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.56.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.56.0.tgz", - "integrity": "sha512-kbFsOObXp3LBULg1d3JIUQMa9Kv4UitDmpS+k0tinPBz3watcUiV2/LUDMMucA6pZO3WGE27P7DsfaN54l9ing==", + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz", + "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==", "cpu": [ - "arm64" + "wasm32" ], "dev": true, "license": "MIT", "optional": true, - "os": [ - "win32" - ] + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.56.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.56.0.tgz", - "integrity": "sha512-vSSgny54D6P4vf2izbtFm/TcWYedw7f8eBrOiGGecyHyQB9q4Kqentjaj8hToe+995nob/Wv48pDqL5a62EWtg==", + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz", + "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==", "cpu": [ - "ia32" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "win32" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.56.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.56.0.tgz", - "integrity": "sha512-FeCnkPCTHQJFbiGG49KjV5YGW/8b9rrXAM2Mz2kiIoktq2qsJxRD5giEMEOD2lPdgs72upzefaUvS+nc8E3UzQ==", + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz", + "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==", "cpu": [ "x64" ], @@ -1364,81 +1302,110 @@ "optional": true, "os": [ "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.56.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.56.0.tgz", - "integrity": "sha512-H8AE9Ur/t0+1VXujj90w0HrSOuv0Nq9r1vSZF2t5km20NTfosQsGGUXDaKdQZzwuLts7IyL1fYT4hM95TI9c4g==", - "cpu": [ - "x64" ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.7", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz", + "integrity": "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "license": "MIT" }, "node_modules/@shikijs/core": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-3.21.0.tgz", - "integrity": "sha512-AXSQu/2n1UIQekY8euBJlvFYZIw0PHY63jUzGbrOma4wPxzznJXTXkri+QcHeBNaFxiiOljKxxJkVSoB3PjbyA==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-4.0.2.tgz", + "integrity": "sha512-hxT0YF4ExEqB8G/qFdtJvpmHXBYJ2lWW7qTHDarVkIudPFE6iCIrqdgWxGn5s+ppkGXI0aEGlibI0PAyzP3zlw==", "license": "MIT", "dependencies": { - "@shikijs/types": "3.21.0", + "@shikijs/primitive": "4.0.2", + "@shikijs/types": "4.0.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" + }, + "engines": { + "node": ">=20" } }, "node_modules/@shikijs/engine-javascript": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-3.21.0.tgz", - "integrity": "sha512-ATwv86xlbmfD9n9gKRiwuPpWgPENAWCLwYCGz9ugTJlsO2kOzhOkvoyV/UD+tJ0uT7YRyD530x6ugNSffmvIiQ==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-4.0.2.tgz", + "integrity": "sha512-7PW0Nm49DcoUIQEXlJhNNBHyoGMjalRETTCcjMqEaMoJRLljy1Bi/EGV3/qLBgLKQejdspiiYuHGQW6dX94Nag==", "license": "MIT", "dependencies": { - "@shikijs/types": "3.21.0", + "@shikijs/types": "4.0.2", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" + }, + "engines": { + "node": ">=20" } }, "node_modules/@shikijs/engine-oniguruma": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.21.0.tgz", - "integrity": "sha512-OYknTCct6qiwpQDqDdf3iedRdzj6hFlOPv5hMvI+hkWfCKs5mlJ4TXziBG9nyabLwGulrUjHiCq3xCspSzErYQ==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-4.0.2.tgz", + "integrity": "sha512-UpCB9Y2sUKlS9z8juFSKz7ZtysmeXCgnRF0dlhXBkmQnek7lAToPte8DkxmEYGNTMii72zU/lyXiCB6StuZeJg==", "license": "MIT", "dependencies": { - "@shikijs/types": "3.21.0", + "@shikijs/types": "4.0.2", "@shikijs/vscode-textmate": "^10.0.2" + }, + "engines": { + "node": ">=20" } }, "node_modules/@shikijs/langs": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.21.0.tgz", - "integrity": "sha512-g6mn5m+Y6GBJ4wxmBYqalK9Sp0CFkUqfNzUy2pJglUginz6ZpWbaWjDB4fbQ/8SHzFjYbtU6Ddlp1pc+PPNDVA==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-4.0.2.tgz", + "integrity": "sha512-KaXby5dvoeuZzN0rYQiPMjFoUrz4hgwIE+D6Du9owcHcl6/g16/yT5BQxSW5cGt2MZBz6Hl0YuRqf12omRfUUg==", "license": "MIT", "dependencies": { - "@shikijs/types": "3.21.0" + "@shikijs/types": "4.0.2" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/primitive": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/primitive/-/primitive-4.0.2.tgz", + "integrity": "sha512-M6UMPrSa3fN5ayeJwFVl9qWofl273wtK1VG8ySDZ1mQBfhCpdd8nEx7nPZ/tk7k+TYcpqBZzj/AnwxT9lO+HJw==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.0.2", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + }, + "engines": { + "node": ">=20" } }, "node_modules/@shikijs/themes": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.21.0.tgz", - "integrity": "sha512-BAE4cr9EDiZyYzwIHEk7JTBJ9CzlPuM4PchfcA5ao1dWXb25nv6hYsoDiBq2aZK9E3dlt3WB78uI96UESD+8Mw==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-4.0.2.tgz", + "integrity": "sha512-mjCafwt8lJJaVSsQvNVrJumbnnj1RI8jbUKrPKgE6E3OvQKxnuRoBaYC51H4IGHePsGN/QtALglWBU7DoKDFnA==", "license": "MIT", "dependencies": { - "@shikijs/types": "3.21.0" + "@shikijs/types": "4.0.2" + }, + "engines": { + "node": ">=20" } }, "node_modules/@shikijs/types": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.21.0.tgz", - "integrity": "sha512-zGrWOxZ0/+0ovPY7PvBU2gIS9tmhSUUt30jAcNV0Bq0gb2S98gwfjIs1vxlmH5zM7/4YxLamT6ChlqqAJmPPjA==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-4.0.2.tgz", + "integrity": "sha512-qzbeRooUTPnLE+sHD/Z8DStmaDgnbbc/pMrU203950aRqjX/6AFHeDYT+j00y2lPdz0ywJKx7o/7qnqTivtlXg==", "license": "MIT", "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" + }, + "engines": { + "node": ">=20" } }, "node_modules/@shikijs/vscode-textmate": { @@ -1447,50 +1414,71 @@ "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", "license": "MIT" }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@streamdown/math": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@streamdown/math/-/math-1.0.2.tgz", + "integrity": "sha512-r8Ur9/lBuFnzZAFdEWrLUF2s/gRwRRRwruqltdZibyjbCBnuW7SJbFm26nXqvpJPW/gzpBUMrBVBzd88z05D5g==", + "license": "Apache-2.0", + "dependencies": { + "katex": "^0.16.27", + "rehype-katex": "^7.0.1", + "remark-math": "^6.0.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0" + } + }, "node_modules/@tailwindcss/node": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.18.tgz", - "integrity": "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.2.tgz", + "integrity": "sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/remapping": "^2.3.4", - "enhanced-resolve": "^5.18.3", + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", - "lightningcss": "1.30.2", + "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", - "tailwindcss": "4.1.18" + "tailwindcss": "4.2.2" } }, "node_modules/@tailwindcss/oxide": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.18.tgz", - "integrity": "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.2.tgz", + "integrity": "sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg==", "dev": true, "license": "MIT", "engines": { - "node": ">= 10" + "node": ">= 20" }, "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.1.18", - "@tailwindcss/oxide-darwin-arm64": "4.1.18", - "@tailwindcss/oxide-darwin-x64": "4.1.18", - "@tailwindcss/oxide-freebsd-x64": "4.1.18", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", - "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", - "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", - "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", - "@tailwindcss/oxide-linux-x64-musl": "4.1.18", - "@tailwindcss/oxide-wasm32-wasi": "4.1.18", - "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", - "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" + "@tailwindcss/oxide-android-arm64": "4.2.2", + "@tailwindcss/oxide-darwin-arm64": "4.2.2", + "@tailwindcss/oxide-darwin-x64": "4.2.2", + "@tailwindcss/oxide-freebsd-x64": "4.2.2", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.2", + "@tailwindcss/oxide-linux-arm64-gnu": "4.2.2", + "@tailwindcss/oxide-linux-arm64-musl": "4.2.2", + "@tailwindcss/oxide-linux-x64-gnu": "4.2.2", + "@tailwindcss/oxide-linux-x64-musl": "4.2.2", + "@tailwindcss/oxide-wasm32-wasi": "4.2.2", + "@tailwindcss/oxide-win32-arm64-msvc": "4.2.2", + "@tailwindcss/oxide-win32-x64-msvc": "4.2.2" } }, "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.18.tgz", - "integrity": "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.2.tgz", + "integrity": "sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg==", "cpu": [ "arm64" ], @@ -1501,13 +1489,13 @@ "android" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.18.tgz", - "integrity": "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.2.tgz", + "integrity": "sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg==", "cpu": [ "arm64" ], @@ -1518,13 +1506,13 @@ "darwin" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.18.tgz", - "integrity": "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.2.tgz", + "integrity": "sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw==", "cpu": [ "x64" ], @@ -1535,13 +1523,13 @@ "darwin" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.18.tgz", - "integrity": "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.2.tgz", + "integrity": "sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ==", "cpu": [ "x64" ], @@ -1552,13 +1540,13 @@ "freebsd" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.18.tgz", - "integrity": "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.2.tgz", + "integrity": "sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ==", "cpu": [ "arm" ], @@ -1569,13 +1557,13 @@ "linux" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.18.tgz", - "integrity": "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.2.tgz", + "integrity": "sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==", "cpu": [ "arm64" ], @@ -1586,13 +1574,13 @@ "linux" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.18.tgz", - "integrity": "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.2.tgz", + "integrity": "sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==", "cpu": [ "arm64" ], @@ -1603,13 +1591,13 @@ "linux" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.18.tgz", - "integrity": "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.2.tgz", + "integrity": "sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==", "cpu": [ "x64" ], @@ -1620,13 +1608,13 @@ "linux" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.18.tgz", - "integrity": "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.2.tgz", + "integrity": "sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==", "cpu": [ "x64" ], @@ -1637,13 +1625,13 @@ "linux" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.18.tgz", - "integrity": "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.2.tgz", + "integrity": "sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==", "bundleDependencies": [ "@napi-rs/wasm-runtime", "@emnapi/core", @@ -1659,21 +1647,21 @@ "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1", + "@emnapi/core": "^1.8.1", + "@emnapi/runtime": "^1.8.1", "@emnapi/wasi-threads": "^1.1.0", - "@napi-rs/wasm-runtime": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.1", "@tybys/wasm-util": "^0.10.1", - "tslib": "^2.4.0" + "tslib": "^2.8.1" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz", - "integrity": "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.2.tgz", + "integrity": "sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ==", "cpu": [ "arm64" ], @@ -1684,13 +1672,13 @@ "win32" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.18.tgz", - "integrity": "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.2.tgz", + "integrity": "sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA==", "cpu": [ "x64" ], @@ -1701,71 +1689,657 @@ "win32" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/vite": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.18.tgz", - "integrity": "sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.2.tgz", + "integrity": "sha512-mEiF5HO1QqCLXoNEfXVA1Tzo+cYsrqV7w9Juj2wdUFyW07JRenqMG225MvPwr3ZD9N1bFQj46X7r33iHxLUW0w==", "dev": true, "license": "MIT", "dependencies": { - "@tailwindcss/node": "4.1.18", - "@tailwindcss/oxide": "4.1.18", - "tailwindcss": "4.1.18" + "@tailwindcss/node": "4.2.2", + "@tailwindcss/oxide": "4.2.2", + "tailwindcss": "4.2.2" }, "peerDependencies": { - "vite": "^5.2.0 || ^6 || ^7" + "vite": "^5.2.0 || ^6 || ^7 || ^8" + } + }, + "node_modules/@tauri-apps/api": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.10.1.tgz", + "integrity": "sha512-hKL/jWf293UDSUN09rR69hrToyIXBb8CjGaWC7gfinvnQrBVvnLr08FeFi38gxtugAVyVcTa5/FD/Xnkb1siBw==", + "license": "Apache-2.0 OR MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" } }, - "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==", + "node_modules/@tauri-apps/cli": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.10.1.tgz", + "integrity": "sha512-jQNGF/5quwORdZSSLtTluyKQ+o6SMa/AUICfhf4egCGFdMHqWssApVgYSbg+jmrZoc8e1DscNvjTnXtlHLS11g==", "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" + "license": "Apache-2.0 OR MIT", + "bin": { + "tauri": "tauri.js" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + }, + "optionalDependencies": { + "@tauri-apps/cli-darwin-arm64": "2.10.1", + "@tauri-apps/cli-darwin-x64": "2.10.1", + "@tauri-apps/cli-linux-arm-gnueabihf": "2.10.1", + "@tauri-apps/cli-linux-arm64-gnu": "2.10.1", + "@tauri-apps/cli-linux-arm64-musl": "2.10.1", + "@tauri-apps/cli-linux-riscv64-gnu": "2.10.1", + "@tauri-apps/cli-linux-x64-gnu": "2.10.1", + "@tauri-apps/cli-linux-x64-musl": "2.10.1", + "@tauri-apps/cli-win32-arm64-msvc": "2.10.1", + "@tauri-apps/cli-win32-ia32-msvc": "2.10.1", + "@tauri-apps/cli-win32-x64-msvc": "2.10.1" + } + }, + "node_modules/@tauri-apps/cli-darwin-arm64": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.10.1.tgz", + "integrity": "sha512-Z2OjCXiZ+fbYZy7PmP3WRnOpM9+Fy+oonKDEmUE6MwN4IGaYqgceTjwHucc/kEEYZos5GICve35f7ZiizgqEnQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" } }, - "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==", + "node_modules/@tauri-apps/cli-darwin-x64": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.10.1.tgz", + "integrity": "sha512-V/irQVvjPMGOTQqNj55PnQPVuH4VJP8vZCN7ajnj+ZS8Kom1tEM2hR3qbbIRoS3dBKs5mbG8yg1WC+97dq17Pw==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.0.0" + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" } }, - "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==", + "node_modules/@tauri-apps/cli-linux-arm-gnueabihf": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.10.1.tgz", + "integrity": "sha512-Hyzwsb4VnCWKGfTw+wSt15Z2pLw2f0JdFBfq2vHBOBhvg7oi6uhKiF87hmbXOBXUZaGkyRDkCHsdzJcIfoJC2w==", + "cpu": [ + "arm" + ], "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" } }, - "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==", + "node_modules/@tauri-apps/cli-linux-arm64-gnu": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.10.1.tgz", + "integrity": "sha512-OyOYs2t5GkBIvyWjA1+h4CZxTcdz1OZPCWAPz5DYEfB0cnWHERTnQ/SLayQzncrT0kwRoSfSz9KxenkyJoTelA==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.2" + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" } }, - "node_modules/@types/debug": { - "version": "4.1.12", + "node_modules/@tauri-apps/cli-linux-arm64-musl": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.10.1.tgz", + "integrity": "sha512-MIj78PDDGjkg3NqGptDOGgfXks7SYJwhiMh8SBoZS+vfdz7yP5jN18bNaLnDhsVIPARcAhE1TlsZe/8Yxo2zqg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-riscv64-gnu": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.10.1.tgz", + "integrity": "sha512-X0lvOVUg8PCVaoEtEAnpxmnkwlE1gcMDTqfhbefICKDnOTJ5Est3qL0SrWxizDackIOKBcvtpejrSiVpuJI1kw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-gnu": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.10.1.tgz", + "integrity": "sha512-2/12bEzsJS9fAKybxgicCDFxYD1WEI9kO+tlDwX5znWG2GwMBaiWcmhGlZ8fi+DMe9CXlcVarMTYc0L3REIRxw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-musl": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.10.1.tgz", + "integrity": "sha512-Y8J0ZzswPz50UcGOFuXGEMrxbjwKSPgXftx5qnkuMs2rmwQB5ssvLb6tn54wDSYxe7S6vlLob9vt0VKuNOaCIQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-arm64-msvc": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.10.1.tgz", + "integrity": "sha512-iSt5B86jHYAPJa/IlYw++SXtFPGnWtFJriHn7X0NFBVunF6zu9+/zOn8OgqIWSl8RgzhLGXQEEtGBdR4wzpVgg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-ia32-msvc": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.10.1.tgz", + "integrity": "sha512-gXyxgEzsFegmnWywYU5pEBURkcFN/Oo45EAwvZrHMh+zUSEAvO5E8TXsgPADYm31d1u7OQU3O3HsYfVBf2moHw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-x64-msvc": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.10.1.tgz", + "integrity": "sha512-6Cn7YpPFwzChy0ERz6djKEmUehWrYlM+xTaNzGPgZocw3BD7OfwfWHKVWxXzdjEW2KfKkHddfdxK1XXTYqBRLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/plugin-dialog": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.6.0.tgz", + "integrity": "sha512-q4Uq3eY87TdcYzXACiYSPhmpBA76shgmQswGkSVio4C82Sz2W4iehe9TnKYwbq7weHiL88Yw19XZm7v28+Micg==", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@tauri-apps/api": "^2.8.0" + } + }, + "node_modules/@tauri-apps/plugin-fs": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-fs/-/plugin-fs-2.4.5.tgz", + "integrity": "sha512-dVxWWGE6VrOxC7/jlhyE+ON/Cc2REJlM35R3PJX3UvFw2XwYhLGQVAIyrehenDdKjotipjYEVc4YjOl3qq90fA==", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@tauri-apps/api": "^2.8.0" + } + }, + "node_modules/@tauri-apps/plugin-http": { + "version": "2.5.7", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-http/-/plugin-http-2.5.7.tgz", + "integrity": "sha512-+F2lEH/c9b0zSsOXKq+5hZNcd9F4IIKCK1T17RqMwpCmVnx2aoqY8yIBccCd25HTYUb3j6NPVbRax/m00hKG8A==", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@tauri-apps/api": "^2.10.1" + } + }, + "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==", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@tauri-apps/api": "^2.8.0" + } + }, + "node_modules/@tauri-apps/plugin-opener": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-opener/-/plugin-opener-2.5.3.tgz", + "integrity": "sha512-CCcUltXMOfUEArbf3db3kCE7Ggy1ExBEBl51Ko2ODJ6GDYHRp1nSNlQm5uNCFY5k7/ufaK5Ib3Du/Zir19IYQQ==", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@tauri-apps/api": "^2.8.0" + } + }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, + "node_modules/@types/debug": { + "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", "license": "MIT", @@ -1773,10 +2347,18 @@ "@types/ms": "*" } }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/diff": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/@types/diff/-/diff-7.0.2.tgz", "integrity": "sha512-JSWRMozjFKsGlEjiiKajUjIJVKuKdE3oVy2DNtK+fUo8q82nhFZ2CPQwicAIkXrofahDXrWJ7mjelvZphMS98Q==", + "dev": true, "license": "MIT" }, "node_modules/@types/estree": { @@ -1794,6 +2376,12 @@ "@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==", + "license": "MIT" + }, "node_modules/@types/hast": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", @@ -1810,6 +2398,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/katex": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@types/katex/-/katex-0.16.8.tgz", + "integrity": "sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg==", + "license": "MIT" + }, "node_modules/@types/mdast": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", @@ -1826,19 +2420,20 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.10.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.9.tgz", - "integrity": "sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw==", + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", + "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~7.16.0" + "undici-types": "~7.18.0" } }, "node_modules/@types/react": { - "version": "19.2.9", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.9.tgz", - "integrity": "sha512-Lpo8kgb/igvMIPeNV2rsYKTgaORYdO1XGVZ4Qz3akwOj0ySGYMPlQWa8BaLn0G63D1aSaAQ5ldR06wCpChQCjA==", + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "dev": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -1854,6 +2449,13 @@ "@types/react": "^19.2.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==", + "license": "MIT", + "optional": true + }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", @@ -1861,17 +2463,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.53.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.53.1.tgz", - "integrity": "sha512-cFYYFZ+oQFi6hUnBTbLRXfTJiaQtYE3t4O692agbBl+2Zy+eqSKWtPjhPXJu1G7j4RLjKgeJPDdq3EqOwmX5Ag==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.2.tgz", + "integrity": "sha512-NZZgp0Fm2IkD+La5PR81sd+g+8oS6JwJje+aRWsDocxHkjyRw0J5L5ZTlN3LI1LlOcGL7ph3eaIUmTXMIjLk0w==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.53.1", - "@typescript-eslint/type-utils": "8.53.1", - "@typescript-eslint/utils": "8.53.1", - "@typescript-eslint/visitor-keys": "8.53.1", + "@typescript-eslint/scope-manager": "8.57.2", + "@typescript-eslint/type-utils": "8.57.2", + "@typescript-eslint/utils": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" @@ -1884,8 +2486,8 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.53.1", - "eslint": "^8.57.0 || ^9.0.0", + "@typescript-eslint/parser": "^8.57.2", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, @@ -1900,16 +2502,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.53.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.53.1.tgz", - "integrity": "sha512-nm3cvFN9SqZGXjmw5bZ6cGmvJSyJPn0wU9gHAZZHDnZl2wF9PhHv78Xf06E0MaNk4zLVHL8hb2/c32XvyJOLQg==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.2.tgz", + "integrity": "sha512-30ScMRHIAD33JJQkgfGW1t8CURZtjc2JpTrq5n2HFhOefbAhb7ucc7xJwdWcrEtqUIYJ73Nybpsggii6GtAHjA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.53.1", - "@typescript-eslint/types": "8.53.1", - "@typescript-eslint/typescript-estree": "8.53.1", - "@typescript-eslint/visitor-keys": "8.53.1", + "@typescript-eslint/scope-manager": "8.57.2", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2", "debug": "^4.4.3" }, "engines": { @@ -1920,19 +2522,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.53.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.53.1.tgz", - "integrity": "sha512-WYC4FB5Ra0xidsmlPb+1SsnaSKPmS3gsjIARwbEkHkoWloQmuzcfypljaJcR78uyLA1h8sHdWWPHSLDI+MtNog==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.2.tgz", + "integrity": "sha512-FuH0wipFywXRTHf+bTTjNyuNQQsQC3qh/dYzaM4I4W0jrCqjCVuUh99+xd9KamUfmCGPvbO8NDngo/vsnNVqgw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.53.1", - "@typescript-eslint/types": "^8.53.1", + "@typescript-eslint/tsconfig-utils": "^8.57.2", + "@typescript-eslint/types": "^8.57.2", "debug": "^4.4.3" }, "engines": { @@ -1947,14 +2549,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.53.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.53.1.tgz", - "integrity": "sha512-Lu23yw1uJMFY8cUeq7JlrizAgeQvWugNQzJp8C3x8Eo5Jw5Q2ykMdiiTB9vBVOOUBysMzmRRmUfwFrZuI2C4SQ==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.2.tgz", + "integrity": "sha512-snZKH+W4WbWkrBqj4gUNRIGb/jipDW3qMqVJ4C9rzdFc+wLwruxk+2a5D+uoFcKPAqyqEnSb4l2ULuZf95eSkw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.53.1", - "@typescript-eslint/visitor-keys": "8.53.1" + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1965,9 +2567,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.53.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.53.1.tgz", - "integrity": "sha512-qfvLXS6F6b1y43pnf0pPbXJ+YoXIC7HKg0UGZ27uMIemKMKA6XH2DTxsEDdpdN29D+vHV07x/pnlPNVLhdhWiA==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.2.tgz", + "integrity": "sha512-3Lm5DSM+DCowsUOJC+YqHHnKEfFh5CoGkj5Z31NQSNF4l5wdOwqGn99wmwN/LImhfY3KJnmordBq/4+VDe2eKw==", "dev": true, "license": "MIT", "engines": { @@ -1982,15 +2584,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.53.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.53.1.tgz", - "integrity": "sha512-MOrdtNvyhy0rHyv0ENzub1d4wQYKb2NmIqG7qEqPWFW7Mpy2jzFC3pQ2yKDvirZB7jypm5uGjF2Qqs6OIqu47w==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.2.tgz", + "integrity": "sha512-Co6ZCShm6kIbAM/s+oYVpKFfW7LBc6FXoPXjTRQ449PPNBY8U0KZXuevz5IFuuUj2H9ss40atTaf9dlGLzbWZg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.53.1", - "@typescript-eslint/typescript-estree": "8.53.1", - "@typescript-eslint/utils": "8.53.1", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2", + "@typescript-eslint/utils": "8.57.2", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, @@ -2002,14 +2604,14 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.53.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.53.1.tgz", - "integrity": "sha512-jr/swrr2aRmUAUjW5/zQHbMaui//vQlsZcJKijZf3M26bnmLj8LyZUpj8/Rd6uzaek06OWsqdofN/Thenm5O8A==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.2.tgz", + "integrity": "sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA==", "dev": true, "license": "MIT", "engines": { @@ -2021,18 +2623,18 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.53.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.53.1.tgz", - "integrity": "sha512-RGlVipGhQAG4GxV1s34O91cxQ/vWiHJTDHbXRr0li2q/BGg3RR/7NM8QDWgkEgrwQYCvmJV9ichIwyoKCQ+DTg==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.2.tgz", + "integrity": "sha512-2MKM+I6g8tJxfSmFKOnHv2t8Sk3T6rF20A1Puk0svLK+uVapDZB/4pfAeB7nE83uAZrU6OxW+HmOd5wHVdXwXA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.53.1", - "@typescript-eslint/tsconfig-utils": "8.53.1", - "@typescript-eslint/types": "8.53.1", - "@typescript-eslint/visitor-keys": "8.53.1", + "@typescript-eslint/project-service": "8.57.2", + "@typescript-eslint/tsconfig-utils": "8.57.2", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2", "debug": "^4.4.3", - "minimatch": "^9.0.5", + "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" @@ -2048,36 +2650,49 @@ "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "dev": true, "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" + "engines": { + "node": "18 || 20 || >=22" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "brace-expansion": "^2.0.1" + "balanced-match": "^4.0.2" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, "license": "ISC", "bin": { @@ -2088,16 +2703,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.53.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.53.1.tgz", - "integrity": "sha512-c4bMvGVWW4hv6JmDUEG7fSYlWOl3II2I4ylt0NM+seinYQlZMQIaKaXIIVJWt9Ofh6whrpM+EdDQXKXjNovvrg==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.2.tgz", + "integrity": "sha512-krRIbvPK1ju1WBKIefiX+bngPs+odIQUtR7kymzPfo1POVw3jlF+nLkmexdSSd4UCbDcQn+wMBATOOmpBbqgKg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.53.1", - "@typescript-eslint/types": "8.53.1", - "@typescript-eslint/typescript-estree": "8.53.1" + "@typescript-eslint/scope-manager": "8.57.2", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2107,19 +2722,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.53.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.53.1.tgz", - "integrity": "sha512-oy+wV7xDKFPRyNggmXuZQSBzvoLnpmJs+GhzRhPjrxl2b/jIlyjVokzm47CZCDUdXKr2zd7ZLodPfOBpOPyPlg==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.2.tgz", + "integrity": "sha512-zhahknjobV2FiD6Ee9iLbS7OV9zi10rG26odsQdfBO/hjSzUQbkIYgda+iNKK1zNiW2ey+Lf8MU5btN17V3dUw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.53.1", - "eslint-visitor-keys": "^4.2.1" + "@typescript-eslint/types": "8.57.2", + "eslint-visitor-keys": "^5.0.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2129,31 +2744,172 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@ungap/structured-clone": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", "license": "ISC" }, + "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==", + "license": "MIT", + "optionalDependencies": { + "d3-selection": "^3.0.0", + "d3-transition": "^3.0.1" + } + }, "node_modules/@vitejs/plugin-react": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.2.tgz", - "integrity": "sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz", + "integrity": "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/core": "^7.28.5", - "@babel/plugin-transform-react-jsx-self": "^7.27.1", - "@babel/plugin-transform-react-jsx-source": "^7.27.1", - "@rolldown/pluginutils": "1.0.0-beta.53", - "@types/babel__core": "^7.20.5", - "react-refresh": "^0.18.0" + "@rolldown/pluginutils": "1.0.0-rc.7" }, "engines": { "node": "^20.19.0 || >=22.12.0" }, "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.2.tgz", + "integrity": "sha512-gbu+7B0YgUJ2nkdsRJrFFW6X7NTP44WlhiclHniUhxADQJH5Szt9mZ9hWnJPJ8YwOK5zUOSSlSvyzRf0u1DSBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.2", + "@vitest/utils": "4.1.2", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.2.tgz", + "integrity": "sha512-Ize4iQtEALHDttPRCmN+FKqOl2vxTiNUhzobQFFt/BM1lRUTG7zRCLOykG/6Vo4E4hnUdfVLo5/eqKPukcWW7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.2", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.2.tgz", + "integrity": "sha512-dwQga8aejqeuB+TvXCMzSQemvV9hNEtDDpgUKDzOmNQayl2OG241PSWeJwKRH3CiC+sESrmoFd49rfnq7T4RnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.2.tgz", + "integrity": "sha512-Gr+FQan34CdiYAwpGJmQG8PgkyFVmARK8/xSijia3eTFgVfpcpztWLuP6FttGNfPLJhaZVP/euvujeNYar36OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.2", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.2.tgz", + "integrity": "sha512-g7yfUmxYS4mNxk31qbOYsSt2F4m1E02LFqO53Xpzg3zKMhLAPZAjjfyl9e6z7HrW6LvUdTwAQR3HHfLjpko16A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.2", + "@vitest/utils": "4.1.2", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.2.tgz", + "integrity": "sha512-DU4fBnbVCJGNBwVA6xSToNXrkZNSiw59H8tcuUspVMsBDBST4nfvsPsEHDHGtWRRnqBERBQu7TrTKskmjqTXKA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.2.tgz", + "integrity": "sha512-xw2/TiX82lQHA06cgbqRKFb5lCAy3axQ4H4SoUFhUsg+wztiet+co86IAMDtF6Vm1hc7J6j09oh/rgDn+JdKIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.2", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, "node_modules/@xterm/addon-fit": { @@ -2162,6 +2918,12 @@ "integrity": "sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g==", "license": "MIT" }, + "node_modules/@xterm/addon-serialize": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@xterm/addon-serialize/-/addon-serialize-0.14.0.tgz", + "integrity": "sha512-uteyTU1EkrQa2Ux6P/uFl2fzmXI46jy5uoQMKEOM0fKTyiW7cSn0WrFenHm5vO5uEXX/GpwW/FgILvv3r0WbkA==", + "license": "MIT" + }, "node_modules/@xterm/addon-web-links": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/@xterm/addon-web-links/-/addon-web-links-0.12.0.tgz", @@ -2170,7 +2932,7 @@ }, "node_modules/@xterm/addon-webgl": { "version": "0.19.0", - "resolved": "https://registry.npmjs.org/@xterm/addon-webgl/-/addon-webgl-0.19.0.tgz", + "resolved": "https://registry.npmmirror.com/@xterm/addon-webgl/-/addon-webgl-0.19.0.tgz", "integrity": "sha512-b3fMOsyLVuCeNJWxolACEUED0vm7qC0cy4wRvf3oURSzDTYVQiGPhTnhWZwIHdvC48Y+oLhvYXnY4XDXPoJo6A==", "license": "MIT" }, @@ -2184,10 +2946,9 @@ ] }, "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "dev": true, + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -2207,22 +2968,32 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, + "version": "8.20.0", + "resolved": "https://registry.npmmirror.com/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", "license": "MIT", "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" }, "funding": { "type": "github", "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -2246,6 +3017,26 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/bail": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", @@ -2264,19 +3055,32 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.9.17", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.17.tgz", - "integrity": "sha512-agD0MgJFUP/4nvjqzIB29zRPUuCF7Ge6mEv9s8dHrtYD7QWXRcx75rOADE/d5ah1NI+0vkDl0yorDd5U852IQQ==", + "version": "2.10.40", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.40.tgz", + "integrity": "sha512-BSSLZ9/Cjjv7Gtj5B68ZzXcXUg8iOf3fme+FCuh8rC/Go+Kmh8cox7M3A8dolou16s64QjLPOSdngh7GxXvkSw==", "dev": true, "license": "Apache-2.0", "bin": { - "baseline-browser-mapping": "dist/cli.js" + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" } }, "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", "dev": true, "license": "MIT", "dependencies": { @@ -2285,9 +3089,9 @@ } }, "node_modules/browserslist": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", - "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "version": "4.28.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.4.tgz", + "integrity": "sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw==", "dev": true, "funding": [ { @@ -2305,11 +3109,11 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.9.0", - "caniuse-lite": "^1.0.30001759", - "electron-to-chromium": "^1.5.263", - "node-releases": "^2.0.27", - "update-browserslist-db": "^1.2.0" + "baseline-browser-mapping": "^2.10.38", + "caniuse-lite": "^1.0.30001799", + "electron-to-chromium": "^1.5.376", + "node-releases": "^2.0.48", + "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" @@ -2329,9 +3133,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001765", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001765.tgz", - "integrity": "sha512-LWcNtSyZrakjECqmpP4qdg0MMGdN368D7X8XvvAqOcqMv0RxnlqVKZl2V6/mBR68oYMxOZPLw/gO7DuisMHUvQ==", + "version": "1.0.30001800", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001800.tgz", + "integrity": "sha512-MMHtuAz9Ys840zAY5F4k6fV5GaivZ9sPk+nz0mY+GYVzRBnYkN0mpqkSR92oWRQ19yQWo4HvBV/FnC16AJX8MA==", "dev": true, "funding": [ { @@ -2359,6 +3163,16 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -2416,6 +3230,13 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/chroma-js": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-3.2.0.tgz", + "integrity": "sha512-os/OippSlX1RlWWr+QDPcGUZs0uoqr32urfxESG9U93lhUfbnlyckte84Q8P1UQY/qth983AS1JONKmLS4T0nw==", + "dev": true, + "license": "(BSD-3-Clause AND Apache-2.0)" + }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -2455,39 +3276,618 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, - "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, - "license": "MIT" + "node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "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==", + "license": "MIT", + "dependencies": { + "layout-base": "^1.0.0" + } + }, + "node_modules/crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/cytoscape": { + "version": "3.33.1", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.1.tgz", + "integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "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==", + "license": "MIT" + }, + "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==", + "license": "ISC", + "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==", + "license": "ISC", + "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==", + "license": "ISC", + "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==", + "license": "ISC", + "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==", + "license": "ISC", + "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==", + "license": "ISC", + "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==", + "license": "ISC", + "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==", + "license": "ISC", + "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==", + "license": "ISC", + "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==", + "license": "ISC", + "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==", + "license": "ISC", + "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-dsv/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==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "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==", + "license": "BSD-3-Clause", + "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==", + "license": "ISC", + "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==", + "license": "ISC", + "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==", + "license": "ISC", + "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==", + "license": "ISC", + "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==", + "license": "ISC", + "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==", + "license": "ISC", + "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==", + "license": "ISC", + "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==", + "license": "ISC", + "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==", + "license": "ISC", + "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==", + "license": "ISC", + "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==", + "license": "BSD-3-Clause", + "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==", + "license": "BSD-3-Clause", + "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==", + "license": "BSD-3-Clause" + }, + "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==", + "license": "BSD-3-Clause", + "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==", + "license": "ISC" + }, + "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==", + "license": "ISC", + "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==", + "license": "ISC", + "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==", + "license": "ISC", + "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==", + "license": "ISC", + "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==", + "license": "ISC", + "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==", + "license": "ISC", + "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==", + "license": "ISC", + "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==", + "license": "ISC", + "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==", + "license": "ISC", + "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==", + "license": "MIT", + "dependencies": { + "d3": "^7.9.0", + "lodash-es": "^4.17.21" + } }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", "dev": true, "license": "MIT", "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" }, "engines": { - "node": ">= 8" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, - "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/dayjs": { + "version": "1.11.20", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz", + "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==", "license": "MIT" }, "node_modules/debug": { @@ -2507,6 +3907,13 @@ } } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, "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", @@ -2527,6 +3934,29 @@ "dev": true, "license": "MIT" }, + "node_modules/deep-rename-keys": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/deep-rename-keys/-/deep-rename-keys-0.2.1.tgz", + "integrity": "sha512-RHd9ABw4Fvk+gYDWqwOftG849x0bYOySl/RgX0tLI9i27ZIeSO91mLZJEp7oPHOMFqHvpgu21YptmDt0FYD/0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "kind-of": "^3.0.2", + "rename-keys": "^1.1.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/delaunator": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.1.0.tgz", + "integrity": "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==", + "license": "ISC", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -2560,77 +3990,81 @@ } }, "node_modules/diff": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.3.tgz", - "integrity": "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==", + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz", + "integrity": "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==", "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" } }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/dompurify": { + "version": "3.4.11", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.11.tgz", + "integrity": "sha512-zhlUV12GsaRzMsf9q5M254YhA4+VuF0fG+QFqu6aYpoGlKtz+w8//jBcGVYBgQkR5GHjUomejY84AV+/uPbWdw==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/electron-to-chromium": { - "version": "1.5.267", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", - "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", + "version": "1.5.384", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.384.tgz", + "integrity": "sha512-g6KAKY1vkYsADvSPWvdJsuYT0ixdcu6lUtD9P/wJKGBEDlZVXh2AX42j1mPqqaQPDluWjara9ziQ7xqAeXCt5A==", "dev": true, "license": "ISC" }, "node_modules/enhanced-resolve": { - "version": "5.18.4", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz", - "integrity": "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==", + "version": "5.20.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", + "integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==", "dev": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" + "tapable": "^2.3.0" }, "engines": { "node": ">=10.13.0" } }, - "node_modules/esbuild": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", - "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", "engines": { - "node": ">=18" + "node": ">=0.12" }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.2", - "@esbuild/android-arm": "0.27.2", - "@esbuild/android-arm64": "0.27.2", - "@esbuild/android-x64": "0.27.2", - "@esbuild/darwin-arm64": "0.27.2", - "@esbuild/darwin-x64": "0.27.2", - "@esbuild/freebsd-arm64": "0.27.2", - "@esbuild/freebsd-x64": "0.27.2", - "@esbuild/linux-arm": "0.27.2", - "@esbuild/linux-arm64": "0.27.2", - "@esbuild/linux-ia32": "0.27.2", - "@esbuild/linux-loong64": "0.27.2", - "@esbuild/linux-mips64el": "0.27.2", - "@esbuild/linux-ppc64": "0.27.2", - "@esbuild/linux-riscv64": "0.27.2", - "@esbuild/linux-s390x": "0.27.2", - "@esbuild/linux-x64": "0.27.2", - "@esbuild/netbsd-arm64": "0.27.2", - "@esbuild/netbsd-x64": "0.27.2", - "@esbuild/openbsd-arm64": "0.27.2", - "@esbuild/openbsd-x64": "0.27.2", - "@esbuild/openharmony-arm64": "0.27.2", - "@esbuild/sunos-x64": "0.27.2", - "@esbuild/win32-arm64": "0.27.2", - "@esbuild/win32-ia32": "0.27.2", - "@esbuild/win32-x64": "0.27.2" + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/es-module-lexer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-toolkit": { + "version": "1.47.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.47.0.tgz", + "integrity": "sha512-n1GuoD0WEQZMBk5tttoZSqwgyLx01oqa5XsBmCHwPyNe1S9jPBEmtR2pSgp2kJuWE3ciFZ6yRHmY4pM4C3OOkw==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -2655,25 +4089,25 @@ } }, "node_modules/eslint": { - "version": "9.39.2", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", - "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.1", + "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.39.2", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", - "ajv": "^6.12.4", + "ajv": "^6.14.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", @@ -2692,7 +4126,7 @@ "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", + "minimatch": "^3.1.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, @@ -2714,6 +4148,22 @@ } } }, + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, "node_modules/eslint-plugin-react-hooks": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", @@ -2735,13 +4185,13 @@ } }, "node_modules/eslint-plugin-react-refresh": { - "version": "0.4.26", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz", - "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==", + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.2.tgz", + "integrity": "sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA==", "dev": true, "license": "MIT", "peerDependencies": { - "eslint": ">=8.40" + "eslint": "^9 || ^10" } }, "node_modules/eslint-scope": { @@ -2774,6 +4224,30 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, "node_modules/espree": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", @@ -2838,6 +4312,16 @@ "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, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -2848,6 +4332,33 @@ "node": ">=0.10.0" } }, + "node_modules/eventemitter3": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-2.0.3.tgz", + "integrity": "sha512-jLN68Dx5kyFHaePoXWPsCGW5qdyZQtLYHkxkg02/Mz6g0kYpDx4FyP6XfArhQdlOC4b8Mv+EMxPo/8La7Tzghg==", + "dev": true, + "license": "MIT" + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "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, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -2858,7 +4369,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, "license": "MIT" }, "node_modules/fast-json-stable-stringify": { @@ -2875,6 +4385,22 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -2938,12 +4464,39 @@ } }, "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, "license": "ISC" }, + "node_modules/framer-motion": { + "version": "12.38.0", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.38.0.tgz", + "integrity": "sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.38.0", + "motion-utils": "^12.36.0", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2976,40 +4529,205 @@ "dev": true, "license": "ISC", "dependencies": { - "is-glob": "^4.0.3" + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "17.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.4.0.tgz", + "integrity": "sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "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==", + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hast-util-from-dom": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/hast-util-from-dom/-/hast-util-from-dom-5.0.1.tgz", + "integrity": "sha512-N+LqofjR2zuzTjCPzyDUdSshy4Ma6li7p/c3pA78uTwzFgENbgbUrm2ugwsOdcjI1muO+o6Dgzp9p8WHtn/39Q==", + "license": "ISC", + "dependencies": { + "@types/hast": "^3.0.0", + "hastscript": "^9.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-html": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hast-util-from-html/-/hast-util-from-html-2.0.3.tgz", + "integrity": "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "devlop": "^1.1.0", + "hast-util-from-parse5": "^8.0.0", + "parse5": "^7.0.0", + "vfile": "^6.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-html-isomorphic": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hast-util-from-html-isomorphic/-/hast-util-from-html-isomorphic-2.0.0.tgz", + "integrity": "sha512-zJfpXq44yff2hmE0XmwEOzdWin5xwH+QIhMLOScpX91e/NSGPsAzNCvLQDIEPyO2TXi+lBmU6hjLIhV8MwP2kw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-from-dom": "^5.0.0", + "hast-util-from-html": "^2.0.0", + "unist-util-remove-position": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-html/node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/hast-util-from-parse5": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz", + "integrity": "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "hastscript": "^9.0.0", + "property-information": "^7.0.0", + "vfile": "^6.0.0", + "vfile-location": "^5.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-is-element": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz", + "integrity": "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-parse-selector": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", + "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-raw": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.1.0.tgz", + "integrity": "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "@ungap/structured-clone": "^1.0.0", + "hast-util-from-parse5": "^8.0.0", + "hast-util-to-parse5": "^8.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "parse5": "^7.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" }, - "engines": { - "node": ">=10.13.0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/globals": { - "version": "16.5.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", - "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", - "dev": true, + "node_modules/hast-util-raw/node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", "license": "MIT", - "engines": { - "node": ">=18" + "dependencies": { + "entities": "^6.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/inikulin/parse5?sponsor=1" } }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, + "node_modules/hast-util-sanitize": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/hast-util-sanitize/-/hast-util-sanitize-5.0.2.tgz", + "integrity": "sha512-3yTWghByc50aGS7JlGhk61SPenfE/p1oaFeNwkOOyrscaOkMGrcW9+Cy/QAIOBpZxP1yqDIzFMR0+Np0i0+usg==", "license": "MIT", - "engines": { - "node": ">=8" + "dependencies": { + "@types/hast": "^3.0.0", + "@ungap/structured-clone": "^1.0.0", + "unist-util-position": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, "node_modules/hast-util-to-html": { @@ -3062,6 +4780,41 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hast-util-to-parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-8.0.1.tgz", + "integrity": "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-text": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-4.0.2.tgz", + "integrity": "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "hast-util-is-element": "^3.0.0", + "unist-util-find-after": "^5.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", @@ -3075,6 +4828,23 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hastscript": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz", + "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^4.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hermes-estree": { "version": "0.25.1", "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", @@ -3092,6 +4862,28 @@ "hermes-estree": "0.25.1" } }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "license": "MIT", + "dependencies": { + "void-elements": "3.1.0" + } + }, "node_modules/html-url-attributes": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", @@ -3112,6 +4904,58 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/i18next": { + "version": "26.0.1", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-26.0.1.tgz", + "integrity": "sha512-vtz5sXU4+nkCm8yEU+JJ6yYIx0mkg9e68W0G0PXpnOsmzLajNsW5o28DJMqbajxfsfq0gV3XdrBudsDQnwxfsQ==", + "funding": [ + { + "type": "individual", + "url": "https://www.locize.com/i18next" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + }, + { + "type": "individual", + "url": "https://www.locize.com" + } + ], + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2" + }, + "peerDependencies": { + "typescript": "^5 || ^6" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/i18next-browser-languagedetector": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.2.1.tgz", + "integrity": "sha512-bZg8+4bdmaOiApD7N7BPT9W8MLZG+nPTOFlLiJiT8uzKXFjhxw4v2ierCXOwB5sFDMtuA5G4kgYZ0AznZxQ/cw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.2" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -3149,12 +4993,31 @@ "node": ">=0.8.19" } }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "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==", "license": "MIT" }, + "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==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/is-alphabetical": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", @@ -3179,6 +5042,13 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true, + "license": "MIT" + }, "node_modules/is-decimal": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", @@ -3234,11 +5104,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, "license": "ISC" }, "node_modules/jiti": { @@ -3259,10 +5135,20 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -3271,6 +5157,57 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsdom": { + "version": "29.0.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.0.1.tgz", + "integrity": "sha512-z6JOK5gRO7aMybVq/y/MlIpKh8JIi68FBKMUtKkK2KH/wMSRlCxQ682d08LB9fYXplyY/UXG8P4XXTScmdjApg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^5.0.1", + "@asamuzakjp/dom-selector": "^7.0.3", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.1", + "@exodus/bytes": "^1.15.0", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.2.7", + "parse5": "^8.0.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.1", + "undici": "^7.24.5", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.1", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/lru-cache": { + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", + "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -3292,10 +5229,9 @@ "license": "MIT" }, "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "license": "MIT" }, "node_modules/json-stable-stringify-without-jsonify": { @@ -3318,6 +5254,22 @@ "node": ">=6" } }, + "node_modules/katex": { + "version": "0.16.38", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.38.tgz", + "integrity": "sha512-cjHooZUmIAUmDsHBN+1n8LaZdpmbj03LtYeYPyuYB7OuloiaeaV6N4LcfjcnHVzGWjVQmKrxxTrpDcmSzEZQwQ==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "license": "MIT", + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -3328,6 +5280,30 @@ "json-buffer": "3.0.1" } }, + "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/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "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==", + "license": "MIT" + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -3343,9 +5319,9 @@ } }, "node_modules/lightningcss": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", - "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", "dev": true, "license": "MPL-2.0", "dependencies": { @@ -3359,23 +5335,23 @@ "url": "https://opencollective.com/parcel" }, "optionalDependencies": { - "lightningcss-android-arm64": "1.30.2", - "lightningcss-darwin-arm64": "1.30.2", - "lightningcss-darwin-x64": "1.30.2", - "lightningcss-freebsd-x64": "1.30.2", - "lightningcss-linux-arm-gnueabihf": "1.30.2", - "lightningcss-linux-arm64-gnu": "1.30.2", - "lightningcss-linux-arm64-musl": "1.30.2", - "lightningcss-linux-x64-gnu": "1.30.2", - "lightningcss-linux-x64-musl": "1.30.2", - "lightningcss-win32-arm64-msvc": "1.30.2", - "lightningcss-win32-x64-msvc": "1.30.2" + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" } }, "node_modules/lightningcss-android-arm64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", - "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", "cpu": [ "arm64" ], @@ -3394,9 +5370,9 @@ } }, "node_modules/lightningcss-darwin-arm64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", - "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", "cpu": [ "arm64" ], @@ -3415,9 +5391,9 @@ } }, "node_modules/lightningcss-darwin-x64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", - "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", "cpu": [ "x64" ], @@ -3436,9 +5412,9 @@ } }, "node_modules/lightningcss-freebsd-x64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", - "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", "cpu": [ "x64" ], @@ -3457,9 +5433,9 @@ } }, "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", - "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", "cpu": [ "arm" ], @@ -3478,9 +5454,9 @@ } }, "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", - "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", "cpu": [ "arm64" ], @@ -3499,9 +5475,9 @@ } }, "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", - "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", "cpu": [ "arm64" ], @@ -3520,9 +5496,9 @@ } }, "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", - "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", "cpu": [ "x64" ], @@ -3541,9 +5517,9 @@ } }, "node_modules/lightningcss-linux-x64-musl": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz", - "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", "cpu": [ "x64" ], @@ -3562,9 +5538,9 @@ } }, "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", - "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", "cpu": [ "arm64" ], @@ -3583,9 +5559,9 @@ } }, "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", - "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", "cpu": [ "x64" ], @@ -3619,6 +5595,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "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==", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -3646,6 +5628,26 @@ "yallist": "^3.0.2" } }, + "node_modules/lucide-react": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.7.0.tgz", + "integrity": "sha512-yI7BeItCLZJTXikmK4KNUGCKoGzSvbKlfCvw44bU4fXAL6v3gYS4uHD1jzsLkfwODYwI6Drw5Tu9Z5ulDe0TSg==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -3666,6 +5668,37 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/marked": { + "version": "17.0.4", + "resolved": "https://registry.npmjs.org/marked/-/marked-17.0.4.tgz", + "integrity": "sha512-NOmVMM+KAokHMvjWmC5N/ZOvgmSWuqJB8FoYI019j4ogb/PeRMKoKIjReZ2w3376kkA8dSJIP8uD993Kxc0iRQ==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/material-icon-theme": { + "version": "5.32.0", + "resolved": "https://registry.npmjs.org/material-icon-theme/-/material-icon-theme-5.32.0.tgz", + "integrity": "sha512-SxJxCcnk6cJIbd+AxmoeghXJ24joXGmUzjiGci16sX4mXZdXprGEzM6ZZ0VHGAofxNlMqznEbExINwFLsxf8eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chroma-js": "^3.1.2", + "events": "^3.3.0", + "fast-deep-equal": "^3.1.3", + "svgson": "^5.3.1" + }, + "engines": { + "vscode": "^1.55.0" + }, + "funding": { + "url": "https://github.com/sponsors/material-extensions" + } + }, "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", @@ -3695,9 +5728,9 @@ } }, "node_modules/mdast-util-from-markdown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", - "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==", + "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==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -3819,6 +5852,25 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/mdast-util-math": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-math/-/mdast-util-math-3.0.0.tgz", + "integrity": "sha512-Tl9GBNeG/AhJnQM221bJR2HPvLOSnLE/T9cJI9tlc6zwQk2nPk/4f0cHkOdEixQPC/j8UtKDdITswvLAy1OZ1w==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "longest-streak": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.1.0", + "unist-util-remove-position": "^5.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", @@ -3948,6 +6000,54 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/mermaid": { + "version": "11.15.0", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.15.0.tgz", + "integrity": "sha512-pTMbcf3rWdtLiYGpmoTjHEpeY8seiy6sR+9nD7LOs8KfUbHE4lOUAprTRqRAcWSQ6MQpdX+YEsxShtGsINtPtw==", + "license": "MIT", + "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/mermaid/node_modules/marked": { + "version": "16.4.2", + "resolved": "https://registry.npmjs.org/marked/-/marked-16.4.2.tgz", + "integrity": "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, "node_modules/micromark": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", @@ -4138,6 +6238,25 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/micromark-extension-math": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-math/-/micromark-extension-math-3.1.0.tgz", + "integrity": "sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg==", + "license": "MIT", + "dependencies": { + "@types/katex": "^0.16.0", + "devlop": "^1.0.0", + "katex": "^0.16.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", @@ -4511,10 +6630,20 @@ ], "license": "MIT" }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -4524,6 +6653,59 @@ "node": "*" } }, + "node_modules/mlly": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", + "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", + "license": "MIT", + "dependencies": { + "acorn": "^8.16.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.3" + } + }, + "node_modules/motion": { + "version": "12.38.0", + "resolved": "https://registry.npmjs.org/motion/-/motion-12.38.0.tgz", + "integrity": "sha512-uYfXzeHlgThchzwz5Te47dlv5JOUC7OB4rjJ/7XTUgtBZD8CchMN8qEJ4ZVsUmTyYA44zjV0fBwsiktRuFnn+w==", + "license": "MIT", + "dependencies": { + "framer-motion": "^12.38.0", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/motion-dom": { + "version": "12.38.0", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.38.0.tgz", + "integrity": "sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.36.0" + } + }, + "node_modules/motion-utils": { + "version": "12.36.0", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.36.0.tgz", + "integrity": "sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -4531,9 +6713,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "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": [ { @@ -4557,10 +6739,24 @@ "license": "MIT" }, "node_modules/node-releases": { - "version": "2.0.27", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", - "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "version": "2.0.50", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.50.tgz", + "integrity": "sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], "license": "MIT" }, "node_modules/oniguruma-parser": { @@ -4630,6 +6826,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "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==", + "license": "MIT" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -4668,6 +6870,25 @@ "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", "license": "MIT" }, + "node_modules/parse5": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", + "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "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==", + "license": "MIT" + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -4682,12 +6903,17 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" } }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -4696,9 +6922,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -4708,10 +6934,37 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "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==", + "license": "MIT" + }, + "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==", + "license": "MIT", + "dependencies": { + "path-data-parser": "0.1.0", + "points-on-curve": "0.2.0" + } + }, "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", "dev": true, "funding": [ { @@ -4729,7 +6982,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -4747,6 +7000,52 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/property-information": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", @@ -4768,71 +7067,73 @@ } }, "node_modules/react": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", - "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/react-dom": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", - "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "license": "MIT", "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^19.2.3" + "react": "^19.2.4" } }, - "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==", + "node_modules/react-i18next": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-17.0.1.tgz", + "integrity": "sha512-iG65FGnFHcYyHNuT01ukffYWCOBFTWSdVD8EZd/dCVWgtjFPObcSsvYYNwcsokO/rDcTb5d6D8Acv8MrOdm6Hw==", "license": "MIT", "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" + "@babel/runtime": "^7.29.2", + "html-parse-stringify": "^3.0.1", + "use-sync-external-store": "^1.6.0" }, "peerDependencies": { - "@types/react": ">=18", - "react": ">=18" + "i18next": ">= 26.0.1", + "react": ">= 16.8.0", + "typescript": "^5 || ^6" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + }, + "typescript": { + "optional": true + } } }, - "node_modules/react-refresh": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", - "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true, "license": "MIT", - "engines": { - "node": ">=0.10.0" - } + "peer": true }, - "node_modules/react-virtuoso": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/react-virtuoso/-/react-virtuoso-4.18.1.tgz", - "integrity": "sha512-KF474cDwaSb9+SJ380xruBB4P+yGWcVkcu26HtMqYNMTYlYbrNy8vqMkE+GpAApPPufJqgOLMoWMFG/3pJMXUA==", + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, "license": "MIT", - "peerDependencies": { - "react": ">=16 || >=17 || >= 18 || >= 19", - "react-dom": ">=16 || >=17 || >= 18 || >=19" + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" } }, "node_modules/regex": { @@ -4859,6 +7160,63 @@ "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==", "license": "MIT" }, + "node_modules/rehype-harden": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/rehype-harden/-/rehype-harden-1.1.8.tgz", + "integrity": "sha512-Qn7vR1xrf6fZCrkm9TDWi/AB4ylrHy+jqsNm1EHOAmbARYA6gsnVJBq/sdBh6kmT4NEZxH5vgIjrscefJAOXcw==", + "license": "MIT", + "dependencies": { + "unist-util-visit": "^5.0.0" + } + }, + "node_modules/rehype-katex": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/rehype-katex/-/rehype-katex-7.0.1.tgz", + "integrity": "sha512-OiM2wrZ/wuhKkigASodFoo8wimG3H12LWQaH8qSPVJn9apWKFSH3YOCtbKpBorTVw/eI7cuT21XBbvwEswbIOA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/katex": "^0.16.0", + "hast-util-from-html-isomorphic": "^2.0.0", + "hast-util-to-text": "^4.0.0", + "katex": "^0.16.0", + "unist-util-visit-parents": "^6.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-raw": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/rehype-raw/-/rehype-raw-7.0.0.tgz", + "integrity": "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-raw": "^9.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-sanitize": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/rehype-sanitize/-/rehype-sanitize-6.0.0.tgz", + "integrity": "sha512-CsnhKNsyI8Tub6L4sm5ZFsme4puGfc6pYylvXo1AeqaGbjOYyzNv3qZPwvs0oMJ39eryyeOdmxwUIo94IpEhqg==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-sanitize": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/remark-gfm": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", @@ -4877,6 +7235,22 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/remark-math": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/remark-math/-/remark-math-6.0.0.tgz", + "integrity": "sha512-MMqgnP74Igy+S3WwnhQ7kqGlEerTETXMvJhrUzDikVZ2/uogJCb+WHUg97hK9/jcfc0dkD73s3LN8zU49cTEtA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-math": "^3.0.0", + "micromark-extension-math": "^3.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", @@ -4925,6 +7299,31 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/remend": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/remend/-/remend-1.3.0.tgz", + "integrity": "sha512-iIhggPkhW3hFImKtB10w0dz4EZbs28mV/dmbcYVonWEJ6UGHHpP+bFZnTh6GNWJONg5m+U56JrL+8IxZRdgWjw==", + "license": "Apache-2.0" + }, + "node_modules/rename-keys": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/rename-keys/-/rename-keys-1.2.0.tgz", + "integrity": "sha512-U7XpAktpbSgHTRSNRrjKSrjYkZKuhUukfoBlXWXUExCAqhzh1TU3BDRAfJmarcl5voKS+pbKU9MvyLWKZ4UEEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -4935,49 +7334,88 @@ "node": ">=4" } }, - "node_modules/rollup": { - "version": "4.56.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.56.0.tgz", - "integrity": "sha512-9FwVqlgUHzbXtDg9RCMgodF3Ua4Na6Gau+Sdt9vyCN4RhHfVKX2DCHy3BjMLTDd47ITDhYAnTwGulWTblJSDLg==", + "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==", + "license": "Unlicense" + }, + "node_modules/rolldown": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", + "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.8" + "@oxc-project/types": "=0.133.0", + "@rolldown/pluginutils": "^1.0.0" }, "bin": { - "rollup": "dist/bin/rollup" + "rolldown": "bin/cli.mjs" }, "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" + "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.56.0", - "@rollup/rollup-android-arm64": "4.56.0", - "@rollup/rollup-darwin-arm64": "4.56.0", - "@rollup/rollup-darwin-x64": "4.56.0", - "@rollup/rollup-freebsd-arm64": "4.56.0", - "@rollup/rollup-freebsd-x64": "4.56.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.56.0", - "@rollup/rollup-linux-arm-musleabihf": "4.56.0", - "@rollup/rollup-linux-arm64-gnu": "4.56.0", - "@rollup/rollup-linux-arm64-musl": "4.56.0", - "@rollup/rollup-linux-loong64-gnu": "4.56.0", - "@rollup/rollup-linux-loong64-musl": "4.56.0", - "@rollup/rollup-linux-ppc64-gnu": "4.56.0", - "@rollup/rollup-linux-ppc64-musl": "4.56.0", - "@rollup/rollup-linux-riscv64-gnu": "4.56.0", - "@rollup/rollup-linux-riscv64-musl": "4.56.0", - "@rollup/rollup-linux-s390x-gnu": "4.56.0", - "@rollup/rollup-linux-x64-gnu": "4.56.0", - "@rollup/rollup-linux-x64-musl": "4.56.0", - "@rollup/rollup-openbsd-x64": "4.56.0", - "@rollup/rollup-openharmony-arm64": "4.56.0", - "@rollup/rollup-win32-arm64-msvc": "4.56.0", - "@rollup/rollup-win32-ia32-msvc": "4.56.0", - "@rollup/rollup-win32-x64-gnu": "4.56.0", - "@rollup/rollup-win32-x64-msvc": "4.56.0", - "fsevents": "~2.3.2" + "@rolldown/binding-android-arm64": "1.0.3", + "@rolldown/binding-darwin-arm64": "1.0.3", + "@rolldown/binding-darwin-x64": "1.0.3", + "@rolldown/binding-freebsd-x64": "1.0.3", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", + "@rolldown/binding-linux-arm64-gnu": "1.0.3", + "@rolldown/binding-linux-arm64-musl": "1.0.3", + "@rolldown/binding-linux-ppc64-gnu": "1.0.3", + "@rolldown/binding-linux-s390x-gnu": "1.0.3", + "@rolldown/binding-linux-x64-gnu": "1.0.3", + "@rolldown/binding-linux-x64-musl": "1.0.3", + "@rolldown/binding-openharmony-arm64": "1.0.3", + "@rolldown/binding-wasm32-wasi": "1.0.3", + "@rolldown/binding-win32-arm64-msvc": "1.0.3", + "@rolldown/binding-win32-x64-msvc": "1.0.3" + } + }, + "node_modules/rolldown/node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "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==", + "license": "MIT", + "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/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "license": "BSD-3-Clause" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" } }, "node_modules/scheduler": { @@ -5000,7 +7438,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -5013,28 +7450,87 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/shiki": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/shiki/-/shiki-3.21.0.tgz", - "integrity": "sha512-N65B/3bqL/TI2crrXr+4UivctrAGEjmsib5rPMMPpFp1xAx/w03v8WZ9RDDFYteXoEgY7qZ4HGgl5KBIu1153w==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-4.0.2.tgz", + "integrity": "sha512-eAVKTMedR5ckPo4xne/PjYQYrU3qx78gtJZ+sHlXEg5IHhhoQhMfZVzetTYuaJS0L2Ef3AcCRzCHV8T0WI6nIQ==", + "license": "MIT", + "dependencies": { + "@shikijs/core": "4.0.2", + "@shikijs/engine-javascript": "4.0.2", + "@shikijs/engine-oniguruma": "4.0.2", + "@shikijs/langs": "4.0.2", + "@shikijs/themes": "4.0.2", + "@shikijs/types": "4.0.2", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/shiki-stream": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/shiki-stream/-/shiki-stream-0.1.4.tgz", + "integrity": "sha512-4pz6JGSDmVTTkPJ/ueixHkFAXY4ySCc+unvCaDZV7hqq/sdJZirRxgIXSuNSKgiFlGTgRR97sdu2R8K55sPsrw==", + "license": "MIT", + "dependencies": { + "@shikijs/core": "^3.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "react": "^19.0.0", + "solid-js": "^1.9.0", + "vue": "^3.2.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "solid-js": { + "optional": true + }, + "vue": { + "optional": true + } + } + }, + "node_modules/shiki-stream/node_modules/@shikijs/core": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-3.23.0.tgz", + "integrity": "sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.23.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4", + "hast-util-to-html": "^9.0.5" + } + }, + "node_modules/shiki-stream/node_modules/@shikijs/types": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.23.0.tgz", + "integrity": "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ==", "license": "MIT", "dependencies": { - "@shikijs/core": "3.21.0", - "@shikijs/engine-javascript": "3.21.0", - "@shikijs/engine-oniguruma": "3.21.0", - "@shikijs/langs": "3.21.0", - "@shikijs/themes": "3.21.0", - "@shikijs/types": "3.21.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, + "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, + "license": "ISC" + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -5055,6 +7551,48 @@ "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, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.0.0.tgz", + "integrity": "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/streamdown": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/streamdown/-/streamdown-2.5.0.tgz", + "integrity": "sha512-/tTnURfIOxZK/pqJAxsfCvETG/XCJHoWnk3jq9xLcuz6CSpnjjuxSRBTTL4PKGhxiZQf0lqPxGhImdpwcZ2XwA==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1", + "hast-util-to-jsx-runtime": "^2.3.6", + "html-url-attributes": "^3.0.1", + "marked": "^17.0.1", + "mermaid": "^11.12.2", + "rehype-harden": "^1.1.8", + "rehype-raw": "^7.0.0", + "rehype-sanitize": "^6.0.0", + "remark-gfm": "^4.0.1", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.1.2", + "remend": "1.3.0", + "tailwind-merge": "^3.4.0", + "unified": "^11.0.5", + "unist-util-visit": "^5.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, "node_modules/stringify-entities": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", @@ -5069,6 +7607,19 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -5082,6 +7633,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/style-mod": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz", + "integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==", + "license": "MIT" + }, "node_modules/style-to-js": { "version": "1.1.21", "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", @@ -5100,6 +7657,12 @@ "inline-style-parser": "0.2.7" } }, + "node_modules/stylis": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz", + "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==", + "license": "MIT" + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -5113,10 +7676,28 @@ "node": ">=8" } }, + "node_modules/svgson": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/svgson/-/svgson-5.3.1.tgz", + "integrity": "sha512-qdPgvUNWb40gWktBJnbJRelWcPzkLed/ShhnRsjbayXz8OtdPOzbil9jtiZdrYvSDumAz/VNQr6JaNfPx/gvPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-rename-keys": "^0.2.1", + "xml-reader": "2.4.3" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, "node_modules/tailwind-merge": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.4.0.tgz", - "integrity": "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz", + "integrity": "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==", "license": "MIT", "funding": { "type": "github", @@ -5124,16 +7705,16 @@ } }, "node_modules/tailwindcss": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", - "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.2.tgz", + "integrity": "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==", "dev": true, "license": "MIT" }, "node_modules/tapable": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", - "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.2.tgz", + "integrity": "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==", "dev": true, "license": "MIT", "engines": { @@ -5144,15 +7725,31 @@ "url": "https://opencollective.com/webpack" } }, + "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, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", - "picomatch": "^4.0.3" + "picomatch": "^4.0.4" }, "engines": { "node": ">=12.0.0" @@ -5161,6 +7758,62 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "7.0.25", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.25.tgz", + "integrity": "sha512-keinCnPbwXEUG3ilrWQZU+CqcTTzHq9m2HhoUP2l7Xmi8l1LuijAXLpAJ5zRW+ifKTNscs4NdCkfkDCBYm352w==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.0.25" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.0.25", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.25.tgz", + "integrity": "sha512-ZjCZK0rppSBu7rjHYDYsEaMOIbbT+nWF57hKkv4IUmZWBNrBWBOjIElc0mKRgLM8bm7x/BBlof6t2gi/Oq/Asw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tough-cookie": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", + "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/trim-lines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", @@ -5182,9 +7835,9 @@ } }, "node_modules/ts-api-utils": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", - "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", "dev": true, "license": "MIT", "engines": { @@ -5194,6 +7847,21 @@ "typescript": ">=4.8.4" } }, + "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==", + "license": "MIT", + "engines": { + "node": ">=6.10" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -5211,7 +7879,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -5222,16 +7890,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.53.1", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.53.1.tgz", - "integrity": "sha512-gB+EVQfP5RDElh9ittfXlhZJdjSU4jUSTyE2+ia8CYyNvet4ElfaLlAIqDvQV9JPknKx0jQH1racTYe/4LaLSg==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.57.2.tgz", + "integrity": "sha512-VEPQ0iPgWO/sBaZOU1xo4nuNdODVOajPnTIbog2GKYr31nIlZ0fWPoCQgGfF3ETyBl1vn63F/p50Um9Z4J8O8A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.53.1", - "@typescript-eslint/parser": "8.53.1", - "@typescript-eslint/typescript-estree": "8.53.1", - "@typescript-eslint/utils": "8.53.1" + "@typescript-eslint/eslint-plugin": "8.57.2", + "@typescript-eslint/parser": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2", + "@typescript-eslint/utils": "8.57.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5241,14 +7909,30 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, + "node_modules/ufo": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz", + "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==", + "license": "MIT" + }, + "node_modules/undici": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", + "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/undici-types": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", - "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", "dev": true, "license": "MIT" }, @@ -5271,6 +7955,20 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/unist-util-find-after": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz", + "integrity": "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^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", @@ -5297,6 +7995,20 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/unist-util-remove-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-5.0.0.tgz", + "integrity": "sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-visit": "^5.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", @@ -5380,6 +8092,28 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "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" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, "node_modules/vfile": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", @@ -5394,6 +8128,20 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/vfile-location": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz", + "integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile": "^6.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", @@ -5409,18 +8157,17 @@ } }, "node_modules/vite": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", - "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "version": "8.0.16", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz", + "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.27.0", - "fdir": "^6.5.0", - "picomatch": "^4.0.3", - "postcss": "^8.5.6", - "rollup": "^4.43.0", - "tinyglobby": "^0.2.15" + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.15", + "rolldown": "1.0.3", + "tinyglobby": "^0.2.17" }, "bin": { "vite": "bin/vite.js" @@ -5436,9 +8183,10 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.18", + "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", - "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", @@ -5451,13 +8199,16 @@ "@types/node": { "optional": true }, - "jiti": { + "@vitejs/devtools": { "optional": true }, - "less": { + "esbuild": { + "optional": true + }, + "jiti": { "optional": true }, - "lightningcss": { + "less": { "optional": true }, "sass": { @@ -5483,11 +8234,165 @@ } } }, + "node_modules/vitest": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.2.tgz", + "integrity": "sha512-xjR1dMTVHlFLh98JE3i/f/WePqJsah4A0FK9cc8Ehp9Udk0AZk6ccpIZhh1qJ/yxVWRZ+Q54ocnD8TXmkhspGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.2", + "@vitest/mocker": "4.1.2", + "@vitest/pretty-format": "4.1.2", + "@vitest/runner": "4.1.2", + "@vitest/snapshot": "4.1.2", + "@vitest/spy": "4.1.2", + "@vitest/utils": "4.1.2", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.2", + "@vitest/browser-preview": "4.1.2", + "@vitest/browser-webdriverio": "4.1.2", + "@vitest/ui": "4.1.2", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", + "license": "MIT" + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/web-namespaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", + "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -5499,6 +8404,23 @@ "node": ">= 8" } }, + "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, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -5509,6 +8431,44 @@ "node": ">=0.10.0" } }, + "node_modules/xml-lexer": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/xml-lexer/-/xml-lexer-0.2.2.tgz", + "integrity": "sha512-G0i98epIwiUEiKmMcavmVdhtymW+pCAohMRgybyIME9ygfVu8QheIi+YoQh3ngiThsT0SQzJT4R0sKDEv8Ou0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "eventemitter3": "^2.0.0" + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xml-reader": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/xml-reader/-/xml-reader-2.4.3.tgz", + "integrity": "sha512-xWldrIxjeAMAu6+HSf9t50ot1uL5M+BtOidRCWHXIeewvSeIpscWCsp4Zxjk8kHHhdqFBrfK8U0EJeCcnyQ/gA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eventemitter3": "^2.0.0", + "xml-lexer": "^0.2.2" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -5530,9 +8490,9 @@ } }, "node_modules/zod": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.5.tgz", - "integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==", + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "dev": true, "license": "MIT", "funding": { diff --git a/package.json b/package.json index 8980457c..5a95142c 100644 --- a/package.json +++ b/package.json @@ -1,45 +1,83 @@ { "name": "opencodeui", - "version": "0.1.0", + "version": "0.6.23", "description": "A third-party web frontend for OpenCode", "license": "GPL-3.0-only", "type": "module", "scripts": { "dev": "vite", + "typecheck": "tsc -b", + "type-check": "npm run typecheck", "build": "tsc -b && vite build", + "icons:tauri": "tauri icon src-tauri/app-icon.manifest.json -o src-tauri/icons", "lint": "eslint .", - "preview": "vite preview" + "format": "prettier . --write", + "format:check": "prettier . --check", + "test": "vitest", + "test:run": "vitest run", + "validate": "npm run typecheck && npm run lint && npm run test:run && npm run build", + "release:prepare": "node scripts/prepare-release.mjs", + "check": "npm run format:check && npm run lint && npm run test:run && npm run build", + "preview": "vite preview", + "tauri": "tauri", + "postinstall": "node scripts/copy-material-icons.mjs" }, "dependencies": { - "@types/diff": "^7.0.2", + "@codemirror/commands": "^6.10.3", + "@codemirror/search": "^6.7.0", + "@codemirror/state": "^6.6.0", + "@codemirror/view": "^6.41.1", + "@opencode-ai/sdk": "^1.16.0", + "@streamdown/math": "^1.0.2", + "@tauri-apps/api": "^2.10.1", + "@tauri-apps/plugin-dialog": "^2.6.0", + "@tauri-apps/plugin-fs": "^2.4.5", + "@tauri-apps/plugin-http": "^2.5.7", + "@tauri-apps/plugin-notification": "^2.3.3", + "@tauri-apps/plugin-opener": "^2.5.3", "@xterm/addon-fit": "^0.11.0", + "@xterm/addon-serialize": "^0.14.0", "@xterm/addon-web-links": "^0.12.0", "@xterm/addon-webgl": "^0.19.0", "@xterm/xterm": "^6.0.0", + "ajv": "^8.20.0", "clsx": "^2.1.1", - "diff": "^8.0.3", + "diff": "^8.0.4", + "i18next": "^26.0.1", + "i18next-browser-languagedetector": "^8.2.1", + "lucide-react": "^1.7.0", + "mermaid": "^11.13.0", + "motion": "^12.38.0", "react": "^19.2.0", "react-dom": "^19.2.0", - "react-markdown": "^10.1.0", - "react-virtuoso": "^4.18.1", - "remark-gfm": "^4.0.1", - "shiki": "^3.21.0", - "tailwind-merge": "^3.4.0" + "react-i18next": "^17.0.1", + "shiki": "^4.0.2", + "shiki-stream": "^0.1.4", + "streamdown": "^2.5.0" }, "devDependencies": { - "@eslint/js": "^9.39.1", - "@tailwindcss/vite": "^4.1.18", - "@types/node": "^24.10.1", + "@eslint/js": "^9.39.4", + "@tailwindcss/vite": "^4.2.2", + "@tauri-apps/cli": "^2.10.0", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.0", + "@types/diff": "^7.0.2", + "@types/node": "^25.5.0", "@types/react": "^19.2.5", "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^5.1.1", - "eslint": "^9.39.1", + "@vitejs/plugin-react": "^6.0.1", + "eslint": "^9.39.4", + "eslint-config-prettier": "^10.1.8", "eslint-plugin-react-hooks": "^7.0.1", - "eslint-plugin-react-refresh": "^0.4.24", - "globals": "^16.5.0", - "tailwindcss": "^4.1.18", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.4.0", + "jsdom": "^29.0.1", + "material-icon-theme": "^5.32.0", + "prettier": "^3.6.2", + "tailwindcss": "^4.2.2", "typescript": "~5.9.3", - "typescript-eslint": "^8.46.4", - "vite": "^7.2.4" + "typescript-eslint": "^8.57.2", + "vite": "^8.0.3", + "vitest": "^4.1.2" } } diff --git a/public/404.html b/public/404.html new file mode 100644 index 00000000..7320d484 --- /dev/null +++ b/public/404.html @@ -0,0 +1,26 @@ + + + + + + + diff --git a/public/fonts/JetBrainsMono-Regular.woff2 b/public/fonts/JetBrainsMono-Regular.woff2 deleted file mode 100644 index 40da4276..00000000 Binary files a/public/fonts/JetBrainsMono-Regular.woff2 and /dev/null differ diff --git a/public/notification-sw.js b/public/notification-sw.js index ad52d5ff..74cf9a73 100644 --- a/public/notification-sw.js +++ b/public/notification-sw.js @@ -4,7 +4,7 @@ // 专门用于显示通知(Android Chrome 不支持 new Notification()) // 不做缓存、不拦截 fetch,只处理通知相关事件 -self.addEventListener('notificationclick', (event) => { +self.addEventListener('notificationclick', event => { event.notification.close() const data = event.notification.data @@ -19,7 +19,7 @@ self.addEventListener('notificationclick', (event) => { // 聚焦已有窗口或打开新窗口 event.waitUntil( - clients.matchAll({ type: 'window', includeUncontrolled: true }).then((windowClients) => { + clients.matchAll({ type: 'window', includeUncontrolled: true }).then(windowClients => { // 找到同源的已有窗口 for (const client of windowClients) { if (client.url.startsWith(self.location.origin) && 'focus' in client) { @@ -34,6 +34,6 @@ self.addEventListener('notificationclick', (event) => { } // 没有已有窗口,打开新的 return clients.openWindow(url) - }) + }), ) }) diff --git a/scripts/bump-version.mjs b/scripts/bump-version.mjs new file mode 100644 index 00000000..1f3dea3d --- /dev/null +++ b/scripts/bump-version.mjs @@ -0,0 +1,204 @@ +#!/usr/bin/env node + +/** + * bump-version.mjs - One-command version bump for OpenCodeUI + * + * Usage: + * node scripts/bump-version.mjs + * + * Examples: + * node scripts/bump-version.mjs 0.2.0 # stable release + * node scripts/bump-version.mjs 0.2.1-canary.1 # canary release + * + * What it does: + * 1. Updates version in package.json, src-tauri/Cargo.toml, src-tauri/tauri.conf.json + * 2. Prepends a new entry to CHANGELOG.md with git log since last tag + * 3. Prints the git commands you need to run next (tag + push) + */ + +import { readFileSync, writeFileSync, existsSync } from 'fs' +import { execSync } from 'child_process' +import { resolve, dirname } from 'path' +import { fileURLToPath } from 'url' + +const __dirname = dirname(fileURLToPath(import.meta.url)) +const root = resolve(__dirname, '..') + +// --------------------------------------------------------------------------- +// Parse args +// --------------------------------------------------------------------------- +const version = process.argv[2] + +if (!version) { + console.error('Usage: node scripts/bump-version.mjs ') + console.error(' e.g. node scripts/bump-version.mjs 0.2.0') + console.error(' e.g. node scripts/bump-version.mjs 0.2.1-canary.1') + process.exit(1) +} + +// Basic semver validation (with optional prerelease) +const semverRe = /^\d+\.\d+\.\d+(-[a-zA-Z0-9]+(\.\d+)?)?$/ +if (!semverRe.test(version)) { + console.error(`Invalid semver: "${version}"`) + console.error('Expected format: MAJOR.MINOR.PATCH or MAJOR.MINOR.PATCH-prerelease.N') + process.exit(1) +} + +const tagName = `v${version}` +const today = new Date().toISOString().slice(0, 10) +const isPrerelease = version.includes('-') +const stableTagRe = /^v\d+\.\d+\.\d+$/ +const existingChangelogPath = resolve(root, 'CHANGELOG.md') +const lineEnding = + existsSync(existingChangelogPath) && /\r\n/.test(readFileSync(existingChangelogPath, 'utf-8')) ? '\r\n' : '\n' + +function formatWithPrettier(relativePath) { + execSync(`npx prettier --write "${relativePath}"`, { + cwd: root, + stdio: 'pipe', + }) +} + +function replaceCargoPackageVersion(lockContent, packageName, nextVersion) { + const packageBlocks = lockContent.split('[[package]]') + const updatedBlocks = packageBlocks.map((block, index) => { + if (index === 0) return block + if (!block.includes(`name = "${packageName}"`)) return block + return block.replace(/^(version\s*=\s*)"[^"]*"/m, `$1"${nextVersion}"`) + }) + return updatedBlocks.join('[[package]]') +} + +function getReleaseBaseTag() { + if (isPrerelease) { + return execSync('git describe --tags --abbrev=0 2>/dev/null', { + encoding: 'utf-8', + cwd: root, + }).trim() + } + + const mergedTags = execSync('git tag --merged HEAD --sort=-v:refname', { + encoding: 'utf-8', + cwd: root, + }) + .split(/\r?\n/) + .map(tag => tag.trim()) + .filter(Boolean) + + const lastStableTag = mergedTags.find(tag => stableTagRe.test(tag) && tag !== tagName) + if (!lastStableTag) { + throw new Error('No previous stable tag found') + } + + return lastStableTag +} + +// --------------------------------------------------------------------------- +// 1. Update package.json +// --------------------------------------------------------------------------- +const pkgPath = resolve(root, 'package.json') +const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')) +const oldVersion = pkg.version + +execSync(`npm version ${version} --no-git-tag-version --allow-same-version`, { + cwd: root, + stdio: 'pipe', +}) +console.log(` package.json ${oldVersion} -> ${version}`) +console.log(` package-lock.json ${oldVersion} -> ${version}`) + +// --------------------------------------------------------------------------- +// 2. Update src-tauri/Cargo.toml +// --------------------------------------------------------------------------- +const cargoPath = resolve(root, 'src-tauri/Cargo.toml') +let cargo = readFileSync(cargoPath, 'utf-8') +const cargoPackageNameMatch = cargo.match(/^(name\s*=\s*)"([^"]+)"/m) +const cargoPackageName = cargoPackageNameMatch?.[2] +cargo = cargo.replace(/^(version\s*=\s*)"[^"]*"/m, `$1"${version}"`) +writeFileSync(cargoPath, cargo) +console.log(` src-tauri/Cargo.toml ${oldVersion} -> ${version}`) + +// --------------------------------------------------------------------------- +// 3. Update src-tauri/tauri.conf.json +// --------------------------------------------------------------------------- +const tauriConfPath = resolve(root, 'src-tauri/tauri.conf.json') +const tauriConf = JSON.parse(readFileSync(tauriConfPath, 'utf-8')) +tauriConf.version = version +writeFileSync(tauriConfPath, JSON.stringify(tauriConf, null, 2) + '\n') +formatWithPrettier('src-tauri/tauri.conf.json') +console.log(` src-tauri/tauri.conf ${oldVersion} -> ${version}`) + +// --------------------------------------------------------------------------- +// 4. Update src-tauri/Cargo.lock (workspace package entry) +// --------------------------------------------------------------------------- +const cargoLockPath = resolve(root, 'src-tauri/Cargo.lock') +if (cargoPackageName && existsSync(cargoLockPath)) { + const cargoLock = readFileSync(cargoLockPath, 'utf-8') + const updatedCargoLock = replaceCargoPackageVersion(cargoLock, cargoPackageName, version) + if (updatedCargoLock !== cargoLock) { + writeFileSync(cargoLockPath, updatedCargoLock) + console.log(` src-tauri/Cargo.lock ${oldVersion} -> ${version}`) + } +} + +// --------------------------------------------------------------------------- +// 5. Generate changelog entry from git log +// --------------------------------------------------------------------------- +let commits = '' +try { + // Stable release: compare from previous stable tag. + // Pre-release: compare from the latest reachable tag. + const lastTag = getReleaseBaseTag() + + commits = execSync(`git log ${lastTag}..HEAD --pretty=format:"- %s (%h)" --no-merges`, { + encoding: 'utf-8', + cwd: root, + }).trim() +} catch { + // No previous tag — include all commits + try { + commits = execSync('git log --pretty=format:"- %s (%h)" --no-merges', { + encoding: 'utf-8', + cwd: root, + }).trim() + } catch { + commits = '- Initial release' + } +} + +if (!commits) { + commits = '- No changes since last tag' +} + +const releaseType = isPrerelease ? ' (Pre-release)' : '' +const changelogEntry = `## [${tagName}] - ${today}${releaseType}${lineEnding}${lineEnding}${commits.replace(/\n/g, lineEnding)}${lineEnding}` + +const changelogPath = resolve(root, 'CHANGELOG.md') +if (existsSync(changelogPath)) { + const existing = readFileSync(changelogPath, 'utf-8') + const headerSeparator = existing.match(/\r?\n\r?\n/) + if (headerSeparator) { + const headerEnd = existing.indexOf(headerSeparator[0]) + const separatorLength = headerSeparator[0].length + const header = existing.slice(0, headerEnd + separatorLength) + const body = existing.slice(headerEnd + separatorLength) + writeFileSync(changelogPath, header + changelogEntry + lineEnding + body) + } else { + writeFileSync(changelogPath, existing + lineEnding + changelogEntry) + } +} else { + writeFileSync(changelogPath, `# Changelog${lineEnding}${lineEnding}${changelogEntry}`) +} +console.log(` CHANGELOG.md added entry for ${tagName}`) + +// --------------------------------------------------------------------------- +// 6. Print next steps +// --------------------------------------------------------------------------- +console.log(` +Done! Next steps: + + git add -A + git commit -m "chore: bump version to ${version}" + git tag ${tagName} + git push && git push origin ${tagName} +`) diff --git a/scripts/copy-material-icons.mjs b/scripts/copy-material-icons.mjs new file mode 100644 index 00000000..dce4b74c --- /dev/null +++ b/scripts/copy-material-icons.mjs @@ -0,0 +1,70 @@ +/** + * Copy only the material-icon-theme SVGs that are actually referenced + * by src/utils/materialIcons.ts to public/material-icons/. + * + * Runs automatically via postinstall hook. + */ + +import { copyFileSync, mkdirSync, existsSync, readFileSync, rmSync } from 'fs' +import { resolve, dirname } from 'path' +import { fileURLToPath } from 'url' + +const __dirname = dirname(fileURLToPath(import.meta.url)) +const root = resolve(__dirname, '..') + +const srcDir = resolve(root, 'node_modules/material-icon-theme/icons') +const destDir = resolve(root, 'public/material-icons') + +if (!existsSync(srcDir)) { + console.warn('[copy-material-icons] Source not found, skipping:', srcDir) + process.exit(0) +} + +// Parse materialIcons.ts to extract every icon name referenced +const tsSource = readFileSync(resolve(root, 'src/utils/materialIcons.ts'), 'utf-8') + +const icons = new Set() + +// Icon values after colon: 'icon-name' +for (const m of tsSource.matchAll(/:\s*'([a-z0-9_-]+)'/g)) { + icons.add(m[1]) +} +// Array items [ext, icon] +for (const m of tsSource.matchAll(/\[\s*'[^']+'\s*,\s*'([a-z0-9_-]+)'\s*\]/g)) { + icons.add(m[1]) +} +// Defaults +icons.add('file') +icons.add('folder') +icons.add('folder-open') + +// Folder icons need both base and -open variants +for (const icon of [...icons]) { + if (icon.startsWith('folder-') && !icon.endsWith('-open')) { + icons.add(icon + '-open') + } +} + +// Clean destination and copy only needed files +if (existsSync(destDir)) { + rmSync(destDir, { recursive: true }) +} +mkdirSync(destDir, { recursive: true }) + +let copied = 0 +let missing = 0 +for (const icon of icons) { + const srcPath = resolve(srcDir, icon + '.svg') + const destPath = resolve(destDir, icon + '.svg') + if (existsSync(srcPath)) { + copyFileSync(srcPath, destPath) + copied++ + } else { + console.warn(` [WARN] Missing: ${icon}.svg`) + missing++ + } +} + +console.log( + `[copy-material-icons] Copied ${copied} icons to public/material-icons/` + (missing ? ` (${missing} missing)` : ''), +) diff --git a/scripts/dev-android.sh b/scripts/dev-android.sh new file mode 100644 index 00000000..cfafc3ef --- /dev/null +++ b/scripts/dev-android.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +# Set environment variables for Android development +export ANDROID_HOME="/e/app/Android/Sdk" +export NDK_HOME="/e/app/Android/Sdk/ndk/29.0.14206865" +export JAVA_HOME="/e/app/Android/Android Studio/jbr" +export PATH="$HOME/.cargo/bin:$PATH" + +echo "Checking connected Android devices..." +adb devices + +echo "Starting Tauri Android Dev..." +npm run tauri android dev diff --git a/scripts/prepare-release.mjs b/scripts/prepare-release.mjs new file mode 100644 index 00000000..06fa03ad --- /dev/null +++ b/scripts/prepare-release.mjs @@ -0,0 +1,94 @@ +#!/usr/bin/env node + +/** + * prepare-release.mjs - Validate before version bumping + * + * Usage: + * node scripts/prepare-release.mjs + * node scripts/prepare-release.mjs --skip-validate + * + * What it does: + * 1. Ensures the git worktree is clean before starting + * 2. Runs `npm run validate` by default + * 3. Runs bump-version.mjs to update release files + * 4. Prints the remaining git steps (commit, tag, push) + */ + +import { execSync } from 'child_process' +import { dirname, resolve } from 'path' +import { fileURLToPath } from 'url' + +const __dirname = dirname(fileURLToPath(import.meta.url)) +const root = resolve(__dirname, '..') + +const args = process.argv.slice(2) +const showHelp = args.includes('--help') || args.includes('-h') +const skipValidate = args.includes('--skip-validate') +const version = args.find(arg => !arg.startsWith('-')) + +if (showHelp || !version) { + console.log('Usage: node scripts/prepare-release.mjs [--skip-validate]') + console.log(' e.g. node scripts/prepare-release.mjs 0.2.0') + console.log(' e.g. node scripts/prepare-release.mjs 0.2.1-canary.1') + process.exit(showHelp ? 0 : 1) +} + +const tagName = `v${version}` + +function run(command, label) { + console.log(`\n> ${label}`) + execSync(command, { cwd: root, stdio: 'inherit' }) +} + +function read(command) { + return execSync(command, { + cwd: root, + encoding: 'utf-8', + stdio: ['ignore', 'pipe', 'pipe'], + }).trim() +} + +const worktreeStatus = read('git status --short') +if (worktreeStatus) { + console.error('Release preparation requires a clean git worktree.') + console.error('Please commit, stash, or discard local changes first.') + process.exit(1) +} + +try { + const localTag = read(`git tag -l ${tagName}`) + if (localTag === tagName) { + console.error(`Tag ${tagName} already exists locally.`) + process.exit(1) + } +} catch { + // Ignore local tag lookup failures and continue. +} + +try { + const remoteTag = read(`git ls-remote --tags origin refs/tags/${tagName}`) + if (remoteTag) { + console.error(`Tag ${tagName} already exists on origin.`) + process.exit(1) + } +} catch { + // If origin is unavailable, let later git commands surface the real error. +} + +if (!skipValidate) { + run('npm run validate', 'Running release validation') +} else { + console.log('\n> Skipping release validation (--skip-validate)') +} + +run(`node scripts/bump-version.mjs ${version}`, `Preparing release ${tagName}`) + +console.log(` +Release preparation finished for ${tagName}. + +Next steps: + git add -A + git commit -m "chore: bump version to ${version}" + git tag ${tagName} + git push && git push origin ${tagName} +`) diff --git a/src-router/Cargo.lock b/src-router/Cargo.lock new file mode 100644 index 00000000..c75e1e44 --- /dev/null +++ b/src-router/Cargo.lock @@ -0,0 +1,930 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +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 = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +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", +] + +[[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", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "axum" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" +dependencies = [ + "axum-core", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "env_filter" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a1c3cc8e57274ec99de65301228b537f1e4eedc1b8e0f9411c6caac8ae7308f" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2daee4ea451f429a58296525ddf28b45a3b64f1acf6587e2067437bb11e218d" +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 = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[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 = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[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-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[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", + "wasip2", +] + +[[package]] +name = "h2" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +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.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "bytes", + "http", + "http-body", + "hyper", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[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.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "jiff" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-static" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "libc" +version = "0.2.183" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" + +[[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.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys", +] + +[[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 = "opencodeui-router" +version = "0.1.0" +dependencies = [ + "axum", + "base64", + "env_logger", + "log", + "rand", + "serde", + "serde_json", + "tokio", +] + +[[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", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[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.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "091397be61a01d4be58e7841595bd4bfedb15f1cd54977d79b8271e94ed799a3" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[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 = "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 = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +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", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[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 = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[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_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", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "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 = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys", +] + +[[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" + +[[package]] +name = "tokio" +version = "1.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[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 = "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", + "tracing", +] + +[[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 = [ + "log", + "pin-project-lite", + "tracing-core", +] + +[[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 = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[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.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" + +[[package]] +name = "zerocopy" +version = "0.8.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2578b716f8a7a858b7f02d5bd870c14bf4ddbbcf3a4c05414ba6503640505e3" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e6cc098ea4d3bd6246687de65af3f920c430e236bee1e3bf2e441463f08a02f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/src-router/Cargo.toml b/src-router/Cargo.toml new file mode 100644 index 00000000..f6c4acaf --- /dev/null +++ b/src-router/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "opencodeui-router" +version = "0.1.0" +edition = "2024" + +[dependencies] +axum = { version = "0.8.8", features = ["http2"] } +base64 = "0.22.1" +env_logger = "0.11.8" +log = { version = "0.4.29", features = ["release_max_level_info"] } +rand = "0.9.2" +serde = { version = "1.0.228", features = ["derive"] } +serde_json = "1.0.149" +tokio = { version = "1.50.0", features = [ + "fs", + "process", + "macros", + "net", + "parking_lot", + "rt-multi-thread", + "signal", + "time" +] } + +[profile.release] +strip = true +lto = "thin" +panic = "abort" +codegen-units = 1 diff --git a/src-router/src/caddy.rs b/src-router/src/caddy.rs new file mode 100644 index 00000000..eb47c161 --- /dev/null +++ b/src-router/src/caddy.rs @@ -0,0 +1,71 @@ +use std::{collections::BTreeMap, path::Path}; + +use tokio::fs; + +use crate::{ + config::{Config, PREVIEW_FILE, ROUTES_FILE}, + scanner, + state::RouteInfo, +}; + +pub(crate) async fn write_map( + config: &Config, + routes: &BTreeMap, +) -> Result<(), String> { + ensure_parent_dir(ROUTES_FILE).await?; + + let mut lines = vec!["# Dynamic routes (auto generated by router)".to_string()]; + + for (token, info) in routes { + lines.push(format!("@route_{token} path /p/{token} /p/{token}/*")); + lines.push(format!("handle @route_{token} {{")); + lines.push(format!("\turi strip_prefix /p/{token}")); + lines.push(format!( + "\treverse_proxy {}:{}", + config.target_container(), + info.port + )); + lines.push("}".to_string()); + lines.push(String::new()); + } + + fs::write(ROUTES_FILE, lines.join("\n") + "\n") + .await + .map_err(|err| err.to_string()) +} + +pub(crate) async fn write_preview_conf(config: &Config, port: Option) -> Result<(), String> { + ensure_parent_dir(PREVIEW_FILE).await?; + + let body = match port { + Some(port) => format!( + "# Preview proxy (auto generated by router)\nreverse_proxy {}:{}\n", + config.target_container(), + port + ), + None => concat!( + "# Preview proxy (no port selected)\n", + "respond \"No preview port configured. Set one from the Routes panel.\" 503\n" + ) + .to_string(), + }; + + fs::write(PREVIEW_FILE, body) + .await + .map_err(|err| err.to_string()) +} + +pub(crate) async fn reload_gateway() -> Result<(), String> { + scanner::run_cmd(&["caddy", "reload", "--config", "/etc/caddy/Caddyfile"]).await?; + Ok(()) +} + +async fn ensure_parent_dir(path: &str) -> Result<(), String> { + let Some(parent) = Path::new(path).parent() else { + return Ok(()); + }; + + fs::create_dir_all(parent) + .await + .map_err(|err| err.to_string()) +} diff --git a/src-router/src/config.rs b/src-router/src/config.rs new file mode 100644 index 00000000..560f9277 --- /dev/null +++ b/src-router/src/config.rs @@ -0,0 +1,127 @@ +use std::{collections::HashSet, env}; + +pub(crate) const ROUTES_FILE: &str = "/etc/caddy/routes.conf"; +pub(crate) const PREVIEW_FILE: &str = "/etc/caddy/preview.conf"; + +#[derive(Clone, Debug)] +pub struct Config { + target_container: String, + router_state_file: String, + public_base_url: String, + preview_domain: String, + router_username: String, + router_password: String, + scan_interval: u64, + token_length: usize, + port_range: (u16, u16), + exclude_ports: HashSet, +} + +impl Config { + pub fn from_env() -> Self { + Self { + target_container: env::var("TARGET_CONTAINER") + .unwrap_or_else(|_| "opencode-backend".to_string()), + router_state_file: env::var("ROUTER_STATE_FILE") + .unwrap_or_else(|_| "/data/routes.json".to_string()), + public_base_url: env::var("PUBLIC_BASE_URL") + .map(|value| value.trim_end_matches('/').to_string()) + .unwrap_or_default(), + preview_domain: env::var("PREVIEW_DOMAIN") + .map(|value| value.trim().to_string()) + .unwrap_or_default(), + router_username: env::var("ROUTER_USERNAME").unwrap_or_default(), + router_password: env::var("ROUTER_PASSWORD").unwrap_or_default(), + scan_interval: parse_u64_env("ROUTER_SCAN_INTERVAL", 5), + token_length: parse_usize_env("ROUTER_TOKEN_LENGTH", 12), + port_range: parse_port_range( + env::var("ROUTER_PORT_RANGE") + .ok() + .as_deref() + .unwrap_or("3000-9999"), + ), + exclude_ports: parse_exclude_ports( + env::var("ROUTER_EXCLUDE_PORTS") + .ok() + .as_deref() + .unwrap_or("4096"), + ), + } + } + + pub fn target_container(&self) -> &str { + &self.target_container + } + + pub fn router_state_file(&self) -> &str { + &self.router_state_file + } + + pub fn public_base_url(&self) -> &str { + &self.public_base_url + } + + pub fn preview_domain(&self) -> &str { + &self.preview_domain + } + + pub fn router_username(&self) -> &str { + &self.router_username + } + + pub fn router_password(&self) -> &str { + &self.router_password + } + + pub fn scan_interval(&self) -> u64 { + self.scan_interval + } + + pub fn token_length(&self) -> usize { + self.token_length + } + + pub fn port_range(&self) -> (u16, u16) { + self.port_range + } + + pub fn exclude_ports(&self) -> &HashSet { + &self.exclude_ports + } +} + +fn parse_u64_env(name: &str, default: u64) -> u64 { + env::var(name) + .ok() + .and_then(|value| value.parse::().ok()) + .unwrap_or(default) +} + +fn parse_usize_env(name: &str, default: usize) -> usize { + env::var(name) + .ok() + .and_then(|value| value.parse::().ok()) + .unwrap_or(default) +} + +fn parse_port_range(value: &str) -> (u16, u16) { + let Some((start, end)) = value.split_once('-') else { + return (3000, 9999); + }; + + let mut start = start.trim().parse::().unwrap_or(3000); + let mut end = end.trim().parse::().unwrap_or(9999); + + if start > end { + std::mem::swap(&mut start, &mut end); + } + + (start, end) +} + +fn parse_exclude_ports(value: &str) -> HashSet { + value + .split(',') + .filter_map(|item| item.trim().parse::().ok()) + .collect() +} diff --git a/src-router/src/main.rs b/src-router/src/main.rs new file mode 100644 index 00000000..29a2b01b --- /dev/null +++ b/src-router/src/main.rs @@ -0,0 +1,36 @@ +mod caddy; +mod config; +mod router; +mod scanner; +mod state; + +use std::{net::SocketAddr, sync::Arc}; + +use config::Config; +use router::AppState; + +#[tokio::main] +async fn main() { + env_logger::init(); + + let config = Arc::new(Config::from_env()); + let state = Arc::new(AppState::new(config)); + + state.initialize().await; + state.spawn_sync_loop(); + + let app = router::app(state); + let addr = SocketAddr::from(([0, 0, 0, 0], 7070)); + + log::info!("Router starting on {}", addr); + + let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); + axum::serve(listener, app) + .with_graceful_shutdown(shutdown_signal()) + .await + .unwrap(); +} + +async fn shutdown_signal() { + let _ = tokio::signal::ctrl_c().await; +} diff --git a/src-router/src/router.html b/src-router/src/router.html new file mode 100644 index 00000000..2ee52c56 --- /dev/null +++ b/src-router/src/router.html @@ -0,0 +1,730 @@ + + + + + + Routes - OpenCode + + + +
+
+
+
Routes
+
+ + 0 active +
+
+
+ + + +
+
+ +
+ +
+ + + + + +
+ +
+
+
+ + + + diff --git a/src-router/src/router.rs b/src-router/src/router.rs new file mode 100644 index 00000000..360be795 --- /dev/null +++ b/src-router/src/router.rs @@ -0,0 +1,396 @@ +use std::{ + collections::HashSet, + sync::Arc, + time::{Duration, SystemTime, UNIX_EPOCH}, +}; + +use axum::{ + Json, Router, + extract::{Query, State}, + http::{HeaderMap, HeaderValue, StatusCode, header}, + response::{Html, IntoResponse, Response}, + routing::{get, post}, +}; +use base64::{Engine as _, engine::general_purpose::STANDARD}; +use rand::{Rng, distr::Alphanumeric, rng}; +use serde::{Deserialize, Serialize}; +use serde_json::{Map, Value, json}; +use tokio::{ + sync::RwLock, + time::{MissedTickBehavior, interval}, +}; + +use crate::{ + caddy, + config::Config, + scanner, + state::{self, RouteInfo}, +}; + +const HTML_TEMPLATE: &str = include_str!("router.html"); + +#[derive(Clone)] +pub struct AppState { + config: Arc, + /// In-memory cache of the full state map (routes + preview port). + /// All reads/writes go through this lock; disk is only for persistence. + state_map: Arc>>, + preview_port: Arc>>, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +struct RoutePayloadItem { + token: String, + port: u16, + public_url: String, + created_at: u64, +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +struct RoutesPayload { + routes: Vec, + preview_port: Option, + preview_domain: Option, +} + +#[derive(Debug, Deserialize)] +struct PreviewSetRequest { + port: Option, +} + +#[derive(Debug, Deserialize)] +struct RoutesQuery { + format: Option, +} + +impl AppState { + pub fn new(config: Arc) -> Self { + Self { + config, + state_map: Arc::new(RwLock::new(Map::new())), + preview_port: Arc::new(RwLock::new(None)), + } + } + + pub async fn initialize(&self) { + let state_map = state::load_state_map(self.config.router_state_file()).await; + let preview_port = state::load_preview_port(&state_map); + + { + let mut cached = self.state_map.write().await; + *cached = state_map; + } + { + let mut current = self.preview_port.write().await; + *current = preview_port; + } + + if let Err(err) = caddy::write_preview_conf(&self.config, preview_port).await { + log::error!("Failed to write preview config: {}", err); + } + + if let Some(port) = preview_port { + log::info!("Restored preview port: {}", port); + } + } + + pub fn spawn_sync_loop(self: &Arc) { + let state = Arc::clone(self); + tokio::spawn(async move { + log::info!( + "Route scanner started (interval={}s, target={}, range={}-{})", + state.config.scan_interval(), + state.config.target_container(), + state.config.port_range().0, + state.config.port_range().1, + ); + + let mut ticker = interval(Duration::from_secs(state.config.scan_interval())); + ticker.set_missed_tick_behavior(MissedTickBehavior::Skip); + + loop { + ticker.tick().await; + if let Err(err) = state.sync_routes().await { + log::error!("Error in sync_routes: {}", err); + } + } + }); + } + + async fn sync_routes(&self) -> Result<(), String> { + let ports = match scanner::list_listening_ports(&self.config).await { + Ok(ports) => ports, + Err(err) => { + log::warn!( + "Failed to query ports from {}: {}", + self.config.target_container(), + err + ); + return Ok(()); + } + }; + + let mut state_map = self.state_map.write().await; + let mut routes = state::extract_routes(&state_map); + let existing_ports: HashSet = routes.values().map(|info| info.port).collect(); + let mut changed = false; + + for port in &ports { + if existing_ports.contains(port) { + continue; + } + + let mut token = generate_token(self.config.token_length()); + while state_map.contains_key(&token) { + token = generate_token(self.config.token_length()); + } + + let route = RouteInfo { + port: *port, + created_at: now_ts(), + }; + + routes.insert(token.clone(), route.clone()); + state_map.insert(token.clone(), state::route_to_value(&route)); + log::info!("New route: port {} -> /p/{}", port, token); + changed = true; + } + + let active_ports: HashSet = ports.iter().copied().collect(); + let stale_tokens: Vec = routes + .iter() + .filter(|(_, info)| !active_ports.contains(&info.port)) + .map(|(token, _)| token.clone()) + .collect(); + + for token in stale_tokens { + if let Some(info) = routes.remove(&token) { + state_map.remove(&token); + log::info!("Removed stale route: port {} (token {})", info.port, token); + changed = true; + } + } + + if changed { + caddy::write_map(&self.config, &routes).await?; + state::save_state_map(self.config.router_state_file(), &state_map).await?; + self.reload_gateway().await; + log::info!("Routes updated: {} active", routes.len()); + } + + Ok(()) + } + + async fn save_preview_port(&self, port: Option) -> Result<(), String> { + { + let mut current = self.preview_port.write().await; + *current = port; + } + + let mut state_map = self.state_map.write().await; + state::set_preview_port_value(&mut state_map, port); + state::save_state_map(self.config.router_state_file(), &state_map).await + } + + async fn set_preview_port(&self, port: Option) -> Result<(), String> { + caddy::write_preview_conf(&self.config, port).await?; + self.save_preview_port(port).await?; + self.reload_gateway().await; + log::info!("Preview port set to: {:?}", port); + Ok(()) + } + + async fn reload_gateway(&self) { + if let Err(err) = caddy::reload_gateway().await { + log::warn!("Failed to reload caddy: {}", err); + } + } + + async fn build_routes_payload(&self) -> RoutesPayload { + let state_map = self.state_map.read().await; + let routes = state::extract_routes(&state_map) + .into_iter() + .map(|(token, info)| RoutePayloadItem { + public_url: self.public_url(&token), + token, + port: info.port, + created_at: info.created_at, + }) + .collect(); + + RoutesPayload { + routes, + preview_port: *self.preview_port.read().await, + preview_domain: non_empty(self.config.preview_domain()), + } + } + + fn public_url(&self, token: &str) -> String { + if !self.config.preview_domain().is_empty() { + return format!("https://{}/p/{}/", self.config.preview_domain(), token); + } + + if !self.config.public_base_url().is_empty() { + return format!("{}/p/{}/", self.config.public_base_url(), token); + } + + String::new() + } +} + +pub fn app(state: Arc) -> Router { + Router::new() + .route("/routes", get(get_routes)) + .route("/preview/set", post(post_preview_set)) + .route("/preview/status", get(get_preview_status)) + .with_state(state) +} + +async fn get_routes( + State(state): State>, + Query(query): Query, + headers: HeaderMap, +) -> Response { + if !check_basic_auth(&headers, &state.config) { + return unauthorized_response(); + } + + let payload = state.build_routes_payload().await; + log::info!( + "Serving /routes ({} routes, format={})", + payload.routes.len(), + query.format.as_deref().unwrap_or("html") + ); + + if query.format.as_deref() == Some("json") { + let mut response = Json(payload).into_response(); + set_no_cache_headers(response.headers_mut()); + return response; + } + + let initial_data = serde_json::to_string(&payload).unwrap_or_else(|_| "{}".to_string()); + let html = HTML_TEMPLATE.replace("__INITIAL_DATA__", &initial_data); + let mut response = Html(html).into_response(); + set_no_cache_headers(response.headers_mut()); + response +} + +async fn get_preview_status(State(state): State>, headers: HeaderMap) -> Response { + if !check_basic_auth(&headers, &state.config) { + return unauthorized_response(); + } + + let payload = json!({ + "previewPort": *state.preview_port.read().await, + "previewDomain": non_empty(state.config.preview_domain()), + }); + Json(payload).into_response() +} + +async fn post_preview_set( + State(state): State>, + headers: HeaderMap, + payload: Result, axum::extract::rejection::JsonRejection>, +) -> Response { + if !check_basic_auth(&headers, &state.config) { + return unauthorized_response(); + } + + let Json(request) = match payload { + Ok(payload) => payload, + Err(_) => { + return ( + StatusCode::BAD_REQUEST, + Json(json!({ "error": "invalid port" })), + ) + .into_response(); + } + }; + + match state.set_preview_port(request.port).await { + Ok(()) => Json(json!({ "ok": true, "previewPort": request.port })).into_response(), + Err(err) => { + log::error!("Failed to set preview port: {}", err); + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(json!({ "error": "failed to set preview port" })), + ) + .into_response() + } + } +} + +fn check_basic_auth(headers: &HeaderMap, config: &Config) -> bool { + if config.router_password().is_empty() { + return true; + } + + let Some(auth_header) = headers.get(header::AUTHORIZATION) else { + return false; + }; + + let Ok(auth_header) = auth_header.to_str() else { + return false; + }; + + let lower = auth_header.to_ascii_lowercase(); + if !lower.starts_with("basic ") { + return false; + }; + let raw = &auth_header["basic ".len()..]; + + let Ok(decoded) = STANDARD.decode(raw.trim()) else { + return false; + }; + + let Ok(decoded) = String::from_utf8(decoded) else { + return false; + }; + + let Some((user, password)) = decoded.split_once(':') else { + return false; + }; + + user == config.router_username() && password == config.router_password() +} + +fn unauthorized_response() -> Response { + let mut response = (StatusCode::UNAUTHORIZED, "Unauthorized").into_response(); + response + .headers_mut() + .insert(header::WWW_AUTHENTICATE, HeaderValue::from_static("Basic")); + response +} + +fn set_no_cache_headers(headers: &mut HeaderMap) { + headers.insert( + header::CACHE_CONTROL, + HeaderValue::from_static("no-cache, no-store, must-revalidate"), + ); + headers.insert(header::PRAGMA, HeaderValue::from_static("no-cache")); +} + +fn generate_token(length: usize) -> String { + rng() + .sample_iter(Alphanumeric) + .take(length) + .map(char::from) + .collect() +} + +fn now_ts() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_secs() +} + +fn non_empty(value: &str) -> Option { + if value.is_empty() { + None + } else { + Some(value.to_string()) + } +} diff --git a/src-router/src/scanner.rs b/src-router/src/scanner.rs new file mode 100644 index 00000000..3933b5a1 --- /dev/null +++ b/src-router/src/scanner.rs @@ -0,0 +1,89 @@ +use std::collections::HashSet; + +use tokio::process::Command; + +use crate::config::Config; + +pub async fn list_listening_ports(config: &Config) -> Result, String> { + let output = run_cmd(&[ + "docker", + "exec", + config.target_container(), + "sh", + "-c", + "cat /proc/net/tcp /proc/net/tcp6", + ]) + .await?; + + let ports = parse_proc_net_tcp(&output); + let (start, end) = config.port_range(); + let mut filtered: Vec = ports + .into_iter() + .filter(|port| !config.exclude_ports().contains(port)) + .filter(|port| *port >= start && *port <= end) + .collect(); + filtered.sort_unstable(); + Ok(filtered) +} + +pub(crate) async fn run_cmd(args: &[&str]) -> Result { + let (program, rest) = args + .split_first() + .ok_or_else(|| "empty command".to_string())?; + + let output = Command::new(program) + .args(rest) + .output() + .await + .map_err(|err| err.to_string())?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string(); + log::warn!( + "Command failed (rc={}): {} | stderr: {}", + output.status.code().unwrap_or(-1), + args.join(" "), + stderr, + ); + return Err(stderr); + } + + Ok(String::from_utf8_lossy(&output.stdout).into_owned()) +} + +fn parse_proc_net_tcp(output: &str) -> HashSet { + let mut ports = HashSet::new(); + + for line in output.lines() { + let line = line.trim(); + if line.is_empty() || line.starts_with("sl") { + continue; + } + + let mut parts = line.split_whitespace(); + let _slot = parts.next(); + let Some(local) = parts.next() else { + continue; + }; + let _remote = parts.next(); + let Some(state) = parts.next() else { + continue; + }; + + if state != "0A" { + continue; + } + + let Some((_, port_hex)) = local.rsplit_once(':') else { + continue; + }; + + let Ok(port) = u16::from_str_radix(port_hex, 16) else { + continue; + }; + + ports.insert(port); + } + + ports +} diff --git a/src-router/src/state.rs b/src-router/src/state.rs new file mode 100644 index 00000000..0b2c1a39 --- /dev/null +++ b/src-router/src/state.rs @@ -0,0 +1,85 @@ +use std::{collections::BTreeMap, path::Path}; + +use serde::{Deserialize, Serialize}; +use serde_json::{Map, Value, json}; +use tokio::fs; + +const PREVIEW_STATE_KEY: &str = "__preview_port__"; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub(crate) struct RouteInfo { + pub(crate) port: u16, + pub(crate) created_at: u64, +} + +pub(crate) async fn load_state_map(path: &str) -> Map { + let Ok(content) = fs::read_to_string(path).await else { + return Map::new(); + }; + + serde_json::from_str::>(&content).unwrap_or_default() +} + +pub(crate) async fn save_state_map( + path: &str, + state_map: &Map, +) -> Result<(), String> { + ensure_parent_dir(path).await?; + + let mut sorted = BTreeMap::new(); + for (key, value) in state_map { + sorted.insert(key.clone(), value.clone()); + } + + let body = serde_json::to_string_pretty(&sorted).map_err(|err| err.to_string())?; + fs::write(path, format!("{body}\n")) + .await + .map_err(|err| err.to_string()) +} + +pub(crate) fn extract_routes(state_map: &Map) -> BTreeMap { + state_map + .iter() + .filter(|(key, _)| !key.starts_with("__")) + .filter_map(|(token, value)| { + serde_json::from_value::(value.clone()) + .ok() + .map(|info| (token.clone(), info)) + }) + .collect() +} + +pub(crate) fn route_to_value(route: &RouteInfo) -> Value { + json!({ + "port": route.port, + "created_at": route.created_at, + }) +} + +pub(crate) fn load_preview_port(state_map: &Map) -> Option { + state_map + .get(PREVIEW_STATE_KEY) + .and_then(Value::as_u64) + .and_then(|value| u16::try_from(value).ok()) +} + +pub(crate) fn set_preview_port_value(state_map: &mut Map, port: Option) { + match port { + Some(port) => { + state_map.insert(PREVIEW_STATE_KEY.to_string(), json!(port)); + } + None => { + state_map.remove(PREVIEW_STATE_KEY); + } + } +} + +async fn ensure_parent_dir(path: &str) -> Result<(), String> { + let Some(parent) = Path::new(path).parent() else { + return Ok(()); + }; + + fs::create_dir_all(parent) + .await + .map_err(|err| err.to_string()) +} diff --git a/src-tauri/.gitignore b/src-tauri/.gitignore new file mode 100644 index 00000000..502406b4 --- /dev/null +++ b/src-tauri/.gitignore @@ -0,0 +1,4 @@ +# Generated by Cargo +# will have compiled files and executables +/target/ +/gen/schemas diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock new file mode 100644 index 00000000..9b01cec3 --- /dev/null +++ b/src-tauri/Cargo.lock @@ -0,0 +1,6565 @@ +# 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 = "ahash" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0453232ace82dee0dd0b4c87a59bd90f7b53b314f3e0f61fe2ee7c8a16482289" + +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom 0.2.17", + "once_cell", + "version_check", +] + +[[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_log-sys" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84521a3cf562bc62942e294181d9eef17eb38ceb8c68677bc49f144e4c3d4f8d" + +[[package]] +name = "android_logger" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbb4e440d04be07da1f1bf44fb4495ebd58669372fe0cffa6e48595ac5bd88a3" +dependencies = [ + "android_log-sys", + "env_filter", + "log", +] + +[[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 = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[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.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c" +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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[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 = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +dependencies = [ + "serde_core", +] + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[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 = "borsh" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1da5ab77c1437701eeff7c88d968729e7766172279eab0676857b3d63af7a6f" +dependencies = [ + "borsh-derive", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0686c856aa6aac0c4498f936d7d6a02df690f614c03e4d906d1018062b5c5e2c" +dependencies = [ + "once_cell", + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "brotli" +version = "8.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "byte-unit" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c6d47a4e2961fb8721bcfc54feae6455f2f64e7054f9bc67e875f0e77f4c58d" +dependencies = [ + "rust_decimal", + "schemars 1.2.1", + "serde", + "utf8-width", +] + +[[package]] +name = "bytecheck" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[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.0", + "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.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +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.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link 0.2.1", +] + +[[package]] +name = "cocoa" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6140449f97a6e97f9511815c5632d84c8aacf8ac271ad77c559218161a1373c" +dependencies = [ + "bitflags 1.3.2", + "block", + "cocoa-foundation", + "core-foundation 0.9.4", + "core-graphics 0.23.2", + "foreign-types", + "libc", + "objc", +] + +[[package]] +name = "cocoa-foundation" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7" +dependencies = [ + "bitflags 1.3.2", + "block", + "core-foundation 0.9.4", + "core-graphics-types 0.1.3", + "libc", + "objc", +] + +[[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 = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] +name = "cookie_store" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eac901828f88a5241ee0600950ab981148a18f2f756900ffba1b125ca6a3ef9" +dependencies = [ + "cookie", + "document-features", + "idna", + "log", + "publicsuffix", + "serde", + "serde_derive", + "serde_json", + "time", + "url", +] + +[[package]] +name = "cookie_store" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fc4bff745c9b4c7fb1e97b25d13153da2bc7796260141df62378998d070207f" +dependencies = [ + "cookie", + "document-features", + "idna", + "log", + "publicsuffix", + "serde", + "serde_derive", + "serde_json", + "time", + "url", +] + +[[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.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "core-graphics-types 0.1.3", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" +dependencies = [ + "bitflags 2.11.0", + "core-foundation 0.10.1", + "core-graphics-types 0.2.0", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "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.0", + "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-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.29.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93d03419cb5950ccfd3daf3ff1c7a36ace64609a1a8746d493df1ca0afde0fa" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "matches", + "phf 0.10.1", + "proc-macro2", + "quote", + "smallvec", + "syn 1.0.109", +] + +[[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.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.117", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "data-encoding" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" + +[[package]] +name = "data-url" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376" + +[[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 = "0.99.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" +dependencies = [ + "convert_case", + "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 = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[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", + "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.0", + "block2", + "libc", + "objc2", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +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 = "dlv-list" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68df3f2b690c1b86e65ef7830956aededf3cb0a16f898f79b9a6f421a7b6211b" +dependencies = [ + "rand 0.8.5", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[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 = "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.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55a075fc573c64510038d7ee9abc7990635863992f83ebc52c8b433b8411a02e" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 0.9.12+spec-1.1.0", + "vswhom", + "winreg", +] + +[[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 = "enigo" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "802e4b2ae123615659085369b453cba87c5562e46ed8050a909fee18a9bc3157" +dependencies = [ + "core-graphics 0.23.2", + "libc", + "objc", + "pkg-config", + "windows 0.51.1", +] + +[[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 = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bf3c259d255ca70051b30e2e95b5446cdb8949ac4cd22c0d7fd634d89f568e2" +dependencies = [ + "log", + "regex", +] + +[[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 = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "fern" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4316185f709b23713e41e3195f90edef7fb00c3ed4adc79769cf09cc762a3b29" +dependencies = [ + "log", +] + +[[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 = "file-locker" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75ae8b5984a4863d8a32109a848d038bd6d914f20f010cc141375f7a183c41cf" +dependencies = [ + "nix", +] + +[[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 = "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", +] + +[[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.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 = "freedesktop_entry_parser" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db9c27b72f19a99a895f8ca89e2d26e4ef31013376e56fdafef697627306c3e4" +dependencies = [ + "nom", + "thiserror 1.0.69", +] + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", +] + +[[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-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[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.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 5.3.0", + "wasip2", + "wasm-bindgen", +] + +[[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 = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags 2.11.0", + "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 = "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.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap 2.13.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" +dependencies = [ + "ahash 0.4.8", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash 0.7.8", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[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.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b7410cae13cbc75623c98ac4cbfd1f0bedddf3227afc24f370cf0f50a44a11c" +dependencies = [ + "log", + "mac", + "markup5ever", + "match_token", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +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 = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots 1.0.6", +] + +[[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", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[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.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.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 = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "iri-string" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" +dependencies = [ + "memchr", + "serde", +] + +[[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 = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[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 = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "js-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +dependencies = [ + "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.0", + "serde", + "unicode-segmentation", +] + +[[package]] +name = "kuchikiki" +version = "0.8.8-speedreader" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02cb977175687f33fa4afa0c95c112b987ea1443e5a51c8f8ff27dc618270cc2" +dependencies = [ + "cssparser", + "html5ever", + "indexmap 2.13.0", + "selectors", +] + +[[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.183" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" + +[[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.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" +dependencies = [ + "libc", +] + +[[package]] +name = "linicon" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ee8c5653188a809616c97296180a0547a61dba205bcdcbdd261dbd022a25fd9" +dependencies = [ + "file-locker", + "freedesktop_entry_parser", + "linicon-theme", + "memmap2", + "thiserror 1.0.69", +] + +[[package]] +name = "linicon-theme" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4f8240c33bb08c5d8b8cdea87b683b05e61037aa76ff26bef40672cc6ecbb80" +dependencies = [ + "freedesktop_entry_parser", + "rust-ini", +] + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + +[[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.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +dependencies = [ + "value-bag", +] + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "mac-notification-sys" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c889829df2867fd6a043c5932c641fcf7fe9d4329317326af08df14747ab9a97" +dependencies = [ + "cc", + "objc2", + "objc2-foundation", + "time", +] + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "markup5ever" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a7213d12e1864c0f002f52c2923d4556935a43dec5e71355c2760e0f6e7a18" +dependencies = [ + "log", + "phf 0.11.3", + "phf_codegen 0.11.3", + "string_cache", + "string_cache_codegen", + "tendril", +] + +[[package]] +name = "match_token" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "matches" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "memmap2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83faa42c0a078c393f6b29d5db232d8be22776a891f8f56e5284faee4a20b327" +dependencies = [ + "libc", +] + +[[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 = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[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 = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.61.2", +] + +[[package]] +name = "muda" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01c1738382f66ed56b3b9c8119e794a2e23148ac8ea214eda86622d4cb9d415a" +dependencies = [ + "crossbeam-channel", + "dpi", + "gtk", + "keyboard-types", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "once_cell", + "png", + "serde", + "thiserror 2.0.18", + "windows-sys 0.60.2", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.11.0", + "jni-sys", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys", +] + +[[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.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.11.0", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "nodrop" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "notify-rust" +version = "4.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21af20a1b50be5ac5861f74af1a863da53a11c38684d9818d82f1c42f7fdc6c2" +dependencies = [ + "futures-lite", + "log", + "mac-notification-sys", + "serde", + "tauri-winrt-notification", + "zbus", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[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_enum" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + +[[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.0", + "block2", + "objc2", + "objc2-core-foundation", + "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.0", + "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.0", + "dispatch2", + "objc2", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[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.0", + "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.0", + "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.0", + "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.0", + "objc2", + "objc2-core-foundation", + "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.0", + "block2", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "open" +version = "5.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43bb73a7fa3799b198970490a51174027ba0d4ec504b03cd08caf513d40024bc" +dependencies = [ + "dunce", + "is-wsl", + "libc", + "pathdiff", +] + +[[package]] +name = "opencodeui" +version = "0.6.23" +dependencies = [ + "futures-util", + "log", + "papaya", + "rapidhash", + "reqwest 0.12.28", + "serde", + "serde_json", + "tauri", + "tauri-build", + "tauri-plugin-decorum", + "tauri-plugin-dialog", + "tauri-plugin-fs", + "tauri-plugin-http", + "tauri-plugin-log", + "tauri-plugin-notification", + "tauri-plugin-opener", + "tauri-plugin-single-instance", + "tokio", + "tokio-tungstenite", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "ordered-multimap" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c672c7ad9ec066e428c00eb917124a06f08db19e2584de982cc34b1f4c12485" +dependencies = [ + "dlv-list", + "hashbrown 0.9.1", +] + +[[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 = "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 = "papaya" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f92dd0b07c53a0a0c764db2ace8c541dc47320dad97c2200c2a637ab9dd2328f" +dependencies = [ + "equivalent", + "seize", +] + +[[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.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" +dependencies = [ + "phf_shared 0.8.0", +] + +[[package]] +name = "phf" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" +dependencies = [ + "phf_macros 0.10.0", + "phf_shared 0.10.0", + "proc-macro-hack", +] + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros 0.11.3", + "phf_shared 0.11.3", +] + +[[package]] +name = "phf_codegen" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815" +dependencies = [ + "phf_generator 0.8.0", + "phf_shared 0.8.0", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", +] + +[[package]] +name = "phf_generator" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" +dependencies = [ + "phf_shared 0.8.0", + "rand 0.7.3", +] + +[[package]] +name = "phf_generator" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" +dependencies = [ + "phf_shared 0.10.0", + "rand 0.8.5", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared 0.11.3", + "rand 0.8.5", +] + +[[package]] +name = "phf_macros" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58fdf3184dd560f160dd73922bea2d5cd6e8f064bf4b13110abd81b03697b4e0" +dependencies = [ + "phf_generator 0.10.0", + "phf_shared 0.10.0", + "proc-macro-hack", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "phf_shared" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" +dependencies = [ + "siphasher 0.3.11", +] + +[[package]] +name = "phf_shared" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +dependencies = [ + "siphasher 0.3.11", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher 1.0.2", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[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.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "plist" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" +dependencies = [ + "base64 0.22.1", + "indexmap 2.13.0", + "quick-xml 0.38.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 = "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 = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +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.4+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-macro-hack" +version = "0.5.20+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" + +[[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 = "psl-types" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" + +[[package]] +name = "ptr_meta" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "publicsuffix" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42ea446cab60335f76979ec15e12619a2165b5ae2c12166bef27d283a9fadf" +dependencies = [ + "idna", + "psl-types", +] + +[[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.38.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" +dependencies = [ + "memchr", +] + +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.18", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.2", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.60.2", +] + +[[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 = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", + "rand_pcg", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[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 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[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 = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rand_pcg" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rapidhash" +version = "4.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e48930979c155e2f33aa36ab3119b5ee81332beb6482199a8ecd6029b80b59" +dependencies = [ + "rustversion", +] + +[[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.0", +] + +[[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 = "rend" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" +dependencies = [ + "bytecheck", +] + +[[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", + "cookie", + "cookie_store 0.22.0", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "mime", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams 0.4.2", + "web-sys", + "webpki-roots 1.0.6", +] + +[[package]] +name = "reqwest" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" +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 = "rkyv" +version = "0.7.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2297bf9c81a3f0dc96bc9521370b88f054168c29826a75e89c55ff196e7ed6a1" +dependencies = [ + "bitvec", + "bytecheck", + "bytes", + "hashbrown 0.12.3", + "ptr_meta", + "rend", + "rkyv_derive", + "seahash", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.7.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d7b42d4b8d06048d3ac8db0eb31bcb942cbeb709f0b5f2b2ebde398d3038f5" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "rust-ini" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63471c4aa97a1cf8332a5f97709a79a4234698de6a1f5087faf66f2dae810e22" +dependencies = [ + "cfg-if", + "ordered-multimap", +] + +[[package]] +name = "rust_decimal" +version = "1.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61f703d19852dbf87cbc513643fa81428361eb6940f1ac14fd58155d295a3eb0" +dependencies = [ + "arrayvec", + "borsh", + "bytes", + "num-traits", + "rand 0.8.5", + "rkyv", + "serde", + "serde_json", +] + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[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.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +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 = "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 = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + +[[package]] +name = "seize" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b55fb86dfd3a2f5f76ea78310a88f96c4ea21a3031f8d212443d56123fd0521" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "selectors" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c37578180969d00692904465fb7f6b3d50b9a2b952b87c23d0e2e5cb5013416" +dependencies = [ + "bitflags 1.3.2", + "cssparser", + "derive_more", + "fxhash", + "log", + "phf 0.8.0", + "phf_codegen 0.8.0", + "precomputed-hash", + "servo_arc", + "smallvec", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +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.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +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.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" +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.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "381b283ce7bc6b476d903296fb59d0d36633652b633b27f64db4fb46dcbfc3b9" +dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.13.0", + "schemars 0.9.0", + "schemars 1.2.1", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6d4e30573c8cb306ed6ab1dca8423eec9a463ea0e155f45399455e0368b27e0" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[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.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52aa42f8fdf0fed91e5ce7f23d8138441002fa31dca008acf47e6fd4721f741" +dependencies = [ + "nodrop", + "stable_deref_trait", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[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 = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[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.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "siphasher" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" + +[[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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +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.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared 0.11.3", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", + "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 = "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", + "quote", + "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.0", + "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.34.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d52c379e63da659a483a958110bbde891695a0ecb53e48cc7786d5eda7bb" +dependencies = [ + "bitflags 2.11.0", + "block2", + "core-foundation 0.10.1", + "core-graphics 0.25.0", + "crossbeam-channel", + "dispatch2", + "dlopen2", + "dpi", + "gdkwayland-sys", + "gdkx11-sys", + "gtk", + "jni", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "once_cell", + "parking_lot", + "raw-window-handle", + "tao-macros", + "unicode-segmentation", + "url", + "windows 0.61.3", + "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 = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[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.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da77cc00fb9028caf5b5d4650f75e31f1ef3693459dfca7f7e506d1ecef0ba2d" +dependencies = [ + "anyhow", + "bytes", + "cookie", + "dirs", + "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.2", + "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 0.61.3", +] + +[[package]] +name = "tauri-build" +version = "2.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bbc990d1dbf57a8e1c7fa2327f2a614d8b757805603c1b9ba5c81bade09fd4d" +dependencies = [ + "anyhow", + "cargo_toml", + "dirs", + "glob", + "heck 0.5.0", + "json-patch", + "schemars 0.8.22", + "semver", + "serde", + "serde_json", + "tauri-utils", + "tauri-winres", + "toml 0.9.12+spec-1.1.0", + "walkdir", +] + +[[package]] +name = "tauri-codegen" +version = "2.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a24476afd977c5d5d169f72425868613d82747916dd29e0a357c84c4bd6d29" +dependencies = [ + "base64 0.22.1", + "brotli", + "ico", + "json-patch", + "plist", + "png", + "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.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d39b349a98dadaffebb73f0a40dcd1f23c999211e5a2e744403db384d0c33de7" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", + "tauri-codegen", + "tauri-utils", +] + +[[package]] +name = "tauri-plugin" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddde7d51c907b940fb573006cdda9a642d6a7c8153657e88f8a5c3c9290cd4aa" +dependencies = [ + "anyhow", + "glob", + "plist", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri-utils", + "toml 0.9.12+spec-1.1.0", + "walkdir", +] + +[[package]] +name = "tauri-plugin-decorum" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db925c61a04a937028bc91ad8ae64a93b84a1715b964530925a54e793d494999" +dependencies = [ + "anyhow", + "cocoa", + "enigo", + "linicon", + "objc", + "rand 0.8.5", + "serde", + "tauri", + "tauri-plugin", +] + +[[package]] +name = "tauri-plugin-dialog" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9204b425d9be8d12aa60c2a83a289cf7d1caae40f57f336ed1155b3a5c0e359b" +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.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed390cc669f937afeb8b28032ce837bac8ea023d975a2e207375ec05afaf1804" +dependencies = [ + "anyhow", + "dunce", + "glob", + "percent-encoding", + "schemars 0.8.22", + "serde", + "serde_json", + "serde_repr", + "tauri", + "tauri-plugin", + "tauri-utils", + "thiserror 2.0.18", + "toml 0.9.12+spec-1.1.0", + "url", +] + +[[package]] +name = "tauri-plugin-http" +version = "2.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8f069451c4e87e7e2636b7f065a4c52866c4ce5e60e2d53fa1038edb6d184dc" +dependencies = [ + "bytes", + "cookie_store 0.21.1", + "data-url", + "http", + "regex", + "reqwest 0.12.28", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "tauri-plugin-fs", + "thiserror 2.0.18", + "tokio", + "url", + "urlpattern", +] + +[[package]] +name = "tauri-plugin-log" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7545bd67f070a4500432c826e2e0682146a1d6712aee22a2786490156b574d93" +dependencies = [ + "android_logger", + "byte-unit", + "fern", + "log", + "objc2", + "objc2-foundation", + "serde", + "serde_json", + "serde_repr", + "swift-rs", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", + "time", +] + +[[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 0.9.2", + "serde", + "serde_json", + "serde_repr", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", + "time", + "url", +] + +[[package]] +name = "tauri-plugin-opener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc624469b06f59f5a29f874bbc61a2ed737c0f9c23ef09855a292c389c42e83f" +dependencies = [ + "dunce", + "glob", + "objc2-app-kit", + "objc2-foundation", + "open", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", + "url", + "windows 0.61.3", + "zbus", +] + +[[package]] +name = "tauri-plugin-single-instance" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc61e4822b8f74d68278e09161d3e3fdd1b14b9eb781e24edccaabf10c420e8c" +dependencies = [ + "serde", + "serde_json", + "tauri", + "thiserror 2.0.18", + "tracing", + "windows-sys 0.60.2", + "zbus", +] + +[[package]] +name = "tauri-runtime" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2826d79a3297ed08cd6ea7f412644ef58e32969504bc4fbd8d7dbeabc4445ea2" +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 0.61.3", +] + +[[package]] +name = "tauri-runtime-wry" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e11ea2e6f801d275fdd890d6c9603736012742a1c33b96d0db788c9cdebf7f9e" +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 0.61.3", + "wry", +] + +[[package]] +name = "tauri-utils" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219a1f983a2af3653f75b5747f76733b0da7ff03069c7a41901a5eb3ace4557d" +dependencies = [ + "anyhow", + "brotli", + "cargo_metadata", + "ctor", + "dunce", + "glob", + "html5ever", + "http", + "infer", + "json-patch", + "kuchikiki", + "log", + "memchr", + "phf 0.11.3", + "proc-macro2", + "quote", + "regex", + "schemars 0.8.22", + "semver", + "serde", + "serde-untagged", + "serde_json", + "serde_with", + "swift-rs", + "thiserror 2.0.18", + "toml 0.9.12+spec-1.1.0", + "url", + "urlpattern", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-winres" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1087b111fe2b005e42dbdc1990fc18593234238d47453b0c99b7de1c9ab2c1e0" +dependencies = [ + "dunce", + "embed-resource", + "toml 0.9.12+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 0.61.3", + "windows-version", +] + +[[package]] +name = "tempfile" +version = "3.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "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.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9e442fc33d7fdb45aa9bfeb312c095964abdf596f7567261062b2a7107aaabd" +dependencies = [ + "deranged", + "itoa", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b36ee98fd31ec7426d599183e8fe26932a8dc1fb76ddb6214d05493377d34ca" + +[[package]] +name = "time-macros" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e552d1249bf61ac2a52db88179fd0673def1e1ad8243a00d9ec9ed71fee3dd" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +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.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[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-tungstenite" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edc5f74e248dc973e0dbb7b74c7e0d6fcc301c694ff50049504004ef4d0cdcd9" +dependencies = [ + "futures-util", + "log", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tungstenite", + "webpki-roots 0.26.11", +] + +[[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.13.0", + "serde_core", + "serde_spanned 1.0.4", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + +[[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.0.0+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e" +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.13.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.13.0", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.25.4+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7193cbd0ce53dc966037f54351dbbcf0d5a642c7f0038c382ef9e677ce8c13f2" +dependencies = [ + "indexmap 2.13.0", + "toml_datetime 1.0.0+spec-1.1.0", + "toml_parser", + "winnow 0.7.15", +] + +[[package]] +name = "toml_parser" +version = "1.0.9+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" +dependencies = [ + "winnow 0.7.15", +] + +[[package]] +name = "toml_writer" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" + +[[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.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags 2.11.0", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[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.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e85aa143ceb072062fc4d6356c1b520a51d636e7bc8e77ec94be3608e5e80c" +dependencies = [ + "crossbeam-channel", + "dirs", + "libappindicator", + "muda", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "once_cell", + "png", + "serde", + "thiserror 2.0.18", + "windows-sys 0.60.2", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "tungstenite" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18e5b8366ee7a95b16d32197d0b2604b43a0be89dc5fac9f8e96ccafbaedda8a" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand 0.8.5", + "rustls", + "rustls-pki-types", + "sha1", + "thiserror 1.0.69", + "utf-8", +] + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "uds_windows" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51b70b87d15e91f553711b40df3048faf27a7a04e01e0ddc0cf9309f0af7c2ca" +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 = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[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-width" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1292c0d970b54115d14f2492fe0170adf21d68a1de108eebc51c1df4f346a091" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" +dependencies = [ + "getrandom 0.4.2", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "value-bag" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ba6f5989077681266825251a52748b8c1d8a4ad098cc37e440103d0ea717fc0" + +[[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.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[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.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[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", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" +dependencies = [ + "cfg-if", + "futures-util", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.117", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +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.13.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.0", + "hashbrown 0.15.5", + "indexmap 2.13.0", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[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 = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.6", +] + +[[package]] +name = "webpki-roots" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" +dependencies = [ + "rustls-pki-types", +] + +[[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 0.61.3", + "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 0.61.3", + "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.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca229916c5ee38c2f2bc1e9d8f04df975b4bd93f9955dc69fabb5d91270045c9" +dependencies = [ + "windows-core 0.51.1", + "windows-targets 0.48.5", +] + +[[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.51.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64" +dependencies = [ + "windows-targets 0.48.5", +] + +[[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.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" +dependencies = [ + "memchr", +] + +[[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 = "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-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.13.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.0", + "indexmap 2.13.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.13.0", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "wry" +version = "0.54.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb26159b420aa77684589a744ae9a9461a95395b848764ad12290a14d960a11a" +dependencies = [ + "base64 0.22.1", + "block2", + "cookie", + "crossbeam-channel", + "dirs", + "dpi", + "dunce", + "gdkx11", + "gtk", + "html5ever", + "http", + "javascriptcore-rs", + "jni", + "kuchikiki", + "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 0.61.3", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zbus" +version = "5.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca82f95dbd3943a40a53cfded6c2d0a2ca26192011846a1810c4256ef92c60bc" +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 0.7.15", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897e79616e84aac4b2c46e9132a4f63b93105d54fe8c0e8f6bffc21fa8d49222" +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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffd8af6d5b78619bab301ff3c560a5bd22426150253db278f164d6cf3b72c50f" +dependencies = [ + "serde", + "winnow 0.7.15", + "zvariant", +] + +[[package]] +name = "zerocopy" +version = "0.8.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96e13bc581734df6250836c59a5f44f3c57db9f9acb9dc8e3eaabdaf6170254d" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3545ea9e86d12ab9bba9fcd99b54c1556fd3199007def5a03c375623d05fac1c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +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.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5708299b21903bbe348e94729f22c49c55d04720a004aa350f1f9c122fd2540b" +dependencies = [ + "endi", + "enumflags2", + "serde", + "winnow 0.7.15", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b59b012ebe9c46656f9cc08d8da8b4c726510aef12559da3e5f1bf72780752c" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75c23a64ef8f40f13a6989991e643554d9bef1d682a281160cf0c1bc389c5e9" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 2.0.117", + "winnow 0.7.15", +] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml new file mode 100644 index 00000000..80c07abb --- /dev/null +++ b/src-tauri/Cargo.toml @@ -0,0 +1,46 @@ +[package] +name = "opencodeui" +version = "0.6.23" +description = "OpenCode UI - Tauri Mobile App" +authors = ["lehhair"] +license = "GPL-3.0-only" +repository = "" +edition = "2021" +rust-version = "1.85.0" + +[lib] +crate-type = ["cdylib", "rlib", "staticlib"] +name = "app_lib" + +[dependencies] +futures-util = "0.3" +log = "0.4" +papaya = "0.2.3" +rapidhash = { version = "4.4.1", features = ["unsafe"] } +reqwest = { version = "0.12", default-features = false, features = [ + "rustls-tls", + "stream" +] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +tauri = { version = "2", features = ["devtools"] } +tauri-plugin-decorum = "1.1.1" +tauri-plugin-dialog = "2" +tauri-plugin-fs = "2" +tauri-plugin-http = "2" +tauri-plugin-log = "2" +tauri-plugin-notification = "2" +tauri-plugin-opener = "2" +tauri-plugin-single-instance = "2" +tokio = { version = "1", features = ["macros", "rt-multi-thread", "sync", "time"] } +tokio-tungstenite = { version = "0.24", features = ["rustls-tls-webpki-roots"] } + +[build-dependencies] +tauri-build = { version = "2", features = [] } + +[profile.release] +strip = true +lto = true +panic = "abort" +codegen-units = 1 +opt-level = "s" diff --git a/src-tauri/app-icon.manifest.json b/src-tauri/app-icon.manifest.json new file mode 100644 index 00000000..2dc478bb --- /dev/null +++ b/src-tauri/app-icon.manifest.json @@ -0,0 +1,6 @@ +{ + "default": "../assets/app-icons/app-icon.svg", + "android_bg": "../assets/app-icons/app-icon-android-background.svg", + "android_fg": "../assets/app-icons/app-icon-android-foreground.svg", + "android_fg_scale": 66 +} diff --git a/src-tauri/build.rs b/src-tauri/build.rs new file mode 100644 index 00000000..795b9b7c --- /dev/null +++ b/src-tauri/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build() +} diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json new file mode 100644 index 00000000..1d4f8dc1 --- /dev/null +++ b/src-tauri/capabilities/default.json @@ -0,0 +1,42 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "default", + "description": "Default capabilities for OpenCode UI", + "windows": ["main", "win-*"], + "permissions": [ + "core:default", + "core:window:allow-start-dragging", + "core:window:allow-set-theme", + "core:window:allow-set-focus", + "core:window:allow-close", + "core:window:allow-center", + "core:window:allow-minimize", + "core:window:allow-maximize", + "core:window:allow-set-size", + "core:window:allow-is-maximized", + "core:window:allow-toggle-maximize", + "decorum:allow-show-snap-overlay", + { + "identifier": "http:default", + "allow": [ + { "url": "http://**" }, + { "url": "https://**" }, + { "url": "http://**:*" }, + { "url": "https://**:*" }, + { "url": "http://**:*/**" }, + { "url": "https://**:*/**" } + ] + }, + "notification:default", + "notification:allow-is-permission-granted", + "notification:allow-request-permission", + "notification:allow-notify", + "dialog:default", + "dialog:allow-open", + "dialog:allow-save", + "fs:default", + "fs:allow-read-file", + "fs:allow-write-file", + "opener:default" + ] +} diff --git a/src-tauri/gen/android/.editorconfig b/src-tauri/gen/android/.editorconfig new file mode 100644 index 00000000..ebe51d3b --- /dev/null +++ b/src-tauri/gen/android/.editorconfig @@ -0,0 +1,12 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = false +insert_final_newline = false \ No newline at end of file diff --git a/src-tauri/gen/android/.gitignore b/src-tauri/gen/android/.gitignore new file mode 100644 index 00000000..b2482031 --- /dev/null +++ b/src-tauri/gen/android/.gitignore @@ -0,0 +1,19 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +build +/captures +.externalNativeBuild +.cxx +local.properties +key.properties + +/.tauri +/tauri.settings.gradle \ No newline at end of file diff --git a/src-tauri/gen/android/app/.gitignore b/src-tauri/gen/android/app/.gitignore new file mode 100644 index 00000000..6bc01217 --- /dev/null +++ b/src-tauri/gen/android/app/.gitignore @@ -0,0 +1,6 @@ +/src/main/java/com/opencodeui/app/generated +/src/main/jniLibs/**/*.so +/src/main/assets/tauri.conf.json +/tauri.build.gradle.kts +/proguard-tauri.pro +/tauri.properties \ No newline at end of file diff --git a/src-tauri/gen/android/app/build.gradle.kts b/src-tauri/gen/android/app/build.gradle.kts new file mode 100644 index 00000000..e5bc30b9 --- /dev/null +++ b/src-tauri/gen/android/app/build.gradle.kts @@ -0,0 +1,95 @@ +import java.util.Properties + +plugins { + id("com.android.application") + id("org.jetbrains.kotlin.android") + id("rust") +} + +val tauriProperties = Properties().apply { + val propFile = file("tauri.properties") + if (propFile.exists()) { + propFile.inputStream().use { load(it) } + } +} + +// 读取签名配置(CI 环境通过 keystore.properties 注入) +val keystoreProperties = Properties().apply { + val propFile = file("keystore.properties") + if (propFile.exists()) { + propFile.inputStream().use { load(it) } + } +} + +android { + compileSdk = 36 + namespace = "com.opencodeui.app" + defaultConfig { + manifestPlaceholders["usesCleartextTraffic"] = "false" + applicationId = "com.opencodeui.app" + minSdk = 24 + targetSdk = 36 + versionCode = tauriProperties.getProperty("tauri.android.versionCode", "1").toInt() + versionName = tauriProperties.getProperty("tauri.android.versionName", "1.0") + } + signingConfigs { + create("release") { + val ksFile = keystoreProperties.getProperty("storeFile") + if (ksFile != null) { + storeFile = file(ksFile) + storePassword = keystoreProperties.getProperty("password") + keyAlias = keystoreProperties.getProperty("keyAlias") + keyPassword = keystoreProperties.getProperty("password") + } + } + } + buildTypes { + getByName("debug") { + manifestPlaceholders["usesCleartextTraffic"] = "true" + isDebuggable = true + isJniDebuggable = true + isMinifyEnabled = false + packaging { + jniLibs.keepDebugSymbols.add("*/arm64-v8a/*.so") + jniLibs.keepDebugSymbols.add("*/armeabi-v7a/*.so") + jniLibs.keepDebugSymbols.add("*/x86/*.so") + jniLibs.keepDebugSymbols.add("*/x86_64/*.so") + } + } + getByName("release") { + manifestPlaceholders["usesCleartextTraffic"] = "true" + isMinifyEnabled = true + val ksFile = keystoreProperties.getProperty("storeFile") + if (ksFile != null) { + signingConfig = signingConfigs.getByName("release") + } + proguardFiles( + *fileTree(".") { include("**/*.pro") } + .plus(getDefaultProguardFile("proguard-android-optimize.txt")) + .toList().toTypedArray() + ) + } + } + kotlinOptions { + jvmTarget = "1.8" + } + buildFeatures { + buildConfig = true + } +} + +rust { + rootDirRel = "../../../" +} + +dependencies { + implementation("androidx.webkit:webkit:1.14.0") + implementation("androidx.appcompat:appcompat:1.7.1") + implementation("androidx.activity:activity-ktx:1.10.1") + implementation("com.google.android.material:material:1.12.0") + testImplementation("junit:junit:4.13.2") + androidTestImplementation("androidx.test.ext:junit:1.1.4") + androidTestImplementation("androidx.test.espresso:espresso-core:3.5.0") +} + +apply(from = "tauri.build.gradle.kts") \ No newline at end of file diff --git a/src-tauri/gen/android/app/proguard-rules.pro b/src-tauri/gen/android/app/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/src-tauri/gen/android/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/src-tauri/gen/android/app/src/main/AndroidManifest.xml b/src-tauri/gen/android/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..2d56b220 --- /dev/null +++ b/src-tauri/gen/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src-tauri/gen/android/app/src/main/java/com/opencodeui/app/MainActivity.kt b/src-tauri/gen/android/app/src/main/java/com/opencodeui/app/MainActivity.kt new file mode 100644 index 00000000..db475c3a --- /dev/null +++ b/src-tauri/gen/android/app/src/main/java/com/opencodeui/app/MainActivity.kt @@ -0,0 +1,309 @@ +package com.opencodeui.app + +import android.content.Context +import android.graphics.Color +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.os.Build +import android.view.View +import android.view.ViewGroup +import android.webkit.WebView +import android.os.VibrationEffect +import android.os.Vibrator +import android.os.VibratorManager +import androidx.activity.enableEdgeToEdge +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.WindowInsetsControllerCompat + +class MainActivity : TauriActivity() { + + private val handler = Handler(Looper.getMainLooper()) + private var cachedInsetsJs: String? = null + private var themeSyncRunnable: Runnable? = null + private var cachedWebView: WebView? = null + + override fun onCreate(savedInstanceState: Bundle?) { + enableEdgeToEdge() + super.onCreate(savedInstanceState) + + // 初始状态栏样式(后续由 WebView 主题同步驱动) + val controller = WindowInsetsControllerCompat(window, window.decorView) + controller.isAppearanceLightStatusBars = true + controller.isAppearanceLightNavigationBars = true + window.statusBarColor = Color.TRANSPARENT + window.navigationBarColor = Color.TRANSPARENT + + // 禁用系统对比度强制(避免状态栏自动加黑/渐变) + if (Build.VERSION.SDK_INT >= 29) { + window.isStatusBarContrastEnforced = false + window.isNavigationBarContrastEnforced = false + } + + // 监听 WindowInsets 变化: + // 1. 顶部交给 Web surface 自己绘制,状态栏透明叠在 WebView 上 + // 2. 底部仍由原生 padding 处理,让键盘弹出时 WebView 物理 resize + val contentView = findViewById(android.R.id.content) + ViewCompat.setOnApplyWindowInsetsListener(contentView) { view, windowInsets -> + val systemInsets = windowInsets.getInsets( + WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout() + ) + val imeInsets = windowInsets.getInsets(WindowInsetsCompat.Type.ime()) + val statusBarInsets = windowInsets.getInsets(WindowInsetsCompat.Type.statusBars()) + val density = resources.displayMetrics.density + val topInsetCssPx = maxOf(statusBarInsets.top, systemInsets.top) / density + + // 键盘弹出时底部取 IME 和系统栏的较大值,让 WebView 整体 resize + val bottomPadding = maxOf(imeInsets.bottom, systemInsets.bottom) + view.setPadding( + systemInsets.left, + 0, + systemInsets.right, + bottomPadding + ) + + cachedInsetsJs = """ + (function() { + var s = document.documentElement.style; + s.setProperty('--safe-area-inset-top', '${topInsetCssPx}px'); + s.setProperty('--safe-area-inset-bottom', '0px'); + s.setProperty('--safe-area-inset-left', '0px'); + s.setProperty('--safe-area-inset-right', '0px'); + })(); + """.trimIndent() + + // 立即尝试注入 + tryInjectInsets(view) + + WindowInsetsCompat.CONSUMED + } + + // WebView 可能还没创建好,轮询几次确保注入成功 + scheduleInsetsInjection(contentView, 0) + } + + override fun onResume() { + super.onResume() + startThemeSync() + } + + override fun onPause() { + stopThemeSync() + super.onPause() + } + + private fun startThemeSync() { + if (themeSyncRunnable != null) return + themeSyncRunnable = Runnable { + val rootView = window.decorView.findViewById(android.R.id.content) + syncSystemBars(rootView) + handler.postDelayed(themeSyncRunnable!!, 800L) + } + handler.post(themeSyncRunnable!!) + } + + private fun stopThemeSync() { + themeSyncRunnable?.let { handler.removeCallbacks(it) } + themeSyncRunnable = null + } + + private fun syncSystemBars(rootView: View) { + val webView = cachedWebView ?: findWebView(rootView) ?: return + // 在 JS 端用 Canvas 2D 将 getComputedStyle 返回的任意格式颜色 + // 统一转为 #rrggbb hex,避免不同 WebView 版本返回不同格式 + // (rgb 逗号/空格分隔, color(srgb ...), oklch(...) 等) + val js = """ + (function() { + var mode = document.documentElement.getAttribute('data-mode') || 'system'; + var raw = getComputedStyle(document.documentElement).getPropertyValue('--color-bg-100').trim(); + if (!raw) return JSON.stringify({ mode: mode, bg: '' }); + var c = document.createElement('canvas').getContext('2d'); + c.fillStyle = raw; + var hex = c.fillStyle; + return JSON.stringify({ mode: mode, bg: hex }); + })(); + """.trimIndent() + webView.evaluateJavascript(js) { result -> + applySystemBarsFromJs(result) + } + } + + private fun applySystemBarsFromJs(result: String?) { + if (result == null || result == "null") return + val unescaped = result + .trim('"') + .replace("\\\\", "\\") + .replace("\\\"", "\"") + val json = try { + org.json.JSONObject(unescaped) + } catch (_: Exception) { + return + } + val mode = json.optString("mode", "system") + val bg = json.optString("bg", "") + val color = parseCssColor(bg) ?: return + val isLightBg = isColorLight(color) + val controller = WindowInsetsControllerCompat(window, window.decorView) + controller.isAppearanceLightStatusBars = isLightBg && mode != "dark" + controller.isAppearanceLightNavigationBars = isLightBg && mode != "dark" + window.statusBarColor = Color.TRANSPARENT + window.navigationBarColor = Color.TRANSPARENT + window.decorView.setBackgroundColor(color) + } + + private inner class SystemBarBridge { + @android.webkit.JavascriptInterface + fun setSystemBars(mode: String, bg: String) { + val color = parseCssColor(bg) ?: return + val isLightBg = isColorLight(color) + val controller = WindowInsetsControllerCompat(window, window.decorView) + controller.isAppearanceLightStatusBars = isLightBg && mode != "dark" + controller.isAppearanceLightNavigationBars = isLightBg && mode != "dark" + window.statusBarColor = Color.TRANSPARENT + window.navigationBarColor = Color.TRANSPARENT + window.decorView.setBackgroundColor(color) + } + + @android.webkit.JavascriptInterface + fun vibrate(ms: Int) { + val duration = ms.coerceIn(1, 50).toLong() + val vibrator = if (android.os.Build.VERSION.SDK_INT >= 31) { + val vm = getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager + vm.defaultVibrator + } else { + @Suppress("DEPRECATION") + getSystemService(Context.VIBRATOR_SERVICE) as Vibrator + } + if (!vibrator.hasVibrator()) return + if (android.os.Build.VERSION.SDK_INT >= 26) { + vibrator.vibrate(VibrationEffect.createOneShot(duration, VibrationEffect.DEFAULT_AMPLITUDE)) + } else { + @Suppress("DEPRECATION") + vibrator.vibrate(duration) + } + } + } + + private fun parseCssColor(value: String): Int? { + val v = value.trim() + if (v.isEmpty()) return null + + // 优先处理 #hex 格式(JS 端已统一转为此格式) + if (v.startsWith("#")) { + return try { + Color.parseColor(v) + } catch (_: Exception) { + null + } + } + + // 兼容处理 rgb/rgba — 同时支持逗号分隔和空格分隔 + if (v.startsWith("rgb")) { + val inner = v.substringAfter('(').substringBefore(')') + // 提取所有数字(整数或浮点) + val nums = Regex("""\d+\.?\d*""").findAll(inner).map { it.value }.toList() + if (nums.size < 3) return null + val r = nums[0].toFloatOrNull()?.toInt()?.coerceIn(0, 255) ?: return null + val g = nums[1].toFloatOrNull()?.toInt()?.coerceIn(0, 255) ?: return null + val b = nums[2].toFloatOrNull()?.toInt()?.coerceIn(0, 255) ?: return null + return Color.rgb(r, g, b) + } + + // 兼容处理 hsl/hsla — 同时支持逗号分隔和空格分隔 + if (v.startsWith("hsl")) { + val inner = v.substringAfter('(').substringBefore(')') + .replace("%", "") + val parts = Regex("""\d+\.?\d*""").findAll(inner).map { it.value }.toList() + if (parts.size < 3) return null + val h = parts[0].toFloatOrNull() ?: return null + val s = (parts[1].toFloatOrNull() ?: return null) / 100f + val l = (parts[2].toFloatOrNull() ?: return null) / 100f + return hslToColor(h, s, l) + } + + // 最后尝试 Color.parseColor (支持 named colors 等) + return try { + Color.parseColor(v) + } catch (_: Exception) { + null + } + } + + private fun hslToColor(h: Float, s: Float, l: Float): Int { + val c = (1 - kotlin.math.abs(2 * l - 1)) * s + val hh = (h % 360) / 60f + val x = c * (1 - kotlin.math.abs(hh % 2 - 1)) + val (r1, g1, b1) = when { + hh < 1 -> Triple(c, x, 0f) + hh < 2 -> Triple(x, c, 0f) + hh < 3 -> Triple(0f, c, x) + hh < 4 -> Triple(0f, x, c) + hh < 5 -> Triple(x, 0f, c) + else -> Triple(c, 0f, x) + } + val m = l - c / 2 + val r = ((r1 + m) * 255).toInt().coerceIn(0, 255) + val g = ((g1 + m) * 255).toInt().coerceIn(0, 255) + val b = ((b1 + m) * 255).toInt().coerceIn(0, 255) + return Color.rgb(r, g, b) + } + + private fun isColorLight(color: Int): Boolean { + val r = Color.red(color) / 255f + val g = Color.green(color) / 255f + val b = Color.blue(color) / 255f + val luminance = 0.299f * r + 0.587f * g + 0.114f * b + return luminance > 0.6f + } + + /** + * 延迟重试注入 insets,确保 WebView 加载完成后 CSS 变量被设置 + * 最多重试 10 次,间隔递增 + */ + private fun scheduleInsetsInjection(rootView: View, attempt: Int) { + if (attempt >= 10) return + val delay = if (attempt < 3) 200L else 1000L + handler.postDelayed({ + if (tryInjectInsets(rootView)) { + // 注入成功后再补几次,确保页面导航后也有值 + if (attempt < 5) { + scheduleInsetsInjection(rootView, attempt + 1) + } + } else { + scheduleInsetsInjection(rootView, attempt + 1) + } + }, delay) + } + + /** + * 尝试向 WebView 注入 insets CSS 变量 + * @return 是否找到了 WebView 并成功注入 + */ + private fun tryInjectInsets(view: View): Boolean { + val js = cachedInsetsJs ?: return false + val webView = findWebView(view) ?: return false + cachedWebView = webView + ensureJsBridge(webView) + webView.evaluateJavascript(js, null) + return true + } + + private fun ensureJsBridge(webView: WebView) { + try { + webView.addJavascriptInterface(SystemBarBridge(), "__opencode_android") + } catch (_: Exception) { + // ignore - may be added already + } + } + + private fun findWebView(view: View): WebView? { + if (view is WebView) return view + if (view is ViewGroup) { + for (i in 0 until view.childCount) { + findWebView(view.getChildAt(i))?.let { return it } + } + } + return null + } +} diff --git a/src-tauri/gen/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/src-tauri/gen/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 00000000..2b068d11 --- /dev/null +++ b/src-tauri/gen/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src-tauri/gen/android/app/src/main/res/drawable/ic_launcher_background.xml b/src-tauri/gen/android/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 00000000..07d5da9c --- /dev/null +++ b/src-tauri/gen/android/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src-tauri/gen/android/app/src/main/res/layout/activity_main.xml b/src-tauri/gen/android/app/src/main/res/layout/activity_main.xml new file mode 100644 index 00000000..4fc24441 --- /dev/null +++ b/src-tauri/gen/android/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,18 @@ + + + + + + \ No newline at end of file diff --git a/src-tauri/gen/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/src-tauri/gen/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 00000000..b8b5e35e --- /dev/null +++ b/src-tauri/gen/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 00000000..0a97f865 Binary files /dev/null and b/src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png b/src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png new file mode 100644 index 00000000..0089a963 Binary files /dev/null and b/src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png differ diff --git a/src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..09937623 Binary files /dev/null and b/src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 00000000..66a524c6 Binary files /dev/null and b/src-tauri/gen/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 00000000..77fa9921 Binary files /dev/null and b/src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png b/src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png new file mode 100644 index 00000000..1da732e5 Binary files /dev/null and b/src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png differ diff --git a/src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..a3fba079 Binary files /dev/null and b/src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 00000000..062ee53d Binary files /dev/null and b/src-tauri/gen/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 00000000..737c5014 Binary files /dev/null and b/src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png b/src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png new file mode 100644 index 00000000..21555214 Binary files /dev/null and b/src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png differ diff --git a/src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..4be6a36f Binary files /dev/null and b/src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 00000000..07d8f889 Binary files /dev/null and b/src-tauri/gen/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 00000000..d0985589 Binary files /dev/null and b/src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png b/src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png new file mode 100644 index 00000000..febaf145 Binary files /dev/null and b/src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png differ diff --git a/src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..3eeb05a0 Binary files /dev/null and b/src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 00000000..94cc5c17 Binary files /dev/null and b/src-tauri/gen/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 00000000..6d8f7a99 Binary files /dev/null and b/src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png b/src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png new file mode 100644 index 00000000..a3ba02ee Binary files /dev/null and b/src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png differ diff --git a/src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..f854170a Binary files /dev/null and b/src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 00000000..f229b801 Binary files /dev/null and b/src-tauri/gen/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/src-tauri/gen/android/app/src/main/res/values-night/themes.xml b/src-tauri/gen/android/app/src/main/res/values-night/themes.xml new file mode 100644 index 00000000..e82189e8 --- /dev/null +++ b/src-tauri/gen/android/app/src/main/res/values-night/themes.xml @@ -0,0 +1,14 @@ + + + + diff --git a/src-tauri/gen/android/app/src/main/res/values/colors.xml b/src-tauri/gen/android/app/src/main/res/values/colors.xml new file mode 100644 index 00000000..f8c6127d --- /dev/null +++ b/src-tauri/gen/android/app/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/src-tauri/gen/android/app/src/main/res/values/ic_launcher_background.xml b/src-tauri/gen/android/app/src/main/res/values/ic_launcher_background.xml new file mode 100644 index 00000000..ea9c223a --- /dev/null +++ b/src-tauri/gen/android/app/src/main/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #fff + \ No newline at end of file diff --git a/src-tauri/gen/android/app/src/main/res/values/strings.xml b/src-tauri/gen/android/app/src/main/res/values/strings.xml new file mode 100644 index 00000000..9c494b5a --- /dev/null +++ b/src-tauri/gen/android/app/src/main/res/values/strings.xml @@ -0,0 +1,4 @@ + + OpenCode + OpenCode + \ No newline at end of file diff --git a/src-tauri/gen/android/app/src/main/res/values/themes.xml b/src-tauri/gen/android/app/src/main/res/values/themes.xml new file mode 100644 index 00000000..32ca4e0c --- /dev/null +++ b/src-tauri/gen/android/app/src/main/res/values/themes.xml @@ -0,0 +1,14 @@ + + + + diff --git a/src-tauri/gen/android/app/src/main/res/xml/file_paths.xml b/src-tauri/gen/android/app/src/main/res/xml/file_paths.xml new file mode 100644 index 00000000..782d63b9 --- /dev/null +++ b/src-tauri/gen/android/app/src/main/res/xml/file_paths.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/src-tauri/gen/android/build.gradle.kts b/src-tauri/gen/android/build.gradle.kts new file mode 100644 index 00000000..607240bc --- /dev/null +++ b/src-tauri/gen/android/build.gradle.kts @@ -0,0 +1,22 @@ +buildscript { + repositories { + google() + mavenCentral() + } + dependencies { + classpath("com.android.tools.build:gradle:8.11.0") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.25") + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +tasks.register("clean").configure { + delete("build") +} + diff --git a/src-tauri/gen/android/buildSrc/build.gradle.kts b/src-tauri/gen/android/buildSrc/build.gradle.kts new file mode 100644 index 00000000..5c55bba7 --- /dev/null +++ b/src-tauri/gen/android/buildSrc/build.gradle.kts @@ -0,0 +1,23 @@ +plugins { + `kotlin-dsl` +} + +gradlePlugin { + plugins { + create("pluginsForCoolKids") { + id = "rust" + implementationClass = "RustPlugin" + } + } +} + +repositories { + google() + mavenCentral() +} + +dependencies { + compileOnly(gradleApi()) + implementation("com.android.tools.build:gradle:8.11.0") +} + diff --git a/src-tauri/gen/android/buildSrc/src/main/java/com/opencodeui/app/kotlin/BuildTask.kt b/src-tauri/gen/android/buildSrc/src/main/java/com/opencodeui/app/kotlin/BuildTask.kt new file mode 100644 index 00000000..a3de1256 --- /dev/null +++ b/src-tauri/gen/android/buildSrc/src/main/java/com/opencodeui/app/kotlin/BuildTask.kt @@ -0,0 +1,68 @@ +import java.io.File +import org.apache.tools.ant.taskdefs.condition.Os +import org.gradle.api.DefaultTask +import org.gradle.api.GradleException +import org.gradle.api.logging.LogLevel +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.TaskAction + +open class BuildTask : DefaultTask() { + @Input + var rootDirRel: String? = null + @Input + var target: String? = null + @Input + var release: Boolean? = null + + @TaskAction + fun assemble() { + val executable = """npm"""; + try { + runTauriCli(executable) + } catch (e: Exception) { + if (Os.isFamily(Os.FAMILY_WINDOWS)) { + // Try different Windows-specific extensions + val fallbacks = listOf( + "$executable.exe", + "$executable.cmd", + "$executable.bat", + ) + + var lastException: Exception = e + for (fallback in fallbacks) { + try { + runTauriCli(fallback) + return + } catch (fallbackException: Exception) { + lastException = fallbackException + } + } + throw lastException + } else { + throw e; + } + } + } + + fun runTauriCli(executable: String) { + val rootDirRel = rootDirRel ?: throw GradleException("rootDirRel cannot be null") + val target = target ?: throw GradleException("target cannot be null") + val release = release ?: throw GradleException("release cannot be null") + val args = listOf("run", "--", "tauri", "android", "android-studio-script"); + + project.exec { + workingDir(File(project.projectDir, rootDirRel)) + executable(executable) + args(args) + if (project.logger.isEnabled(LogLevel.DEBUG)) { + args("-vv") + } else if (project.logger.isEnabled(LogLevel.INFO)) { + args("-v") + } + if (release) { + args("--release") + } + args(listOf("--target", target)) + }.assertNormalExitValue() + } +} \ No newline at end of file diff --git a/src-tauri/gen/android/buildSrc/src/main/java/com/opencodeui/app/kotlin/RustPlugin.kt b/src-tauri/gen/android/buildSrc/src/main/java/com/opencodeui/app/kotlin/RustPlugin.kt new file mode 100644 index 00000000..4aa7fcaf --- /dev/null +++ b/src-tauri/gen/android/buildSrc/src/main/java/com/opencodeui/app/kotlin/RustPlugin.kt @@ -0,0 +1,85 @@ +import com.android.build.api.dsl.ApplicationExtension +import org.gradle.api.DefaultTask +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.configure +import org.gradle.kotlin.dsl.get + +const val TASK_GROUP = "rust" + +open class Config { + lateinit var rootDirRel: String +} + +open class RustPlugin : Plugin { + private lateinit var config: Config + + override fun apply(project: Project) = with(project) { + config = extensions.create("rust", Config::class.java) + + val defaultAbiList = listOf("arm64-v8a", "armeabi-v7a", "x86", "x86_64"); + val abiList = (findProperty("abiList") as? String)?.split(',') ?: defaultAbiList + + val defaultArchList = listOf("arm64", "arm", "x86", "x86_64"); + val archList = (findProperty("archList") as? String)?.split(',') ?: defaultArchList + + val targetsList = (findProperty("targetList") as? String)?.split(',') ?: listOf("aarch64", "armv7", "i686", "x86_64") + + extensions.configure { + @Suppress("UnstableApiUsage") + flavorDimensions.add("abi") + productFlavors { + create("universal") { + dimension = "abi" + ndk { + abiFilters += abiList + } + } + defaultArchList.forEachIndexed { index, arch -> + create(arch) { + dimension = "abi" + ndk { + abiFilters.add(defaultAbiList[index]) + } + } + } + } + } + + afterEvaluate { + for (profile in listOf("debug", "release")) { + val profileCapitalized = profile.replaceFirstChar { it.uppercase() } + val buildTask = tasks.maybeCreate( + "rustBuildUniversal$profileCapitalized", + DefaultTask::class.java + ).apply { + group = TASK_GROUP + description = "Build dynamic library in $profile mode for all targets" + } + + tasks["mergeUniversal${profileCapitalized}JniLibFolders"].dependsOn(buildTask) + + for (targetPair in targetsList.withIndex()) { + val targetName = targetPair.value + val targetArch = archList[targetPair.index] + val targetArchCapitalized = targetArch.replaceFirstChar { it.uppercase() } + val targetBuildTask = project.tasks.maybeCreate( + "rustBuild$targetArchCapitalized$profileCapitalized", + BuildTask::class.java + ).apply { + group = TASK_GROUP + description = "Build dynamic library in $profile mode for $targetArch" + rootDirRel = config.rootDirRel + target = targetName + release = profile == "release" + } + + buildTask.dependsOn(targetBuildTask) + tasks["merge$targetArchCapitalized${profileCapitalized}JniLibFolders"].dependsOn( + targetBuildTask + ) + } + } + } + } +} \ No newline at end of file diff --git a/src-tauri/gen/android/gradle.properties b/src-tauri/gen/android/gradle.properties new file mode 100644 index 00000000..2a7ec695 --- /dev/null +++ b/src-tauri/gen/android/gradle.properties @@ -0,0 +1,24 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app"s APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Kotlin code style for this project: "official" or "obsolete": +kotlin.code.style=official +# Enables namespacing of each library's R class so that its R class includes only the +# resources declared in the library itself and none from the library's dependencies, +# thereby reducing the size of the R class for that library +android.nonTransitiveRClass=true +android.nonFinalResIds=false \ No newline at end of file diff --git a/src-tauri/gen/android/gradle/wrapper/gradle-wrapper.jar b/src-tauri/gen/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..e708b1c0 Binary files /dev/null and b/src-tauri/gen/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/src-tauri/gen/android/gradle/wrapper/gradle-wrapper.properties b/src-tauri/gen/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..c5f9a53c --- /dev/null +++ b/src-tauri/gen/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Tue May 10 19:22:52 CST 2022 +distributionBase=GRADLE_USER_HOME +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip +distributionPath=wrapper/dists +zipStorePath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME diff --git a/src-tauri/gen/android/gradlew b/src-tauri/gen/android/gradlew new file mode 100644 index 00000000..4f906e0c --- /dev/null +++ b/src-tauri/gen/android/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/src-tauri/gen/android/gradlew.bat b/src-tauri/gen/android/gradlew.bat new file mode 100644 index 00000000..107acd32 --- /dev/null +++ b/src-tauri/gen/android/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/src-tauri/gen/android/settings.gradle b/src-tauri/gen/android/settings.gradle new file mode 100644 index 00000000..39391166 --- /dev/null +++ b/src-tauri/gen/android/settings.gradle @@ -0,0 +1,3 @@ +include ':app' + +apply from: 'tauri.settings.gradle' diff --git a/src-tauri/icons/128x128.png b/src-tauri/icons/128x128.png new file mode 100644 index 00000000..2e9c6b9a Binary files /dev/null and b/src-tauri/icons/128x128.png differ diff --git a/src-tauri/icons/128x128@2x.png b/src-tauri/icons/128x128@2x.png new file mode 100644 index 00000000..1c687cb6 Binary files /dev/null and b/src-tauri/icons/128x128@2x.png differ diff --git a/src-tauri/icons/32x32.png b/src-tauri/icons/32x32.png new file mode 100644 index 00000000..6897382f Binary files /dev/null and b/src-tauri/icons/32x32.png differ diff --git a/src-tauri/icons/64x64.png b/src-tauri/icons/64x64.png new file mode 100644 index 00000000..ba054440 Binary files /dev/null and b/src-tauri/icons/64x64.png differ diff --git a/src-tauri/icons/Square107x107Logo.png b/src-tauri/icons/Square107x107Logo.png new file mode 100644 index 00000000..ca3c513b Binary files /dev/null and b/src-tauri/icons/Square107x107Logo.png differ diff --git a/src-tauri/icons/Square142x142Logo.png b/src-tauri/icons/Square142x142Logo.png new file mode 100644 index 00000000..4e56583d Binary files /dev/null and b/src-tauri/icons/Square142x142Logo.png differ diff --git a/src-tauri/icons/Square150x150Logo.png b/src-tauri/icons/Square150x150Logo.png new file mode 100644 index 00000000..ee0507e6 Binary files /dev/null and b/src-tauri/icons/Square150x150Logo.png differ diff --git a/src-tauri/icons/Square284x284Logo.png b/src-tauri/icons/Square284x284Logo.png new file mode 100644 index 00000000..5b6efcff Binary files /dev/null and b/src-tauri/icons/Square284x284Logo.png differ diff --git a/src-tauri/icons/Square30x30Logo.png b/src-tauri/icons/Square30x30Logo.png new file mode 100644 index 00000000..eb5565ef Binary files /dev/null and b/src-tauri/icons/Square30x30Logo.png differ diff --git a/src-tauri/icons/Square310x310Logo.png b/src-tauri/icons/Square310x310Logo.png new file mode 100644 index 00000000..55c9b806 Binary files /dev/null and b/src-tauri/icons/Square310x310Logo.png differ diff --git a/src-tauri/icons/Square44x44Logo.png b/src-tauri/icons/Square44x44Logo.png new file mode 100644 index 00000000..c5f2b030 Binary files /dev/null and b/src-tauri/icons/Square44x44Logo.png differ diff --git a/src-tauri/icons/Square71x71Logo.png b/src-tauri/icons/Square71x71Logo.png new file mode 100644 index 00000000..358b7b35 Binary files /dev/null and b/src-tauri/icons/Square71x71Logo.png differ diff --git a/src-tauri/icons/Square89x89Logo.png b/src-tauri/icons/Square89x89Logo.png new file mode 100644 index 00000000..080c4308 Binary files /dev/null and b/src-tauri/icons/Square89x89Logo.png differ diff --git a/src-tauri/icons/StoreLogo.png b/src-tauri/icons/StoreLogo.png new file mode 100644 index 00000000..58151391 Binary files /dev/null and b/src-tauri/icons/StoreLogo.png differ diff --git a/src-tauri/icons/icon.icns b/src-tauri/icons/icon.icns new file mode 100644 index 00000000..96827b19 Binary files /dev/null and b/src-tauri/icons/icon.icns differ diff --git a/src-tauri/icons/icon.ico b/src-tauri/icons/icon.ico new file mode 100644 index 00000000..a4c28f86 Binary files /dev/null and b/src-tauri/icons/icon.ico differ diff --git a/src-tauri/icons/icon.png b/src-tauri/icons/icon.png new file mode 100644 index 00000000..1f815fe6 Binary files /dev/null and b/src-tauri/icons/icon.png differ diff --git a/src-tauri/icons/ios/AppIcon-20x20@1x.png b/src-tauri/icons/ios/AppIcon-20x20@1x.png new file mode 100644 index 00000000..232e45be Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-20x20@1x.png differ diff --git a/src-tauri/icons/ios/AppIcon-20x20@2x-1.png b/src-tauri/icons/ios/AppIcon-20x20@2x-1.png new file mode 100644 index 00000000..42b13784 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-20x20@2x-1.png differ diff --git a/src-tauri/icons/ios/AppIcon-20x20@2x.png b/src-tauri/icons/ios/AppIcon-20x20@2x.png new file mode 100644 index 00000000..42b13784 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-20x20@2x.png differ diff --git a/src-tauri/icons/ios/AppIcon-20x20@3x.png b/src-tauri/icons/ios/AppIcon-20x20@3x.png new file mode 100644 index 00000000..9bca5915 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-20x20@3x.png differ diff --git a/src-tauri/icons/ios/AppIcon-29x29@1x.png b/src-tauri/icons/ios/AppIcon-29x29@1x.png new file mode 100644 index 00000000..d95f9982 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-29x29@1x.png differ diff --git a/src-tauri/icons/ios/AppIcon-29x29@2x-1.png b/src-tauri/icons/ios/AppIcon-29x29@2x-1.png new file mode 100644 index 00000000..54a531d3 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-29x29@2x-1.png differ diff --git a/src-tauri/icons/ios/AppIcon-29x29@2x.png b/src-tauri/icons/ios/AppIcon-29x29@2x.png new file mode 100644 index 00000000..54a531d3 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-29x29@2x.png differ diff --git a/src-tauri/icons/ios/AppIcon-29x29@3x.png b/src-tauri/icons/ios/AppIcon-29x29@3x.png new file mode 100644 index 00000000..6dbddcd2 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-29x29@3x.png differ diff --git a/src-tauri/icons/ios/AppIcon-40x40@1x.png b/src-tauri/icons/ios/AppIcon-40x40@1x.png new file mode 100644 index 00000000..42b13784 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-40x40@1x.png differ diff --git a/src-tauri/icons/ios/AppIcon-40x40@2x-1.png b/src-tauri/icons/ios/AppIcon-40x40@2x-1.png new file mode 100644 index 00000000..b6da2e96 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-40x40@2x-1.png differ diff --git a/src-tauri/icons/ios/AppIcon-40x40@2x.png b/src-tauri/icons/ios/AppIcon-40x40@2x.png new file mode 100644 index 00000000..b6da2e96 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-40x40@2x.png differ diff --git a/src-tauri/icons/ios/AppIcon-40x40@3x.png b/src-tauri/icons/ios/AppIcon-40x40@3x.png new file mode 100644 index 00000000..11e0d769 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-40x40@3x.png differ diff --git a/src-tauri/icons/ios/AppIcon-512@2x.png b/src-tauri/icons/ios/AppIcon-512@2x.png new file mode 100644 index 00000000..17d42881 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-512@2x.png differ diff --git a/src-tauri/icons/ios/AppIcon-60x60@2x.png b/src-tauri/icons/ios/AppIcon-60x60@2x.png new file mode 100644 index 00000000..11e0d769 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-60x60@2x.png differ diff --git a/src-tauri/icons/ios/AppIcon-60x60@3x.png b/src-tauri/icons/ios/AppIcon-60x60@3x.png new file mode 100644 index 00000000..6ce60446 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-60x60@3x.png differ diff --git a/src-tauri/icons/ios/AppIcon-76x76@1x.png b/src-tauri/icons/ios/AppIcon-76x76@1x.png new file mode 100644 index 00000000..c2ef24d7 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-76x76@1x.png differ diff --git a/src-tauri/icons/ios/AppIcon-76x76@2x.png b/src-tauri/icons/ios/AppIcon-76x76@2x.png new file mode 100644 index 00000000..5b564ae5 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-76x76@2x.png differ diff --git a/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png b/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png new file mode 100644 index 00000000..ee8400d4 Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png differ diff --git a/src-tauri/src/app/bridge/args.rs b/src-tauri/src/app/bridge/args.rs new file mode 100644 index 00000000..8cbbc073 --- /dev/null +++ b/src-tauri/src/app/bridge/args.rs @@ -0,0 +1,74 @@ +use serde::Deserialize; + +/// Arguments for `bridge_connect`. +/// +/// `bridge_id` is an opaque label chosen by the frontend (e.g. `"sse"`, +/// a PTY id, etc.). Together with the window label it forms the unique +/// connection key. +/// +/// The Rust layer inspects the URL scheme to pick the transport: +/// - `ws://` / `wss://` → WebSocket (bidirectional) +/// - `http://` / `https://` → HTTP streaming (read-only) +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ConnectArgs { + bridge_id: String, + url: String, + auth_header: Option, +} + +impl ConnectArgs { + #[inline(always)] + pub fn bridge_id(&self) -> &str { + &self.bridge_id + } + + #[inline(always)] + pub fn url(&self) -> &str { + &self.url + } + + #[inline(always)] + pub fn auth_header(&self) -> Option<&str> { + self.auth_header.as_deref() + } + + /// Returns `true` when the URL uses WebSocket scheme. + pub fn is_websocket(&self) -> bool { + self.url.starts_with("ws://") || self.url.starts_with("wss://") + } +} + +/// Arguments for `bridge_send` (WebSocket only). +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SendArgs { + bridge_id: String, + data: String, +} + +impl SendArgs { + #[inline(always)] + pub fn bridge_id(&self) -> &str { + &self.bridge_id + } + + #[inline(always)] + pub fn data(&self) -> &str { + &self.data + } +} + +/// Arguments for `bridge_disconnect`. +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct DisconnectArgs { + bridge_id: String, +} + +impl DisconnectArgs { + #[inline(always)] + pub fn bridge_id(&self) -> &str { + &self.bridge_id + } +} diff --git a/src-tauri/src/app/bridge/event.rs b/src-tauri/src/app/bridge/event.rs new file mode 100644 index 00000000..3cb035ba --- /dev/null +++ b/src-tauri/src/app/bridge/event.rs @@ -0,0 +1,15 @@ +use serde::Serialize; + +/// Unified bridge event pushed to the frontend via Tauri Channel. +/// +/// The Rust layer is a transparent proxy — `data` is forwarded as-is +/// without parsing or field renaming. The frontend decides how to +/// interpret it (SSE line parsing, terminal output, etc.). +#[derive(Clone, Serialize)] +#[serde(rename_all = "camelCase", tag = "event", content = "data")] +pub enum BridgeEvent { + Connected, + Data { data: String }, + Disconnected { code: Option, reason: String }, + Error { message: String }, +} diff --git a/src-tauri/src/app/bridge/mod.rs b/src-tauri/src/app/bridge/mod.rs new file mode 100644 index 00000000..c781d250 --- /dev/null +++ b/src-tauri/src/app/bridge/mod.rs @@ -0,0 +1,7 @@ +mod args; +mod event; +mod state; + +pub use args::{ConnectArgs, DisconnectArgs, SendArgs}; +pub use event::BridgeEvent; +pub use state::{BridgeCommand, BridgeConnection, BridgeKey, BridgeState}; diff --git a/src-tauri/src/app/bridge/state.rs b/src-tauri/src/app/bridge/state.rs new file mode 100644 index 00000000..ebba9cb1 --- /dev/null +++ b/src-tauri/src/app/bridge/state.rs @@ -0,0 +1,142 @@ +use std::{ + collections::HashMap, + sync::{ + atomic::{AtomicU64, Ordering}, + Mutex, + }, +}; + +use tokio::sync::mpsc::UnboundedSender; + +/// Command sent from the frontend to an active WebSocket bridge. +#[derive(Debug)] +pub enum BridgeCommand { + Send(String), + Close, +} + +/// A single active bridge connection. +pub struct BridgeConnection { + pub id: u64, + /// `Some` for WebSocket connections (bidirectional), + /// `None` for HTTP stream connections (read-only, cancelled via id mismatch). + pub tx: Option>, +} + +impl BridgeConnection { + pub fn new_ws(id: u64, tx: UnboundedSender) -> Self { + Self { id, tx: Some(tx) } + } + + pub fn new_stream(id: u64) -> Self { + Self { id, tx: None } + } +} + +/// Composite key: (window label, bridge id). +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub struct BridgeKey { + window_label: String, + bridge_id: String, +} + +impl BridgeKey { + pub fn new(window_label: &str, bridge_id: &str) -> Self { + Self { + window_label: window_label.to_string(), + bridge_id: bridge_id.to_string(), + } + } + + pub fn window_label(&self) -> &str { + &self.window_label + } +} + +/// Global bridge state shared across all windows. +#[derive(Default)] +pub struct BridgeState { + next_id: AtomicU64, + active: Mutex>, +} + +impl BridgeState { + /// Allocate the next connection id. + pub fn next_conn_id(&self) -> u64 { + self.next_id.fetch_add(1, Ordering::SeqCst) + 1 + } + + /// Insert a new connection, returning the previous one (if any) so + /// the caller can shut it down. + pub fn replace(&self, key: BridgeKey, conn: BridgeConnection) -> Option { + self.active + .lock() + .expect("bridge state poisoned") + .insert(key, conn) + } + + /// Get the sender for a WebSocket connection. + pub fn sender(&self, key: &BridgeKey) -> Option> { + self.active + .lock() + .expect("bridge state poisoned") + .get(key) + .and_then(|conn| conn.tx.clone()) + } + + /// Remove the connection only if its id matches (prevents a new + /// connection from being removed by an old task's cleanup). + pub fn remove_if_current(&self, key: &BridgeKey, id: u64) { + let mut guard = self.active.lock().expect("bridge state poisoned"); + if guard.get(key).is_some_and(|conn| conn.id == id) { + guard.remove(key); + } + } + + /// Gracefully disconnect a specific bridge. + pub fn disconnect(&self, key: &BridgeKey) -> bool { + let removed = self + .active + .lock() + .expect("bridge state poisoned") + .remove(key); + if let Some(conn) = removed { + if let Some(tx) = conn.tx { + let _ = tx.send(BridgeCommand::Close); + } + return true; + } + false + } + + /// Disconnect all bridges belonging to a window (called on window destroy). + pub fn disconnect_window(&self, window_label: &str) { + let removed = { + let mut guard = self.active.lock().expect("bridge state poisoned"); + let keys: Vec<_> = guard + .keys() + .filter(|k| k.window_label() == window_label) + .cloned() + .collect(); + keys.into_iter() + .filter_map(|k| guard.remove(&k)) + .collect::>() + }; + + for conn in removed { + if let Some(tx) = conn.tx { + let _ = tx.send(BridgeCommand::Close); + } + } + } + + /// Check whether a connection id is still current (used by HTTP + /// stream loops to detect cancellation). + pub fn is_current(&self, key: &BridgeKey, id: u64) -> bool { + self.active + .lock() + .expect("bridge state poisoned") + .get(key) + .is_some_and(|conn| conn.id == id) + } +} diff --git a/src-tauri/src/app/commands/bridge.rs b/src-tauri/src/app/commands/bridge.rs new file mode 100644 index 00000000..c6b2b9aa --- /dev/null +++ b/src-tauri/src/app/commands/bridge.rs @@ -0,0 +1,402 @@ +// ============================================ +// Unified Bridge Commands +// +// A single set of Tauri commands that transparently proxies both +// HTTP streaming (SSE) and WebSocket (PTY) connections. +// +// The frontend picks the transport by URL scheme: +// ws:// / wss:// → WebSocket (bidirectional) +// http:// / https:// → HTTP stream (read-only) +// ============================================ + +use crate::app::bridge::{ + BridgeCommand, BridgeConnection, BridgeEvent, BridgeKey, BridgeState, ConnectArgs, + DisconnectArgs, SendArgs, +}; +use futures_util::{SinkExt, StreamExt}; +use std::time::Duration; +use tauri::{ipc::Channel, State}; +use tokio::sync::mpsc; + +fn emit(channel: &Channel, event: BridgeEvent) { + let _ = channel.send(event); +} + +fn split_valid_utf8_prefix(bytes: &[u8]) -> Option<(String, usize)> { + if bytes.is_empty() { + return None; + } + + match std::str::from_utf8(bytes) { + Ok(text) => Some((text.to_string(), bytes.len())), + Err(error) => { + let valid_up_to = error.valid_up_to(); + + if let Some(error_len) = error.error_len() { + let consumed = valid_up_to + error_len; + let text = String::from_utf8_lossy(&bytes[..consumed]).into_owned(); + Some((text, consumed)) + } else if valid_up_to > 0 { + let text = std::str::from_utf8(&bytes[..valid_up_to]) + .expect("valid_up_to must point to a valid UTF-8 prefix") + .to_string(); + Some((text, valid_up_to)) + } else { + None + } + } + } +} + +fn emit_stream_chunk(channel: &Channel, pending_utf8: &mut Vec, chunk: &[u8]) { + if chunk.is_empty() { + return; + } + + pending_utf8.extend_from_slice(chunk); + + while let Some((text, consumed)) = split_valid_utf8_prefix(pending_utf8.as_slice()) { + pending_utf8.drain(..consumed); + if !text.is_empty() { + emit(channel, BridgeEvent::Data { data: text }); + } + } +} + +// ============================================ +// bridge_connect — auto-selects transport +// ============================================ + +#[tauri::command] +pub async fn bridge_connect( + window: tauri::Window, + state: State<'_, BridgeState>, + args: ConnectArgs, + on_event: Channel, +) -> Result<(), String> { + if args.is_websocket() { + connect_ws(window, state, args, on_event).await + } else { + connect_stream(window, state, args, on_event).await + } +} + +// ============================================ +// bridge_send — WebSocket only +// ============================================ + +#[tauri::command] +pub async fn bridge_send( + window: tauri::Window, + state: State<'_, BridgeState>, + args: SendArgs, +) -> Result<(), String> { + let key = BridgeKey::new(window.label(), args.bridge_id()); + let sender = state + .sender(&key) + .ok_or_else(|| format!("bridge '{}' is not active", args.bridge_id()))?; + + sender + .send(BridgeCommand::Send(args.data().to_string())) + .map_err(|_| format!("bridge '{}' is closed", args.bridge_id())) +} + +// ============================================ +// bridge_disconnect +// ============================================ + +#[tauri::command] +pub async fn bridge_disconnect( + window: tauri::Window, + state: State<'_, BridgeState>, + args: DisconnectArgs, +) -> Result<(), String> { + let key = BridgeKey::new(window.label(), args.bridge_id()); + state.disconnect(&key); + Ok(()) +} + +// ============================================ +// HTTP stream transport (for SSE) +// ============================================ + +async fn connect_stream( + window: tauri::Window, + state: State<'_, BridgeState>, + args: ConnectArgs, + on_event: Channel, +) -> Result<(), String> { + let conn_id = state.next_conn_id(); + let key = BridgeKey::new(window.label(), args.bridge_id()); + + // Replace any previous connection with the same key + if let Some(prev) = state.replace(key.clone(), BridgeConnection::new_stream(conn_id)) { + if let Some(tx) = prev.tx { + let _ = tx.send(BridgeCommand::Close); + } + } + + let client = reqwest::Client::builder() + .connect_timeout(Duration::from_secs(15)) + .tcp_keepalive(Duration::from_secs(30)) + .build() + .map_err(|e| format!("failed to create HTTP client: {}", e))?; + + let mut req = client.get(args.url()); + if let Some(auth) = args.auth_header() { + req = req.header("Authorization", auth); + } + + let response = match req.send().await { + Ok(r) => r, + Err(e) => { + let msg = format!("HTTP stream connection failed: {}", e); + emit( + &on_event, + BridgeEvent::Error { + message: msg.clone(), + }, + ); + state.remove_if_current(&key, conn_id); + return Err(msg); + } + }; + + if !response.status().is_success() { + let msg = format!("HTTP stream server returned {}", response.status()); + emit( + &on_event, + BridgeEvent::Error { + message: msg.clone(), + }, + ); + state.remove_if_current(&key, conn_id); + return Err(msg); + } + + emit(&on_event, BridgeEvent::Connected); + + // Read timeout — if no data arrives for 90s the connection is likely dead + const READ_TIMEOUT: Duration = Duration::from_secs(90); + let mut stream = response.bytes_stream(); + let mut pending_utf8 = Vec::new(); + + loop { + // Check cancellation (disconnect or replaced by a new connect) + if !state.is_current(&key, conn_id) { + emit( + &on_event, + BridgeEvent::Disconnected { + code: None, + reason: "Disconnected by client".to_string(), + }, + ); + return Ok(()); + } + + match tokio::time::timeout(READ_TIMEOUT, stream.next()).await { + Ok(Some(Ok(chunk))) => { + emit_stream_chunk(&on_event, &mut pending_utf8, chunk.as_ref()); + } + Ok(Some(Err(e))) => { + let msg = format!("HTTP stream error: {}", e); + emit( + &on_event, + BridgeEvent::Error { + message: msg.clone(), + }, + ); + state.remove_if_current(&key, conn_id); + return Err(msg); + } + Ok(None) => { + state.remove_if_current(&key, conn_id); + emit( + &on_event, + BridgeEvent::Disconnected { + code: None, + reason: "Stream ended".to_string(), + }, + ); + return Ok(()); + } + Err(_) => { + let msg = format!( + "HTTP stream read timeout ({}s without data)", + READ_TIMEOUT.as_secs() + ); + emit( + &on_event, + BridgeEvent::Error { + message: msg.clone(), + }, + ); + state.remove_if_current(&key, conn_id); + return Err(msg); + } + } + } +} + +#[cfg(test)] +mod tests { + use super::split_valid_utf8_prefix; + + #[test] + fn split_valid_utf8_prefix_waits_for_incomplete_chinese_bytes() { + let text = "中文A"; + let bytes = text.as_bytes(); + + assert_eq!(split_valid_utf8_prefix(&bytes[..2]), None); + assert_eq!( + split_valid_utf8_prefix(&bytes[..3]), + Some(("中".to_string(), 3)) + ); + assert_eq!( + split_valid_utf8_prefix(bytes), + Some((text.to_string(), bytes.len())) + ); + } + + #[test] + fn split_valid_utf8_prefix_returns_valid_prefix_before_incomplete_tail() { + let mut bytes = "中文".as_bytes().to_vec(); + bytes.extend_from_slice(&"世".as_bytes()[..2]); + + assert_eq!( + split_valid_utf8_prefix(&bytes), + Some(("中文".to_string(), "中文".len())) + ); + } +} + +// ============================================ +// WebSocket transport (for PTY) +// ============================================ + +async fn connect_ws( + window: tauri::Window, + state: State<'_, BridgeState>, + args: ConnectArgs, + on_event: Channel, +) -> Result<(), String> { + use tokio_tungstenite::{ + connect_async, + tungstenite::{client::IntoClientRequest, http::HeaderValue, Error as WsError, Message}, + }; + + let conn_id = state.next_conn_id(); + let key = BridgeKey::new(window.label(), args.bridge_id()); + let (tx, mut rx) = mpsc::unbounded_channel(); + + // Replace any previous connection with the same key + if let Some(prev) = state.replace(key.clone(), BridgeConnection::new_ws(conn_id, tx)) { + if let Some(prev_tx) = prev.tx { + let _ = prev_tx.send(BridgeCommand::Close); + } + } + + let mut request = args + .url() + .into_client_request() + .map_err(|e| format!("invalid WebSocket URL: {}", e))?; + + if let Some(auth) = args.auth_header() { + let value = HeaderValue::from_str(auth) + .map_err(|e| format!("invalid Authorization header: {}", e))?; + request.headers_mut().insert("Authorization", value); + } + + let (ws_stream, _) = match connect_async(request).await { + Ok(result) => result, + Err(error) => { + let message = match error { + WsError::Http(response) => { + format!("WebSocket server returned {}", response.status()) + } + other => format!("WebSocket connection failed: {}", other), + }; + emit( + &on_event, + BridgeEvent::Error { + message: message.clone(), + }, + ); + state.remove_if_current(&key, conn_id); + return Err(message); + } + }; + + emit(&on_event, BridgeEvent::Connected); + + let (mut write, mut read) = ws_stream.split(); + + loop { + tokio::select! { + outbound = rx.recv() => match outbound { + Some(BridgeCommand::Send(data)) => { + if let Err(error) = write.send(Message::Text(data.into())).await { + let msg = format!("WebSocket write failed: {}", error); + emit(&on_event, BridgeEvent::Error { message: msg.clone() }); + state.remove_if_current(&key, conn_id); + return Err(msg); + } + } + Some(BridgeCommand::Close) | None => { + let _ = write.close().await; + state.remove_if_current(&key, conn_id); + emit(&on_event, BridgeEvent::Disconnected { + code: Some(1000), + reason: "Disconnected by client".to_string(), + }); + return Ok(()); + } + }, + inbound = read.next() => match inbound { + Some(Ok(message)) => match message { + Message::Text(text) => { + emit(&on_event, BridgeEvent::Data { data: text.to_string() }); + } + Message::Binary(bytes) => { + emit(&on_event, BridgeEvent::Data { + data: String::from_utf8_lossy(&bytes).into_owned(), + }); + } + Message::Ping(payload) => { + if let Err(error) = write.send(Message::Pong(payload)).await { + let msg = format!("WebSocket pong failed: {}", error); + emit(&on_event, BridgeEvent::Error { message: msg.clone() }); + state.remove_if_current(&key, conn_id); + return Err(msg); + } + } + Message::Pong(_) => {} + Message::Close(frame) => { + let code = frame.as_ref().map(|f| u16::from(f.code)); + let reason = frame + .map(|f| f.reason.to_string()) + .unwrap_or_else(|| "Connection closed by server".to_string()); + state.remove_if_current(&key, conn_id); + emit(&on_event, BridgeEvent::Disconnected { code, reason }); + return Ok(()); + } + _ => {} + }, + Some(Err(error)) => { + let msg = format!("WebSocket stream error: {}", error); + emit(&on_event, BridgeEvent::Error { message: msg.clone() }); + state.remove_if_current(&key, conn_id); + return Err(msg); + } + None => { + state.remove_if_current(&key, conn_id); + emit(&on_event, BridgeEvent::Disconnected { + code: None, + reason: "Stream ended".to_string(), + }); + return Ok(()); + } + } + } + } +} diff --git a/src-tauri/src/app/commands/mod.rs b/src-tauri/src/app/commands/mod.rs new file mode 100644 index 00000000..14419a16 --- /dev/null +++ b/src-tauri/src/app/commands/mod.rs @@ -0,0 +1,5 @@ +pub mod bridge; +#[cfg(not(target_os = "android"))] +pub mod opencode; +#[cfg(not(target_os = "android"))] +pub mod utils; diff --git a/src-tauri/src/app/commands/opencode.rs b/src-tauri/src/app/commands/opencode.rs new file mode 100644 index 00000000..dd980365 --- /dev/null +++ b/src-tauri/src/app/commands/opencode.rs @@ -0,0 +1,387 @@ +// ============================================ +// OpenCode Service Management (desktop only) +// Android 不支持子进程管理和 window.destroy() +// ============================================ + +use crate::app::service::ServiceState; +use serde::Serialize; +use std::{ + collections::VecDeque, + env, + ffi::OsString, + io::{BufRead, BufReader, Read}, + path::{Path, PathBuf}, + process::{Child, Command, Stdio}, + sync::{atomic::Ordering, mpsc}, + thread, + time::Duration, +}; +use tauri::State; + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct StartOpencodeServiceResult { + started: bool, + started_by_us: bool, + url: Option, +} + +struct SpawnedOpencodeServe { + child: Child, + output: mpsc::Receiver, +} + +/// 检查 opencode 服务是否在运行(通过 health endpoint) +pub async fn is_service_running(url: &str) -> bool { + let health_url = format!("{}/global/health", url.trim_end_matches('/')); + match reqwest::Client::builder() + .connect_timeout(Duration::from_secs(3)) + .build() + { + Ok(client) => client + .get(&health_url) + .timeout(Duration::from_secs(5)) + .send() + .await + .map(|r| r.status().is_success()) + .unwrap_or(false), + Err(_) => false, + } +} + +/// 启动 opencode serve 进程 +fn spawn_opencode_serve( + binary_path: &str, + env_vars: &std::collections::HashMap, +) -> Result { + log::info!("Starting opencode serve with binary: {}", binary_path); + if !env_vars.is_empty() { + log::info!("Injecting {} environment variable(s)", env_vars.len()); + } + + let serve_args = ["serve".to_string()]; + + let mut cmd = build_opencode_command(binary_path, &serve_args); + cmd.stdout(Stdio::piped()).stderr(Stdio::piped()); + + // 注入用户配置的环境变量 + for (key, value) in env_vars { + cmd.env(key, value); + } + + #[cfg(target_os = "windows")] + { + use std::os::windows::process::CommandExt; + const CREATE_NO_WINDOW: u32 = 0x08000000; + cmd.creation_flags(CREATE_NO_WINDOW); + } + + let mut child = cmd.spawn().map_err(|e| { + format!( + "Failed to start '{}': {}. Check that the path is correct.", + binary_path, e + ) + })?; + + let (tx, output) = mpsc::channel(); + if let Some(stdout) = child.stdout.take() { + spawn_output_reader(stdout, tx.clone()); + } + if let Some(stderr) = child.stderr.take() { + spawn_output_reader(stderr, tx); + } + + Ok(SpawnedOpencodeServe { child, output }) +} + +fn spawn_output_reader(reader: R, tx: mpsc::Sender) +where + R: Read + Send + 'static, +{ + thread::spawn(move || { + let mut tx = Some(tx); + for line in BufReader::new(reader).lines().map_while(Result::ok) { + if let Some(sender) = tx.as_ref() { + if sender.send(line).is_err() { + tx = None; + } + } + } + }); +} + +fn parse_listening_url(line: &str) -> Option { + let start = line.find("http://").or_else(|| line.find("https://"))?; + let raw_url = line[start..] + .split_whitespace() + .next()? + .trim_end_matches(|c| matches!(c, ',' | ';' | ')')); + let normalized = raw_url + .replace("http://0.0.0.0:", "http://127.0.0.1:") + .replace("https://0.0.0.0:", "https://127.0.0.1:"); + let parsed = reqwest::Url::parse(&normalized).ok()?; + + Some(parsed.to_string().trim_end_matches('/').to_string()) +} + +fn remember_recent_output(recent_output: &mut VecDeque, line: String) { + if recent_output.len() >= 8 { + recent_output.pop_front(); + } + recent_output.push_back(line); +} + +fn format_recent_output(recent_output: &VecDeque) -> String { + if recent_output.is_empty() { + return String::new(); + } + + format!( + " Recent output: {}", + recent_output + .iter() + .cloned() + .collect::>() + .join(" | ") + ) +} + +fn build_opencode_command(binary_path: &str, args: &[String]) -> Command { + #[cfg(target_os = "windows")] + { + let path = Path::new(binary_path); + let ext = path + .extension() + .and_then(|value| value.to_str()) + .unwrap_or(""); + let requires_shell = ext.eq_ignore_ascii_case("cmd") + || ext.eq_ignore_ascii_case("bat") + || path.extension().is_none(); + + if requires_shell { + let mut cmd = Command::new("cmd.exe"); + cmd.arg("/C").arg(binary_path).args(args); + return cmd; + } + } + + let mut cmd = Command::new(binary_path); + cmd.args(args); + cmd +} + +fn patched_env_var( + env_vars: &std::collections::HashMap, + key: &str, +) -> Option { + for (env_key, value) in env_vars { + if env_key.eq_ignore_ascii_case(key) { + return Some(OsString::from(value)); + } + } + env::var_os(key) +} + +fn path_candidates(env_vars: &std::collections::HashMap) -> Vec { + let mut candidates = Vec::new(); + + if let Some(bin) = patched_env_var(env_vars, "OPENCODE_BIN") { + if !bin.is_empty() { + candidates.push(PathBuf::from(bin)); + } + } + + let Some(path) = patched_env_var(env_vars, "PATH") else { + return candidates; + }; + + let names: Vec<&str> = if cfg!(windows) { + vec!["opencode.exe", "opencode.cmd", "opencode.bat", "opencode"] + } else { + vec!["opencode"] + }; + + for dir in env::split_paths(&path) { + for name in &names { + candidates.push(dir.join(name)); + } + } + + candidates +} + +fn is_runnable_file(path: &Path) -> bool { + path.is_file() +} + +/// 自动检测 opencode 可执行文件,行为接近直接在终端输入 `opencode`。 +#[tauri::command] +pub async fn detect_opencode_binary( + env_vars: std::collections::HashMap, +) -> Result, String> { + for candidate in path_candidates(&env_vars) { + if is_runnable_file(&candidate) { + return Ok(Some(candidate.to_string_lossy().to_string())); + } + } + + Ok(None) +} + +/// 跨平台杀进程 +pub fn kill_process_by_pid(pid: u32) { + #[cfg(target_os = "windows")] + { + use std::os::windows::process::CommandExt; + const CREATE_NO_WINDOW: u32 = 0x08000000; + let _ = Command::new("taskkill") + .args(["/PID", &pid.to_string(), "/F", "/T"]) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .creation_flags(CREATE_NO_WINDOW) + .spawn(); + } + + #[cfg(not(target_os = "windows"))] + { + let _ = Command::new("kill") + .arg(pid.to_string()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .spawn(); + } +} + +/// 检查 opencode 服务是否在运行 +#[tauri::command] +pub async fn check_opencode_service(url: String) -> Result { + Ok(is_service_running(&url).await) +} + +/// 启动 opencode serve +#[tauri::command] +pub async fn start_opencode_service( + state: State<'_, ServiceState>, + url: String, + binary_path: String, + env_vars: std::collections::HashMap, +) -> Result { + if state.we_started.load(Ordering::SeqCst) { + let current_url = state.service_url.lock().map_err(|e| e.to_string())?.clone(); + if let Some(current_url) = current_url { + if is_service_running(¤t_url).await { + log::info!("opencode service already running at {}", current_url); + return Ok(StartOpencodeServiceResult { + started: false, + started_by_us: true, + url: Some(current_url), + }); + } + } + } + + if is_service_running(&url).await { + log::info!("opencode service already running at {}", url); + return Ok(StartOpencodeServiceResult { + started: false, + started_by_us: false, + url: Some(url), + }); + } + + let mut spawned = spawn_opencode_serve(&binary_path, &env_vars)?; + let pid = spawned.child.id(); + log::info!("Started opencode serve, PID: {}", pid); + + state.child_pid.store(pid, Ordering::SeqCst); + state.we_started.store(true, Ordering::SeqCst); + *state.service_url.lock().map_err(|e| e.to_string())? = None; + + let mut detected_url: Option = None; + let mut recent_output = VecDeque::new(); + + for _ in 0..30 { + while let Ok(line) = spawned.output.try_recv() { + if let Some(parsed_url) = parse_listening_url(&line) { + log::info!("Detected opencode serve URL: {}", parsed_url); + *state.service_url.lock().map_err(|e| e.to_string())? = Some(parsed_url.clone()); + detected_url = Some(parsed_url); + } + remember_recent_output(&mut recent_output, line); + } + + if let Some(status) = spawned.child.try_wait().map_err(|e| e.to_string())? { + state.child_pid.store(0, Ordering::SeqCst); + state.we_started.store(false, Ordering::SeqCst); + *state.service_url.lock().map_err(|e| e.to_string())? = None; + return Err(format!( + "opencode serve exited during startup with status {}.{}", + status, + format_recent_output(&recent_output) + )); + } + + let health_url = detected_url.as_deref().unwrap_or(&url); + if is_service_running(health_url).await { + log::info!("opencode service is ready at {}", health_url); + *state.service_url.lock().map_err(|e| e.to_string())? = Some(health_url.to_string()); + return Ok(StartOpencodeServiceResult { + started: true, + started_by_us: true, + url: Some(health_url.to_string()), + }); + } + + tokio::time::sleep(Duration::from_millis(500)).await; + } + + log::warn!("opencode service started but health check not passing yet"); + Ok(StartOpencodeServiceResult { + started: true, + started_by_us: true, + url: detected_url, + }) +} + +/// 停止 opencode serve +#[tauri::command] +pub async fn stop_opencode_service(state: State<'_, ServiceState>) -> Result<(), String> { + let pid = state.child_pid.swap(0, Ordering::SeqCst); + state.we_started.store(false, Ordering::SeqCst); + *state.service_url.lock().map_err(|e| e.to_string())? = None; + + if pid > 0 { + log::info!("Stopping opencode serve, PID: {}", pid); + kill_process_by_pid(pid); + } + + Ok(()) +} + +/// 查询是否由我们启动了 opencode 服务 +#[tauri::command] +pub async fn get_service_started_by_us(state: State<'_, ServiceState>) -> Result { + Ok(state.we_started.load(Ordering::SeqCst)) +} + +/// 确认关闭应用(前端调用,可选择是否同时停止服务) +#[tauri::command] +pub async fn confirm_close_app( + window: tauri::Window, + state: State<'_, ServiceState>, + stop_service: bool, +) -> Result<(), String> { + if stop_service { + let pid = state.child_pid.swap(0, Ordering::SeqCst); + if pid > 0 { + log::info!("Closing app and stopping opencode serve, PID: {}", pid); + kill_process_by_pid(pid); + } + state.we_started.store(false, Ordering::SeqCst); + *state.service_url.lock().map_err(|e| e.to_string())? = None; + } else { + log::info!("Closing app, keeping opencode serve running"); + } + + window.destroy().map_err(|e| e.to_string()) +} diff --git a/src-tauri/src/app/commands/utils.rs b/src-tauri/src/app/commands/utils.rs new file mode 100644 index 00000000..8427d08b --- /dev/null +++ b/src-tauri/src/app/commands/utils.rs @@ -0,0 +1,61 @@ +use crate::app::dir_state::OpenDirectoryState; +use serde::Serialize; +use std::sync::Arc; +use tauri::State; + +#[derive(Serialize)] +pub struct DroppedPathInfo { + #[serde(rename = "type")] + kind: &'static str, + path: String, + name: String, +} + +/// 获取启动时传入的目录路径(一次性读取后清空) +#[tauri::command] +pub fn get_cli_directory( + window: tauri::Window, + state: State<'_, OpenDirectoryState>, +) -> Option> { + state.pending().pin().remove(window.label()).cloned() +} + +/// 新建桌面窗口 +#[cfg(not(target_os = "android"))] +#[tauri::command] +pub async fn open_new_window(app: tauri::AppHandle, directory: Option) { + crate::app::create_new_window(&app, directory); +} + +/// 桌面窗口前端首帧完成后,通知 Rust 显示真实窗口并关闭 loading 窗口 +#[cfg(not(target_os = "android"))] +#[tauri::command] +pub fn desktop_window_ready(window: tauri::Window) -> Result<(), String> { + crate::app::mark_window_ready(&window).map_err(|err| err.to_string()) +} + +/// 获取拖入路径的基础信息,用于前端区分文件/目录并生成 @ 引用。 +#[tauri::command] +pub fn get_dropped_paths_info(paths: Vec) -> Vec { + paths + .into_iter() + .filter_map(|path| { + let metadata = std::fs::metadata(&path).ok()?; + let kind = if metadata.is_dir() { + "folder" + } else if metadata.is_file() { + "file" + } else { + return None; + }; + + let name = std::path::Path::new(&path) + .file_name() + .map(|name| name.to_string_lossy().to_string()) + .filter(|name| !name.is_empty()) + .unwrap_or_else(|| path.clone()); + + Some(DroppedPathInfo { kind, path, name }) + }) + .collect() +} diff --git a/src-tauri/src/app/dir_state.rs b/src-tauri/src/app/dir_state.rs new file mode 100644 index 00000000..83e44634 --- /dev/null +++ b/src-tauri/src/app/dir_state.rs @@ -0,0 +1,27 @@ +// ============================================ +// Open Directory State (desktop only) +// 存储启动时传入的目录路径(右键菜单、拖放等) +// ============================================ + +use papaya::HashMap as PaHashMap; +use rapidhash::fast::RandomState; +use std::sync::Arc; + +pub struct OpenDirectoryState { + /// per-window 待处理目录: window label → directory path + pending: PaHashMap, RandomState>, +} + +impl Default for OpenDirectoryState { + fn default() -> Self { + Self { + pending: PaHashMap::with_hasher(RandomState::new()), + } + } +} + +impl OpenDirectoryState { + pub fn pending(&self) -> &PaHashMap, RandomState> { + &self.pending + } +} diff --git a/src-tauri/src/app/mod.rs b/src-tauri/src/app/mod.rs new file mode 100644 index 00000000..bfb621c6 --- /dev/null +++ b/src-tauri/src/app/mod.rs @@ -0,0 +1,462 @@ +// ============================================ +// Tauri Application Entry Point +// Unified Bridge + Plugin Registration + Service Management +// ============================================ +mod bridge; +mod commands; +#[cfg(not(target_os = "android"))] +mod dir_state; +mod service; + +use bridge::BridgeState; +use serde::{Deserialize, Serialize}; +use std::sync::atomic::{AtomicU64, Ordering}; +use tauri::Manager; + +#[cfg(any(windows, target_os = "macos"))] +use tauri_plugin_decorum::WebviewWindowExt; + +// Desktop-only imports for service management +#[cfg(not(target_os = "android"))] +use dir_state::OpenDirectoryState; +#[cfg(not(target_os = "android"))] +use std::sync::Arc; +#[cfg(not(target_os = "android"))] +use tauri::Emitter; + +#[cfg(not(target_os = "android"))] +#[derive(Debug, Clone, Serialize, Deserialize)] +struct SavedWindowState { + width: u32, + height: u32, + x: i32, + y: i32, + maximized: bool, +} + +#[cfg(not(target_os = "android"))] +fn window_state_path(app: &tauri::AppHandle) -> Option { + let dir = app.path().app_config_dir().ok()?; + Some(dir.join("window-state.json")) +} + +#[cfg(not(target_os = "android"))] +fn load_window_state(app: &tauri::AppHandle) -> Option { + let path = window_state_path(app)?; + let data = std::fs::read_to_string(path).ok()?; + serde_json::from_str(&data).ok() +} + +#[cfg(not(target_os = "android"))] +fn save_window_state(window: &tauri::Window) { + if window.label() != "main" { + return; + } + + let Ok(size) = window.outer_size() else { + return; + }; + let Ok(position) = window.outer_position() else { + return; + }; + let maximized = window.is_maximized().unwrap_or(false); + + let state = SavedWindowState { + width: size.width, + height: size.height, + x: position.x, + y: position.y, + maximized, + }; + + let app = window.app_handle(); + let Some(path) = window_state_path(app) else { + return; + }; + if let Some(parent) = path.parent() { + let _ = std::fs::create_dir_all(parent); + } + if let Ok(data) = serde_json::to_string(&state) { + let _ = std::fs::write(path, data); + } +} + +#[cfg(not(target_os = "android"))] +fn restore_window_state(window: &tauri::WebviewWindow) { + let app = window.app_handle(); + let Some(state) = load_window_state(app) else { + return; + }; + + if state.width >= 400 && state.height >= 300 { + let _ = window.set_size(tauri::PhysicalSize::new(state.width, state.height)); + } + let _ = window.set_position(tauri::PhysicalPosition::new(state.x, state.y)); + + if state.maximized { + let _ = window.maximize(); + } +} + +/// 从命令行参数中提取目录路径 +#[cfg(not(target_os = "android"))] +fn extract_directory_from_args(args: &[String]) -> Option { + for arg in args.iter().skip(1) { + if arg.starts_with('-') { + continue; + } + if std::path::Path::new(arg).is_dir() { + return Some(arg.clone()); + } + } + None +} + +#[cfg(not(target_os = "android"))] +fn create_main_window(app: &tauri::AppHandle) -> Result { + if let Some(window) = app.get_webview_window("main") { + return Ok(window); + } + + let config = app + .config() + .app + .windows + .iter() + .find(|window| window.label == "main") + .cloned() + .expect("main window config missing"); + + configure_desktop_window_builder(tauri::WebviewWindowBuilder::from_config(app, &config)?) + .visible(false) + .build() +} + +#[cfg(target_os = "android")] +fn create_main_window(app: &tauri::AppHandle) -> Result { + if let Some(window) = app.get_webview_window("main") { + return Ok(window); + } + + let config = app + .config() + .app + .windows + .iter() + .find(|window| window.label == "main") + .cloned() + .expect("main window config missing"); + + tauri::WebviewWindowBuilder::from_config(app, &config)?.build() +} + +#[cfg(not(target_os = "android"))] +fn create_hidden_content_window( + app: &tauri::AppHandle, + label: &str, +) -> Result { + let builder = configure_desktop_window_builder(tauri::WebviewWindowBuilder::new( + app, + label, + tauri::WebviewUrl::App("index.html".into()), + )) + .title("OpenCode") + .inner_size(800.0, 600.0); + + builder.visible(false).build() +} + +/// macOS 红绿灯(关闭/最小化/最大化)相对窗口左上角的偏移。 +/// 注意:这里通过 decorum 的 `set_traffic_lights_inset` 应用,其内部定位算法与 +/// Tauri 原生 `trafficLightPosition` 不同,y 值需按与自定义标题栏的视觉对齐微调。 +#[cfg(target_os = "macos")] +const TRAFFIC_LIGHT_INSET: (f32, f32) = (12.0, 14.0); + +/// 重新定位 macOS 红绿灯。 +/// macOS 在退出全屏后会把红绿灯重置回系统默认位置, +/// 因此需要在退出全屏时重新应用偏移,保持与自定义标题栏垂直对齐。 +#[cfg(target_os = "macos")] +fn reposition_traffic_lights(window: &tauri::WebviewWindow) { + let (x, y) = TRAFFIC_LIGHT_INSET; + let _ = window.set_traffic_lights_inset(x, y); +} + +/// 记录每个窗口上一次的全屏状态(按 label),用于检测「退出全屏」这一跳变。 +#[cfg(target_os = "macos")] +fn fullscreen_state() -> &'static std::sync::Mutex> { + static STATE: std::sync::OnceLock>> = + std::sync::OnceLock::new(); + STATE.get_or_init(|| std::sync::Mutex::new(std::collections::HashMap::new())) +} + +#[cfg(not(target_os = "android"))] +fn finish_desktop_window_setup(window: &tauri::WebviewWindow) { + #[cfg(windows)] + let _ = window.create_overlay_titlebar(); + + // macOS:初始定位红绿灯,使其与自定义标题栏对齐 + #[cfg(target_os = "macos")] + reposition_traffic_lights(window); +} + +#[cfg(not(target_os = "android"))] +pub(crate) fn mark_window_ready( + window: &tauri::Window, +) -> Result<(), tauri::Error> { + window.show()?; + let _ = window.set_focus(); + + Ok(()) +} + +/// 创建新窗口,可选地关联一个目录(多窗口支持) +#[cfg(not(target_os = "android"))] +pub(crate) fn create_new_window(app: &tauri::AppHandle, directory: Option) { + static WIN_COUNTER: AtomicU64 = AtomicU64::new(1); + let label = format!("win-{}", WIN_COUNTER.fetch_add(1, Ordering::SeqCst)); + + if let Some(ref dir) = directory { + if let Some(state) = app.try_state::() { + state + .pending() + .pin() + .insert(label.clone(), Arc::from(dir.clone())); + } + } + + match create_hidden_content_window(app, &label) { + Ok(window) => { + finish_desktop_window_setup(&window); + + log::info!( + "Created new window '{}' for directory: {:?}", + label, + directory + ) + } + Err(e) => log::error!("Failed to create new window: {}", e), + } +} + +#[cfg(not(target_os = "android"))] +fn configure_desktop_window_builder<'a, R: tauri::Runtime, M: tauri::Manager>( + window_builder: tauri::WebviewWindowBuilder<'a, R, M>, +) -> tauri::WebviewWindowBuilder<'a, R, M> { + let window_builder = window_builder; + + #[cfg(target_os = "macos")] + let window_builder = window_builder + .title_bar_style(tauri::TitleBarStyle::Overlay) + .hidden_title(true) + .traffic_light_position(tauri::LogicalPosition::new(12.0, 14.0)); + + window_builder +} + +pub fn run() { + let builder = tauri::Builder::default().manage(BridgeState::default()); + + #[cfg(not(target_os = "android"))] + let builder = builder.plugin(tauri_plugin_decorum::init()); + + // Desktop: 注册 OpenDirectoryState + single-instance 插件(需在 setup 之前) + #[cfg(not(target_os = "android"))] + let builder = + builder + .manage(OpenDirectoryState::default()) + .plugin(tauri_plugin_single_instance::init(|app, args, _cwd| { + // 始终新建窗口(类似 VSCode:双击图标 = 新窗口) + let dir = extract_directory_from_args(&args); + log::info!("Single-instance: opening new window, directory: {:?}", dir); + create_new_window(app, dir); + })); + + let builder = builder + .plugin(tauri_plugin_http::init()) + .plugin(tauri_plugin_notification::init()) + .plugin(tauri_plugin_dialog::init()) + .plugin(tauri_plugin_fs::init()) + .plugin(tauri_plugin_opener::init()) + .setup(|app| { + // 始终启用 log 插件,方便排查问题 + app.handle().plugin( + tauri_plugin_log::Builder::default() + .level(log::LevelFilter::Info) + .build(), + )?; + + #[cfg(not(target_os = "android"))] + { + let main_window = create_main_window(&app.handle())?; + finish_desktop_window_setup(&main_window); + restore_window_state(&main_window); + + #[cfg(debug_assertions)] + main_window.open_devtools(); + } + + #[cfg(target_os = "android")] + { + let _main_window = create_main_window(&app.handle())?; + } + + // Desktop: 解析 CLI 参数,存入 pending state + #[cfg(not(target_os = "android"))] + { + let args: Vec = std::env::args().collect(); + if let Some(dir) = extract_directory_from_args(&args) { + log::info!("CLI directory argument: {}", dir); + if let Some(state) = app.try_state::() { + state + .pending() + .pin() + .insert("main".to_string(), Arc::from(dir)); + } + } + } + + Ok(()) + }); + + // Desktop: 注册 service management commands + 窗口关闭拦截 + #[cfg(not(target_os = "android"))] + let builder = builder + .manage(service::ServiceState::default()) + .on_window_event(|window, event| { + match event { + tauri::WindowEvent::CloseRequested { api, .. } => { + save_window_state(window); + + // 只在最后一个窗口关闭时询问是否停止服务 + let is_last = window.app_handle().webview_windows().len() <= 1; + if is_last { + let state = window.state::(); + if state.we_started.load(Ordering::SeqCst) { + api.prevent_close(); + let _ = window.emit("close-requested", ()); + } + } + } + tauri::WindowEvent::Resized(_) => { + // macOS:仅在「退出全屏」时重新对齐红绿灯。 + // 普通缩放时 overlay 模式会自动把红绿灯锚定在左上角,无需干预; + // 若每帧都重算(setFrame 重设标题栏容器)反而会与 AppKit 的 resize + // 周期错相,导致拖拽卡顿。只有进出全屏时系统会重置位置。 + #[cfg(target_os = "macos")] + if let Some(webview) = window.get_webview_window(window.label()) { + let is_fs = webview.is_fullscreen().unwrap_or(false); + let was_fs = fullscreen_state() + .lock() + .ok() + .map(|mut m| { + m.insert(window.label().to_string(), is_fs).unwrap_or(false) + }) + .unwrap_or(false); + if was_fs && !is_fs { + reposition_traffic_lights(&webview); + } + } + } + tauri::WindowEvent::Destroyed => { + save_window_state(window); + + #[cfg(target_os = "macos")] + if let Ok(mut states) = fullscreen_state().lock() { + states.remove(window.label()); + } + + // 窗口销毁时清理该窗口的所有桥接连接 + let state = window.state::(); + state.disconnect_window(window.label()); + } + tauri::WindowEvent::DragDrop(event) => { + match event { + tauri::DragDropEvent::Enter { paths, position } => { + let paths: Vec = paths + .into_iter() + .map(|p| p.to_string_lossy().to_string()) + .collect(); + let _ = window.emit( + "file-drop-enter", + (paths, position.x, position.y), + ); + } + tauri::DragDropEvent::Over { position } => { + let _ = window.emit("file-drop-over", (position.x, position.y)); + } + tauri::DragDropEvent::Drop { paths, position } => { + let paths: Vec = paths + .into_iter() + .map(|p| p.to_string_lossy().to_string()) + .collect(); + let _ = window.emit( + "file-drop-drop", + (paths, position.x, position.y), + ); + } + tauri::DragDropEvent::Leave => { + let _ = window.emit("file-drop-leave", ()); + } + _ => {} + } + } + _ => {} + } + }) + .invoke_handler(tauri::generate_handler![ + commands::bridge::bridge_connect, + commands::bridge::bridge_send, + commands::bridge::bridge_disconnect, + commands::utils::get_cli_directory, + commands::utils::get_dropped_paths_info, + commands::utils::open_new_window, + commands::utils::desktop_window_ready, + commands::opencode::check_opencode_service, + commands::opencode::detect_opencode_binary, + commands::opencode::start_opencode_service, + commands::opencode::stop_opencode_service, + commands::opencode::get_service_started_by_us, + commands::opencode::confirm_close_app, + ]); + + // Android: 注册 bridge commands + #[cfg(target_os = "android")] + let builder = builder.invoke_handler(tauri::generate_handler![ + commands::bridge::bridge_connect, + commands::bridge::bridge_send, + commands::bridge::bridge_disconnect, + ]); + + // build + run 分开调用,以支持 macOS RunEvent::Opened + let app = builder + .build(tauri::generate_context!()) + .unwrap_or_else(|err| panic!("error while building tauri application: {err}")); + + app.run(|_app_handle, _event| { + // macOS: 处理 Finder "Open with" / 拖文件夹到 Dock 图标 + #[cfg(target_os = "macos")] + if let tauri::RunEvent::Opened { urls } = &_event { + for url in urls { + if let Ok(path) = url.to_file_path() { + if path.is_dir() { + let dir = path.to_string_lossy().to_string(); + log::info!("macOS Opened directory: {}", dir); + + // 如果只有 main 窗口且它还没消费目录,说明是冷启动,设给 main + // 否则新建窗口 + if let Some(state) = _app_handle.try_state::() { + let pending = state.pending().pin(); + let win_count = _app_handle.webview_windows().len(); + if win_count <= 1 && !pending.contains_key("main") { + pending.insert("main".to_string(), Arc::from(dir.clone())); + let _ = _app_handle.emit("open-directory", dir); + } else { + create_new_window(_app_handle, Some(dir)); + } + } + } + } + } + } + }); +} diff --git a/src-tauri/src/app/service.rs b/src-tauri/src/app/service.rs new file mode 100644 index 00000000..51f093d0 --- /dev/null +++ b/src-tauri/src/app/service.rs @@ -0,0 +1,24 @@ +use std::sync::{ + atomic::{AtomicBool, AtomicU32}, + Mutex, +}; + +/// 跟踪我们是否启动了 opencode serve 进程 +pub struct ServiceState { + /// 我们启动的子进程 PID + pub child_pid: AtomicU32, + /// 是否由我们启动(用于关闭时判断是否需要询问) + pub we_started: AtomicBool, + /// 我们启动的 opencode serve 实际地址 + pub service_url: Mutex>, +} + +impl Default for ServiceState { + fn default() -> Self { + Self { + child_pid: AtomicU32::new(0), + we_started: AtomicBool::new(false), + service_url: Mutex::new(None), + } + } +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs new file mode 100644 index 00000000..580fd30b --- /dev/null +++ b/src-tauri/src/lib.rs @@ -0,0 +1,6 @@ +pub mod app; + +#[cfg_attr(mobile, tauri::mobile_entry_point)] +pub fn run() { + app::run(); +} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs new file mode 100644 index 00000000..b43a4f21 --- /dev/null +++ b/src-tauri/src/main.rs @@ -0,0 +1,8 @@ +// Prevents additional console window on Windows in release, DO NOT REMOVE!! +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +mod app; + +fn main() { + app::run(); +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json new file mode 100644 index 00000000..3fe7b576 --- /dev/null +++ b/src-tauri/tauri.conf.json @@ -0,0 +1,49 @@ +{ + "$schema": "../node_modules/@tauri-apps/cli/config.schema.json", + "productName": "OpenCode", + "version": "0.6.23", + "identifier": "com.opencodeui.app", + "build": { + "frontendDist": "../dist", + "devUrl": "http://localhost:5173", + "beforeDevCommand": "npm run dev", + "beforeBuildCommand": "npm run build" + }, + "app": { + "withGlobalTauri": true, + "windows": [ + { + "label": "main", + "create": false, + "title": "OpenCode", + "width": 800, + "height": 600, + "titleBarStyle": "Overlay", + "hiddenTitle": true, + "trafficLightPosition": { + "x": 12, + "y": 14 + }, + "resizable": true, + "fullscreen": false, + "dragDropEnabled": true + } + ], + "security": { + "csp": "default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: http: https: asset: http://asset.localhost; font-src 'self' data:; connect-src 'self' ipc: http://ipc.localhost http: https: ws: wss:; media-src 'self' data: blob: http: https:; frame-src 'self' data: blob: http: https:; object-src 'none'; base-uri 'self'" + } + }, + "plugins": { + "notification": null + }, + "bundle": { + "active": true, + "targets": ["nsis", "dmg", "deb"], + "icon": ["icons/32x32.png", "icons/128x128.png", "icons/128x128@2x.png", "icons/icon.icns", "icons/icon.ico"], + "windows": { + "nsis": { + "installerHooks": "./windows/hooks.nsi" + } + } + } +} diff --git a/src-tauri/windows/hooks.nsi b/src-tauri/windows/hooks.nsi new file mode 100644 index 00000000..73f51205 --- /dev/null +++ b/src-tauri/windows/hooks.nsi @@ -0,0 +1,19 @@ +; OpenCodeUI - NSIS Installer Hooks +; 安装时注册 Windows 资源管理器右键菜单,卸载时清理 + +!macro NSIS_HOOK_POSTINSTALL + ; 右键文件夹 → "Open with OpenCode" + WriteRegStr HKCU "Software\Classes\Directory\shell\OpenCodeUI" "" "Open with OpenCode" + WriteRegStr HKCU "Software\Classes\Directory\shell\OpenCodeUI" "Icon" "$INSTDIR\${MAINBINARYNAME}.exe" + WriteRegStr HKCU "Software\Classes\Directory\shell\OpenCodeUI\command" "" '"$INSTDIR\${MAINBINARYNAME}.exe" "%V"' + + ; 右键文件夹空白处 → "Open with OpenCode" + WriteRegStr HKCU "Software\Classes\Directory\Background\shell\OpenCodeUI" "" "Open with OpenCode" + WriteRegStr HKCU "Software\Classes\Directory\Background\shell\OpenCodeUI" "Icon" "$INSTDIR\${MAINBINARYNAME}.exe" + WriteRegStr HKCU "Software\Classes\Directory\Background\shell\OpenCodeUI\command" "" '"$INSTDIR\${MAINBINARYNAME}.exe" "%V"' +!macroend + +!macro NSIS_HOOK_PREUNINSTALL + DeleteRegKey HKCU "Software\Classes\Directory\shell\OpenCodeUI" + DeleteRegKey HKCU "Software\Classes\Directory\Background\shell\OpenCodeUI" +!macroend diff --git a/src/App.tsx b/src/App.tsx index f86a6871..a62ad79e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,469 +1,1013 @@ -import { useRef, useEffect, useState, useCallback, useMemo } from 'react' -import { Header, InputBox, PermissionDialog, QuestionDialog, Sidebar, ChatArea, type ChatAreaHandle } from './features/chat' -import { type ModelSelectorHandle } from './features/chat/ModelSelector' -import { SettingsDialog } from './features/settings/SettingsDialog' -import { CommandPalette, type CommandItem } from './components/CommandPalette' +import { lazy, Suspense, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { invoke } from '@tauri-apps/api/core' +import { Sidebar } from './features/chat' +import { ChatPane } from './features/chat/ChatPane' +import { SplitContainer } from './features/chat/SplitContainer' +import type { CommandItem } from './components/CommandPalette' +import { ToastContainer } from './components/ToastContainer' import { RightPanel } from './components/RightPanel' import { BottomPanel } from './components/BottomPanel' -import { useTheme, useModels, useModelSelection, useChatSession, useGlobalKeybindings } from './hooks' +import { DesktopTitlebar } from './components/DesktopTitlebar' +import { useDirectory, useGlobalEvents, useGlobalKeybindings, useRouter } from './hooks' +import { useViewportHeight } from './hooks/useViewportHeight' +import { useCloseServiceDialog } from './hooks/useCloseServiceDialog' +import { useWakeLock } from './hooks/useWakeLock' import type { KeybindingHandlers } from './hooks/useKeybindings' import { keybindingStore } from './store/keybindingStore' -import { layoutStore } from './store/layoutStore' -import { STORAGE_KEY_WIDE_MODE } from './constants' -import { restoreModelSelection } from './utils/sessionHelpers' -import type { Attachment } from './api' +import { + layoutStore, + paneLayoutStore, + useLayoutStore, + usePaneController, + usePaneControllers, + usePaneLayout, + updateStore, +} from './store' +import { + ChatViewportProvider, + CHAT_SURFACE_MIN_WIDTH, + canUseSplitPane, + useChatViewportController, +} from './features/chat/chatViewport' +import { uiErrorHandler, isSameDirectory, collectActiveDirectories } from './utils' +import { initNotificationSound } from './utils/notificationSoundBridge' import { createPtySession } from './api/pty' import type { TerminalTab } from './store/layoutStore' +import type { SettingsTab } from './features/settings/SettingsDialog' +import { isTauri, isTauriMobile } from './utils/tauri' +import { InternalDragLayer } from './components/InternalDragLayer' + +const SettingsDialog = lazy(() => + import('./features/settings/SettingsDialog').then(module => ({ default: module.SettingsDialog })), +) +const CommandPalette = lazy(() => + import('./components/CommandPalette').then(module => ({ default: module.CommandPalette })), +) +const CloseServiceDialog = lazy(() => + import('./components/CloseServiceDialog').then(module => ({ default: module.CloseServiceDialog })), +) + +const MOBILE_PAGER_SCROLL_END_MS = 120 +const MOBILE_RIGHT_PANEL_UNMOUNT_MS = 420 + +type MobilePagerPage = 'left' | 'chat' | 'right' function App() { - // ============================================ - // Refs - // ============================================ - const chatAreaRef = useRef(null) - const modelSelectorRef = useRef(null) - const lastEscTimeRef = useRef(0) - const escHintTimerRef = useRef | null>(null) - - // ============================================ - // Cancel Hint (double-Esc to abort) - // ============================================ - const [showCancelHint, setShowCancelHint] = useState(false) - - // ============================================ - // Theme - // ============================================ - const { - mode: themeMode, setThemeWithAnimation, - presetId, setPresetWithAnimation, availablePresets, - customCSS, setCustomCSS, - } = useTheme() - - // ============================================ - // Models - // ============================================ - const { models, isLoading: modelsLoading } = useModels() + const { t } = useTranslation(['commands', 'chat', 'common', 'components']) + const router = useRouter() const { - selectedModelKey, - selectedVariant, - currentModel, - handleModelChange, - handleVariantChange, - restoreFromMessage, - } = useModelSelection({ models }) - - // ============================================ - // Wide Mode - // ============================================ - const [isWideMode, setIsWideMode] = useState(() => { - return localStorage.getItem(STORAGE_KEY_WIDE_MODE) === 'true' + sessionId: routeSessionId, + directory: routeDirectory, + navigateToSession: navigateRouteToSession, + navigateHome: navigateRouteHome, + replaceSession, + } = router + const { currentDirectory, savedDirectories, sidebarExpanded, setSidebarExpanded } = useDirectory() + const { rightPanelOpen, rightPanelWidth, wakeLock } = useLayoutStore() + const { surfaceRef, value: chatViewport } = useChatViewportController({ + sidebarExpanded, + rightPanelOpen, + requestedRightPanelWidth: rightPanelWidth, }) + const splitPaneEnabled = canUseSplitPane(chatViewport) + const paneLayout = usePaneLayout() + const focusedController = usePaneController(paneLayout.focusedPaneId) + const paneControllers = usePaneControllers() + const syncingFromRouteRef = useRef(false) + const lastRouteSessionIdRef = useRef(undefined) + // 当 currentDirectory 为 undefined 时表示全局模式, + // 不应 fallback 到 session 自身的 directory,否则 replaceSession 会把 dir 参数写回 URL + const focusedRouteDirectory = + currentDirectory !== undefined + ? paneLayout.focusedSessionId === routeSessionId + ? routeDirectory || focusedController?.effectiveDirectory || currentDirectory + : focusedController?.effectiveDirectory || currentDirectory + : undefined + + useEffect(() => { + const cleanup = initNotificationSound() + return cleanup + }, []) - const toggleWideMode = useCallback(() => { - setIsWideMode(prev => { - const next = !prev - localStorage.setItem(STORAGE_KEY_WIDE_MODE, String(next)) - return next + useEffect(() => { + if (!isTauri() || isTauriMobile()) return + + void invoke('desktop_window_ready').catch(() => { + // best effort only }) }, []) - // ============================================ - // Settings Dialog - // ============================================ + useEffect(() => { + if (import.meta.env.DEV) return + void updateStore.checkForUpdates() + }, []) + + useViewportHeight() + useWakeLock(wakeLock) + + const activeDirectories = useMemo( + () => + collectActiveDirectories({ + routeDirectory, + currentDirectory, + paneDirectories: paneControllers + .map(controller => controller.effectiveDirectory) + .filter((directory): directory is string => Boolean(directory)), + projectDirectories: (Array.isArray(savedDirectories) ? savedDirectories : []).map(directory => directory.path), + }), + [routeDirectory, currentDirectory, paneControllers, savedDirectories], + ) + + // 全局唯一 SSE 连接。所有 pane 通过 consumer 机制接收自己的 session 事件。 + useGlobalEvents(activeDirectories) + + // URL -> focused pane session + useEffect(() => { + if (lastRouteSessionIdRef.current === routeSessionId) return + lastRouteSessionIdRef.current = routeSessionId + if (paneLayoutStore.getFocusedSessionId() === routeSessionId) return + syncingFromRouteRef.current = true + paneLayoutStore.setFocusedSession(routeSessionId) + }, [routeSessionId]) + + // focused pane session -> URL(路由只反映当前 focused pane) + useEffect(() => { + if (syncingFromRouteRef.current) { + syncingFromRouteRef.current = false + return + } + if (paneLayoutStore.getFocusedSessionId() !== paneLayout.focusedSessionId) return + if (paneLayout.focusedSessionId === routeSessionId && isSameDirectory(routeDirectory, focusedRouteDirectory)) return + replaceSession(paneLayout.focusedSessionId, focusedRouteDirectory) + }, [ + paneLayout.focusedPaneId, + paneLayout.focusedSessionId, + routeSessionId, + routeDirectory, + replaceSession, + focusedRouteDirectory, + ]) + + const navigatePaneToSession = useCallback( + (paneId: string, sessionId: string, directory?: string) => { + paneLayoutStore.focusPane(paneId) + paneLayoutStore.setPaneSession(paneId, sessionId) + navigateRouteToSession(sessionId, directory) + }, + [navigateRouteToSession], + ) + + const navigatePaneHome = useCallback( + (paneId: string) => { + paneLayoutStore.focusPane(paneId) + paneLayoutStore.setPaneSession(paneId, null) + navigateRouteHome() + }, + [navigateRouteHome], + ) + + const handleSelectSession = useCallback( + (session: { id: string; directory?: string }) => { + const paneId = paneLayout.focusedPaneId ?? paneLayoutStore.getFocusedPaneId() + if (!paneId) return + navigatePaneToSession(paneId, session.id, session.directory) + }, + [paneLayout.focusedPaneId, navigatePaneToSession], + ) + + const handleNewSession = useCallback(() => { + const paneId = paneLayout.focusedPaneId ?? paneLayoutStore.getFocusedPaneId() + if (!paneId) return + navigatePaneHome(paneId) + }, [paneLayout.focusedPaneId, navigatePaneHome]) + + const handleEnterSplitMode = useCallback(() => { + paneLayoutStore.enterSplitMode(paneLayout.focusedSessionId) + }, [paneLayout.focusedSessionId]) + + const handleToggleFocusedPaneFullscreen = useCallback(() => { + const paneId = paneLayout.focusedPaneId ?? paneLayoutStore.getFocusedPaneId() + if (!paneId) return + paneLayoutStore.togglePaneFullscreen(paneId) + }, [paneLayout.focusedPaneId]) + + const isMobilePanelLayout = chatViewport.interaction.sidebarBehavior === 'overlay' + const mobileLeftPanelWidth = chatViewport.layout.sidebar.overlayWidth + const mobilePageWidth = Math.max(1, chatViewport.layout.viewportWidth) + const mobileChatScrollLeft = mobileLeftPanelWidth + const mobileRightScrollLeft = mobileLeftPanelWidth + mobilePageWidth + const mobilePagerRef = useRef(null) + const mobilePagerInitializedRef = useRef(false) + const mobilePagerInteractingRef = useRef(false) + const mobileProgrammaticTargetRef = useRef(null) + const mobileScrollEndTimerRef = useRef(null) + const mobileRightUnmountTimerRef = useRef(null) + const shouldRenderMobileRightPanelRef = useRef(false) + const [shouldRenderMobileRightPanel, setShouldRenderMobileRightPanel] = useState(false) + + const setMobileRightPanelRendered = useCallback((rendered: boolean) => { + if (shouldRenderMobileRightPanelRef.current === rendered) return + shouldRenderMobileRightPanelRef.current = rendered + setShouldRenderMobileRightPanel(rendered) + }, []) + + const clearMobileRightUnmountTimer = useCallback(() => { + if (mobileRightUnmountTimerRef.current === null) return + window.clearTimeout(mobileRightUnmountTimerRef.current) + mobileRightUnmountTimerRef.current = null + }, []) + + const ensureMobileRightPanelRendered = useCallback(() => { + clearMobileRightUnmountTimer() + setMobileRightPanelRendered(true) + }, [clearMobileRightUnmountTimer, setMobileRightPanelRendered]) + + const mobileActivePage: MobilePagerPage = rightPanelOpen ? 'right' : sidebarExpanded ? 'left' : 'chat' + + const getMobilePageScrollLeft = useCallback( + (page: MobilePagerPage) => (page === 'left' ? 0 : page === 'right' ? mobileRightScrollLeft : mobileChatScrollLeft), + [mobileChatScrollLeft, mobileRightScrollLeft], + ) + + const scrollMobilePagerTo = useCallback( + (page: MobilePagerPage, behavior: ScrollBehavior = 'smooth') => { + const pager = mobilePagerRef.current + if (!pager) return + + const left = getMobilePageScrollLeft(page) + if (Math.abs(pager.scrollLeft - left) < 1) { + mobileProgrammaticTargetRef.current = null + pager.scrollTo({ left, behavior: 'auto' }) + return + } + + mobileProgrammaticTargetRef.current = behavior === 'smooth' ? page : null + pager.scrollTo({ left, behavior }) + }, + [getMobilePageScrollLeft], + ) + + const getNearestMobilePage = useCallback( + (scrollLeft: number): MobilePagerPage => { + const leftDistance = Math.abs(scrollLeft) + const chatDistance = Math.abs(scrollLeft - mobileChatScrollLeft) + const rightDistance = Math.abs(scrollLeft - mobileRightScrollLeft) + + if (leftDistance <= chatDistance && leftDistance <= rightDistance) return 'left' + if (rightDistance <= chatDistance) return 'right' + return 'chat' + }, + [mobileChatScrollLeft, mobileRightScrollLeft], + ) + + const syncMobilePagerState = useCallback(() => { + const pager = mobilePagerRef.current + if (!pager) return + + const page = getNearestMobilePage(pager.scrollLeft) + if (page === 'left') { + if (!sidebarExpanded) setSidebarExpanded(true) + if (rightPanelOpen) layoutStore.closeRightPanel() + return + } + + if (page === 'right') { + ensureMobileRightPanelRendered() + if (sidebarExpanded) setSidebarExpanded(false) + if (!rightPanelOpen) layoutStore.openRightPanel() + return + } + + if (sidebarExpanded) setSidebarExpanded(false) + if (rightPanelOpen) layoutStore.closeRightPanel() + }, [ensureMobileRightPanelRendered, getNearestMobilePage, rightPanelOpen, setSidebarExpanded, sidebarExpanded]) + + const handleMobilePagerScroll = useCallback(() => { + const pager = mobilePagerRef.current + if (!pager) return + + const scrollLeft = pager.scrollLeft + + // -1 (滑向左栏) 到 0 (对话页) 到 1 (滑向右栏) + const rawProgress = (scrollLeft - mobileChatScrollLeft) / (scrollLeft < mobileChatScrollLeft ? mobileLeftPanelWidth : mobilePageWidth) + const progress = Math.max(-1, Math.min(1, rawProgress)) + const absProgress = Math.abs(progress) + const rightProgress = Math.max(0, progress) + const easedRightProgress = rightProgress * rightProgress + const originX = 50 - progress * 50 + + pager.style.setProperty('--mobile-chat-rotate-y', `${progress * 10}deg`) + pager.style.setProperty('--mobile-chat-scale', `${1 - absProgress * 0.06}`) + pager.style.setProperty('--mobile-chat-offset-x', `${easedRightProgress * -48}px`) + pager.style.setProperty('--mobile-chat-transform-origin', `${originX}% 50%`) + + if (scrollLeft > mobileChatScrollLeft + 24) { + ensureMobileRightPanelRendered() + } + + if (mobileScrollEndTimerRef.current !== null) { + window.clearTimeout(mobileScrollEndTimerRef.current) + } + + mobileScrollEndTimerRef.current = window.setTimeout(() => { + mobileScrollEndTimerRef.current = null + if (mobilePagerInteractingRef.current) return + + if (mobileProgrammaticTargetRef.current) { + const targetLeft = getMobilePageScrollLeft(mobileProgrammaticTargetRef.current) + if (Math.abs(pager.scrollLeft - targetLeft) >= 2) return + mobileProgrammaticTargetRef.current = null + } + + syncMobilePagerState() + }, MOBILE_PAGER_SCROLL_END_MS) + }, [ + ensureMobileRightPanelRendered, + getMobilePageScrollLeft, + mobileChatScrollLeft, + mobileLeftPanelWidth, + mobilePageWidth, + syncMobilePagerState, + ]) + + const handleMobilePagerInteractionStart = useCallback(() => { + mobilePagerInteractingRef.current = true + mobileProgrammaticTargetRef.current = null + }, []) + + const handleMobilePagerInteractionEnd = useCallback(() => { + mobilePagerInteractingRef.current = false + + if (mobileScrollEndTimerRef.current !== null) { + window.clearTimeout(mobileScrollEndTimerRef.current) + } + + mobileScrollEndTimerRef.current = window.setTimeout(() => { + mobileScrollEndTimerRef.current = null + syncMobilePagerState() + }, MOBILE_PAGER_SCROLL_END_MS) + }, [syncMobilePagerState]) + + useLayoutEffect(() => { + if (!isMobilePanelLayout) { + mobilePagerInitializedRef.current = false + return + } + + const isInitializing = !mobilePagerInitializedRef.current + const page = rightPanelOpen ? 'right' : isInitializing ? 'chat' : sidebarExpanded ? 'left' : 'chat' + if (!mobilePagerInitializedRef.current) { + const pager = mobilePagerRef.current + if (pager) { + pager.scrollLeft = getMobilePageScrollLeft(page) + } + mobileProgrammaticTargetRef.current = null + mobilePagerInitializedRef.current = true + if (!rightPanelOpen && sidebarExpanded) { + setSidebarExpanded(false) + } + return + } + + const frameId = window.requestAnimationFrame(() => { + scrollMobilePagerTo(page, 'smooth') + }) + return () => window.cancelAnimationFrame(frameId) + }, [getMobilePageScrollLeft, isMobilePanelLayout, rightPanelOpen, scrollMobilePagerTo, setSidebarExpanded, sidebarExpanded]) + + useEffect(() => { + if (!isMobilePanelLayout) { + clearMobileRightUnmountTimer() + const frameId = window.requestAnimationFrame(() => setMobileRightPanelRendered(false)) + return () => window.cancelAnimationFrame(frameId) + } + + if (rightPanelOpen) { + clearMobileRightUnmountTimer() + const frameId = window.requestAnimationFrame(() => setMobileRightPanelRendered(true)) + return () => window.cancelAnimationFrame(frameId) + } + + clearMobileRightUnmountTimer() + mobileRightUnmountTimerRef.current = window.setTimeout(() => { + setMobileRightPanelRendered(false) + mobileRightUnmountTimerRef.current = null + }, MOBILE_RIGHT_PANEL_UNMOUNT_MS) + + return clearMobileRightUnmountTimer + }, [clearMobileRightUnmountTimer, isMobilePanelLayout, rightPanelOpen, setMobileRightPanelRendered]) + + useEffect(() => { + if (!isMobilePanelLayout || !rightPanelOpen || !sidebarExpanded) return + + const frameId = window.requestAnimationFrame(() => setSidebarExpanded(false)) + return () => window.cancelAnimationFrame(frameId) + }, [isMobilePanelLayout, rightPanelOpen, setSidebarExpanded, sidebarExpanded]) + + useEffect(() => { + return () => { + if (mobileScrollEndTimerRef.current !== null) window.clearTimeout(mobileScrollEndTimerRef.current) + if (mobileRightUnmountTimerRef.current !== null) window.clearTimeout(mobileRightUnmountTimerRef.current) + mobileProgrammaticTargetRef.current = null + } + }, []) + + const handleOpenSidebar = useCallback(() => { + if (isMobilePanelLayout && rightPanelOpen) { + layoutStore.closeRightPanel() + } + if (isMobilePanelLayout) { + scrollMobilePagerTo('left') + } + setSidebarExpanded(true) + }, [isMobilePanelLayout, rightPanelOpen, scrollMobilePagerTo, setSidebarExpanded]) + + const handleCloseSidebar = useCallback(() => { + if (isMobilePanelLayout) { + scrollMobilePagerTo('chat') + } + setSidebarExpanded(false) + }, [isMobilePanelLayout, scrollMobilePagerTo, setSidebarExpanded]) + + const handleToggleSidebar = useCallback(() => { + if (sidebarExpanded) { + handleCloseSidebar() + } else { + handleOpenSidebar() + } + }, [handleCloseSidebar, handleOpenSidebar, sidebarExpanded]) + + const handleToggleRightPanel = useCallback(() => { + if (!isMobilePanelLayout) { + layoutStore.toggleRightPanel() + return + } + + if (rightPanelOpen) { + scrollMobilePagerTo('chat') + layoutStore.closeRightPanel() + return + } + + ensureMobileRightPanelRendered() + if (sidebarExpanded) setSidebarExpanded(false) + scrollMobilePagerTo('right') + layoutStore.openRightPanel() + }, [ensureMobileRightPanelRendered, isMobilePanelLayout, rightPanelOpen, scrollMobilePagerTo, setSidebarExpanded, sidebarExpanded]) + + const focusedDirectory = focusedRouteDirectory || '' + const [settingsDialogOpen, setSettingsDialogOpen] = useState(false) - const [settingsInitialTab, setSettingsInitialTab] = useState<'general' | 'keybindings'>('general') - const openSettings = useCallback(() => { setSettingsInitialTab('general'); setSettingsDialogOpen(true) }, []) + const [settingsInitialTab, setSettingsInitialTab] = useState('servers') + const openSettingsTab = useCallback((tab: SettingsTab) => { + setSettingsInitialTab(tab) + setSettingsDialogOpen(true) + }, []) + const openSettings = useCallback(() => { + openSettingsTab('servers') + }, [openSettingsTab]) + const openAboutSettings = useCallback(() => { + openSettingsTab('about') + }, [openSettingsTab]) const closeSettings = useCallback(() => setSettingsDialogOpen(false), []) - // ============================================ - // Project Dialog (triggered externally via keybinding) - // ============================================ + const renderPaneLeaf = useCallback( + (paneId: string, paneSessionId: string | null) => ( + + ), + [ + paneLayout.focusedPaneId, + paneLayout.paneCount, + paneLayout.isSplit, + paneLayout.fullscreenPaneId, + chatViewport.interaction.sidebarBehavior, + splitPaneEnabled, + handleOpenSidebar, + handleToggleRightPanel, + handleEnterSplitMode, + handleToggleFocusedPaneFullscreen, + openSettings, + navigatePaneToSession, + navigatePaneHome, + ], + ) + const [projectDialogOpen, setProjectDialogOpen] = useState(false) const openProject = useCallback(() => setProjectDialogOpen(true), []) const closeProjectDialog = useCallback(() => setProjectDialogOpen(false), []) - // ============================================ - // Command Palette - // ============================================ - const [commandPaletteOpen, setCommandPaletteOpen] = useState(false) - - // ============================================ - // Chat Session - // ============================================ - const { - // State - messages, - isStreaming, - prependedCount, - canUndo, - canRedo, - redoSteps, - revertedContent, - agents, - selectedAgent, - setSelectedAgent, - routeSessionId, - sidebarExpanded, - setSidebarExpanded, - effectiveDirectory, - - // Permissions - pendingPermissionRequests, - pendingQuestionRequests, - handlePermissionReply, - handleQuestionReply, - handleQuestionReject, - isReplying, - - // Session management - loadMoreHistory, - handleRedoAll, - clearRevert, - - // Animation - registerMessage, - registerInputBox, - - // Handlers - handleSend, - handleAbort, - handleCommand, - handleUndoWithAnimation, - handleRedoWithAnimation, - handleSelectSession, - handleNewSession, - handleVisibleMessageIdsChange, - handleArchiveSession, - handlePreviousSession, - handleNextSession, - handleToggleAgent, - handleCopyLastResponse, - } = useChatSession({ chatAreaRef, currentModel }) - - - // ============================================ - // Model Restoration Effect - // ============================================ + // 桌面标题栏通过 CustomEvent 触发打开项目/设置 useEffect(() => { - // 1. 优先从 revertedContent 恢复(Undo/Redo 场景) - if (revertedContent?.model) { - const modelSelection = restoreModelSelection( - revertedContent.model, - revertedContent.variant ?? null, - models - ) - if (modelSelection) { - restoreFromMessage(revertedContent.model, revertedContent.variant) - return - } + const onOpenProject = () => openProject() + const onOpenSettings = () => openSettings() + window.addEventListener('titlebar:open-project', onOpenProject) + window.addEventListener('titlebar:open-settings', onOpenSettings) + return () => { + window.removeEventListener('titlebar:open-project', onOpenProject) + window.removeEventListener('titlebar:open-settings', onOpenSettings) } + }, [openProject, openSettings]) - // 2. 其次从历史消息恢复 - if (messages.length === 0) return - - const lastUserMsg = [...messages].reverse().find(m => m.info.role === 'user') - if (lastUserMsg && 'model' in lastUserMsg.info) { - const userInfo = lastUserMsg.info as { model?: { providerID: string; modelID: string }; variant?: string } - restoreFromMessage(userInfo.model, userInfo.variant) - } - }, [messages, models, revertedContent, restoreFromMessage]) + const [commandPaletteOpen, setCommandPaletteOpen] = useState(false) - // ============================================ - // Global Keybindings - // ============================================ - - // Create new terminal handler const handleNewTerminal = useCallback(async () => { try { - const pty = await createPtySession({ cwd: effectiveDirectory }, effectiveDirectory) + const pty = await createPtySession({ cwd: focusedDirectory }, focusedDirectory) const tab: TerminalTab = { id: pty.id, - title: pty.title || 'Terminal', + title: pty.title || t('components:terminal.terminal'), status: 'connecting', } layoutStore.addTerminalTab(tab, true) } catch (error) { - console.error('[App] Failed to create terminal:', error) + uiErrorHandler('create terminal', error) } - }, [effectiveDirectory]) - - const keybindingHandlers = useMemo(() => ({ - // General - openSettings, - openProject, - commandPalette: () => setCommandPaletteOpen(true), - toggleSidebar: () => setSidebarExpanded(!sidebarExpanded), - toggleRightPanel: () => layoutStore.toggleRightPanel(), - focusInput: () => { - const input = document.querySelector('[data-input-box] textarea') - input?.focus() - }, - - // Session - newSession: handleNewSession, - archiveSession: handleArchiveSession, - previousSession: handlePreviousSession, - nextSession: handleNextSession, - - // Terminal - toggleTerminal: () => layoutStore.toggleBottomPanel(), - newTerminal: handleNewTerminal, - - // Model - selectModel: () => modelSelectorRef.current?.openMenu(), - toggleAgent: handleToggleAgent, - - // Message - cancelMessage: () => { - if (!isStreaming) return - - const now = Date.now() - const elapsed = now - lastEscTimeRef.current - - if (elapsed < 600) { - // 双击确认 → 真正取消 - lastEscTimeRef.current = 0 - setShowCancelHint(false) - if (escHintTimerRef.current) clearTimeout(escHintTimerRef.current) - handleAbort() - } else { - // 第一次按 → 显示提示 - lastEscTimeRef.current = now - setShowCancelHint(true) - if (escHintTimerRef.current) clearTimeout(escHintTimerRef.current) - escHintTimerRef.current = setTimeout(() => { - setShowCancelHint(false) - lastEscTimeRef.current = 0 - }, 1500) - } - }, - copyLastResponse: handleCopyLastResponse, - }), [ - openSettings, - openProject, - sidebarExpanded, - setSidebarExpanded, - handleNewSession, - handleArchiveSession, - handlePreviousSession, - handleNextSession, - handleNewTerminal, - handleToggleAgent, - isStreaming, - handleAbort, - handleCopyLastResponse, - ]) + }, [focusedDirectory, t]) + + const keybindingHandlers = useMemo( + () => ({ + openSettings, + openProject, + commandPalette: () => setCommandPaletteOpen(true), + toggleSidebar: handleToggleSidebar, + toggleRightPanel: handleToggleRightPanel, + focusInput: () => { + const input = document.querySelector('[data-input-box] textarea') + input?.focus() + }, + newSession: () => focusedController?.newSession(), + archiveSession: () => focusedController?.archiveSession(), + previousSession: () => focusedController?.previousSession(), + nextSession: () => focusedController?.nextSession(), + toggleTerminal: () => layoutStore.toggleBottomPanel(), + newTerminal: handleNewTerminal, + selectModel: () => focusedController?.openModelSelector(), + toggleAgent: () => focusedController?.toggleAgent(), + cancelMessage: () => focusedController?.cancelMessage(), + copyLastResponse: () => focusedController?.copyLastResponse(), + toggleFullAuto: () => focusedController?.toggleFullAuto(), + // Pane + focusNextPane: () => { + paneLayoutStore.focusNextPane() + requestAnimationFrame(() => { + const pid = paneLayoutStore.getFocusedPaneId() + if (pid) { + const input = document.querySelector(`[data-pane-id="${pid}"] textarea`) + input?.focus() + } + }) + }, + focusPrevPane: () => { + paneLayoutStore.focusPrevPane() + requestAnimationFrame(() => { + const pid = paneLayoutStore.getFocusedPaneId() + if (pid) { + const input = document.querySelector(`[data-pane-id="${pid}"] textarea`) + input?.focus() + } + }) + }, + splitRight: () => { + const pid = paneLayout.focusedPaneId ?? paneLayoutStore.getFocusedPaneId() + if (pid && splitPaneEnabled) paneLayoutStore.splitPane(pid, 'horizontal') + }, + splitDown: () => { + const pid = paneLayout.focusedPaneId ?? paneLayoutStore.getFocusedPaneId() + if (pid && splitPaneEnabled) paneLayoutStore.splitPane(pid, 'vertical') + }, + closePane: () => { + const pid = paneLayout.focusedPaneId ?? paneLayoutStore.getFocusedPaneId() + if (pid && paneLayout.isSplit) paneLayoutStore.closePane(pid) + }, + togglePaneFullscreen: () => { + if (paneLayout.isSplit) handleToggleFocusedPaneFullscreen() + }, + }), + [ + openSettings, + openProject, + focusedController, + handleToggleSidebar, + handleToggleRightPanel, + handleNewTerminal, + paneLayout.focusedPaneId, + paneLayout.isSplit, + splitPaneEnabled, + handleToggleFocusedPaneFullscreen, + ], + ) useGlobalKeybindings(keybindingHandlers) - // ============================================ - // Command Palette - Commands List - // ============================================ const commands = useMemo(() => { - const getShortcut = (action: string) => keybindingStore.getKey(action as import('./store/keybindingStore').KeybindingAction) + const getShortcut = (action: string) => + keybindingStore.getKey(action as import('./store/keybindingStore').KeybindingAction) return [ - // General - { id: 'openSettings', label: 'Open Settings', description: 'Open settings dialog', category: 'General', shortcut: getShortcut('openSettings'), action: openSettings }, - { id: 'openProject', label: 'Open Project', description: 'Open project selector', category: 'General', shortcut: getShortcut('openProject'), action: openProject }, - { id: 'openSettingsShortcuts', label: 'Open Shortcuts Settings', description: 'Open settings to shortcuts tab', category: 'General', action: () => { setSettingsInitialTab('keybindings'); setSettingsDialogOpen(true) } }, - { id: 'toggleSidebar', label: 'Toggle Sidebar', description: 'Show or hide sidebar', category: 'General', shortcut: getShortcut('toggleSidebar'), action: () => setSidebarExpanded(!sidebarExpanded) }, - { id: 'toggleRightPanel', label: 'Toggle Right Panel', description: 'Show or hide right panel', category: 'General', shortcut: getShortcut('toggleRightPanel'), action: () => layoutStore.toggleRightPanel() }, - { id: 'focusInput', label: 'Focus Input', description: 'Focus message input', category: 'General', shortcut: getShortcut('focusInput'), action: () => { const input = document.querySelector('[data-input-box] textarea'); input?.focus() } }, - - // Session - { id: 'newSession', label: 'New Session', description: 'Create new chat session', category: 'Session', shortcut: getShortcut('newSession'), action: handleNewSession }, - { id: 'archiveSession', label: 'Archive Session', description: 'Archive current session', category: 'Session', shortcut: getShortcut('archiveSession'), action: handleArchiveSession }, - { id: 'previousSession', label: 'Previous Session', description: 'Switch to previous session', category: 'Session', shortcut: getShortcut('previousSession'), action: handlePreviousSession }, - { id: 'nextSession', label: 'Next Session', description: 'Switch to next session', category: 'Session', shortcut: getShortcut('nextSession'), action: handleNextSession }, - - // Terminal - { id: 'toggleTerminal', label: 'Toggle Terminal', description: 'Show or hide terminal panel', category: 'Terminal', shortcut: getShortcut('toggleTerminal'), action: () => layoutStore.toggleBottomPanel() }, - { id: 'newTerminal', label: 'New Terminal', description: 'Open new terminal tab', category: 'Terminal', shortcut: getShortcut('newTerminal'), action: handleNewTerminal }, - - // Model - { id: 'selectModel', label: 'Select Model', description: 'Open model selector', category: 'Model', shortcut: getShortcut('selectModel'), action: () => modelSelectorRef.current?.openMenu() }, - { id: 'toggleAgent', label: 'Toggle Agent', description: 'Switch agent mode', category: 'Model', shortcut: getShortcut('toggleAgent'), action: handleToggleAgent }, - - // Message - { id: 'copyLastResponse', label: 'Copy Last Response', description: 'Copy last AI response to clipboard', category: 'Message', shortcut: getShortcut('copyLastResponse'), action: handleCopyLastResponse }, - { id: 'cancelMessage', label: 'Cancel Message', description: 'Cancel current response', category: 'Message', shortcut: getShortcut('cancelMessage'), action: () => { if (isStreaming) handleAbort() }, when: () => isStreaming }, + { + id: 'openSettings', + label: t('commands:openSettings'), + description: t('commands:openSettingsDesc'), + category: t('commands:categories.general'), + shortcut: getShortcut('openSettings'), + action: openSettings, + }, + { + id: 'openProject', + label: t('commands:openProject'), + description: t('commands:openProjectDesc'), + category: t('commands:categories.general'), + shortcut: getShortcut('openProject'), + action: openProject, + }, + { + id: 'openSettingsShortcuts', + label: t('commands:openShortcutsSettings'), + description: t('commands:openShortcutsSettingsDesc'), + category: t('commands:categories.general'), + action: () => { + openSettingsTab('keybindings') + }, + }, + { + id: 'toggleSidebar', + label: t('commands:toggleSidebar'), + description: t('commands:toggleSidebarDesc'), + category: t('commands:categories.general'), + shortcut: getShortcut('toggleSidebar'), + action: handleToggleSidebar, + }, + { + id: 'toggleRightPanel', + label: t('commands:toggleRightPanel'), + description: t('commands:toggleRightPanelDesc'), + category: t('commands:categories.general'), + shortcut: getShortcut('toggleRightPanel'), + action: handleToggleRightPanel, + }, + { + id: 'focusInput', + label: t('commands:focusInput'), + description: t('commands:focusInputDesc'), + category: t('commands:categories.general'), + shortcut: getShortcut('focusInput'), + action: () => { + const input = document.querySelector('[data-input-box] textarea') + input?.focus() + }, + }, + { + id: 'newSession', + label: t('commands:newSession'), + description: t('commands:newSessionDesc'), + category: t('commands:categories.session'), + shortcut: getShortcut('newSession'), + action: () => focusedController?.newSession(), + }, + { + id: 'archiveSession', + label: t('commands:archiveSession'), + description: t('commands:archiveSessionDesc'), + category: t('commands:categories.session'), + shortcut: getShortcut('archiveSession'), + action: () => focusedController?.archiveSession(), + }, + { + id: 'previousSession', + label: t('commands:previousSession'), + description: t('commands:previousSessionDesc'), + category: t('commands:categories.session'), + shortcut: getShortcut('previousSession'), + action: () => focusedController?.previousSession(), + }, + { + id: 'nextSession', + label: t('commands:nextSession'), + description: t('commands:nextSessionDesc'), + category: t('commands:categories.session'), + shortcut: getShortcut('nextSession'), + action: () => focusedController?.nextSession(), + }, + { + id: 'toggleTerminal', + label: t('commands:toggleTerminal'), + description: t('commands:toggleTerminalDesc'), + category: t('commands:categories.terminal'), + shortcut: getShortcut('toggleTerminal'), + action: () => layoutStore.toggleBottomPanel(), + }, + { + id: 'newTerminal', + label: t('commands:newTerminal'), + description: t('commands:newTerminalDesc'), + category: t('commands:categories.terminal'), + shortcut: getShortcut('newTerminal'), + action: handleNewTerminal, + }, + { + id: 'selectModel', + label: t('commands:selectModel'), + description: t('commands:selectModelDesc'), + category: t('commands:categories.model'), + shortcut: getShortcut('selectModel'), + action: () => focusedController?.openModelSelector(), + }, + { + id: 'toggleAgent', + label: t('commands:toggleAgent'), + description: t('commands:toggleAgentDesc'), + category: t('commands:categories.model'), + shortcut: getShortcut('toggleAgent'), + action: () => focusedController?.toggleAgent(), + }, + { + id: 'copyLastResponse', + label: t('commands:copyLastResponse'), + description: t('commands:copyLastResponseDesc'), + category: t('commands:categories.message'), + shortcut: getShortcut('copyLastResponse'), + action: () => focusedController?.copyLastResponse(), + }, + { + id: 'cancelMessage', + label: t('commands:cancelMessage'), + description: t('commands:cancelMessageDesc'), + category: t('commands:categories.message'), + shortcut: getShortcut('cancelMessage'), + action: () => focusedController?.cancelMessage(), + when: () => !!focusedController?.isStreaming, + }, + // Pane + { + id: 'focusNextPane', + label: t('commands:focusNextPane'), + description: t('commands:focusNextPaneDesc'), + category: t('commands:categories.pane'), + shortcut: getShortcut('focusNextPane'), + action: () => paneLayoutStore.focusNextPane(), + }, + { + id: 'focusPrevPane', + label: t('commands:focusPrevPane'), + description: t('commands:focusPrevPaneDesc'), + category: t('commands:categories.pane'), + shortcut: getShortcut('focusPrevPane'), + action: () => paneLayoutStore.focusPrevPane(), + }, + { + id: 'splitRight', + label: t('commands:splitRight'), + description: t('commands:splitRightDesc'), + category: t('commands:categories.pane'), + shortcut: getShortcut('splitRight'), + action: () => { + const pid = paneLayout.focusedPaneId ?? paneLayoutStore.getFocusedPaneId() + if (pid && splitPaneEnabled) paneLayoutStore.splitPane(pid, 'horizontal') + }, + }, + { + id: 'splitDown', + label: t('commands:splitDown'), + description: t('commands:splitDownDesc'), + category: t('commands:categories.pane'), + shortcut: getShortcut('splitDown'), + action: () => { + const pid = paneLayout.focusedPaneId ?? paneLayoutStore.getFocusedPaneId() + if (pid && splitPaneEnabled) paneLayoutStore.splitPane(pid, 'vertical') + }, + }, + { + id: 'closePane', + label: t('commands:closePane'), + description: t('commands:closePaneDesc'), + category: t('commands:categories.pane'), + shortcut: getShortcut('closePane'), + action: () => { + const pid = paneLayout.focusedPaneId ?? paneLayoutStore.getFocusedPaneId() + if (pid && paneLayout.isSplit) paneLayoutStore.closePane(pid) + }, + when: () => paneLayout.isSplit, + }, + { + id: 'togglePaneFullscreen', + label: t('commands:togglePaneFullscreen'), + description: t('commands:togglePaneFullscreenDesc'), + category: t('commands:categories.pane'), + shortcut: getShortcut('togglePaneFullscreen'), + action: () => { + if (paneLayout.isSplit) handleToggleFocusedPaneFullscreen() + }, + when: () => paneLayout.isSplit, + }, ] }, [ - openSettings, openProject, sidebarExpanded, setSidebarExpanded, - handleNewSession, handleArchiveSession, handlePreviousSession, handleNextSession, - handleNewTerminal, handleToggleAgent, handleCopyLastResponse, - isStreaming, handleAbort, + t, + openSettings, + openProject, + openSettingsTab, + handleToggleSidebar, + handleToggleRightPanel, + focusedController, + handleNewTerminal, + paneLayout.focusedPaneId, + paneLayout.isSplit, + splitPaneEnabled, + handleToggleFocusedPaneFullscreen, ]) - // ============================================ - // Render - // ============================================ - const isIdle = !isStreaming - - // streaming 结束时清理 cancel hint - useEffect(() => { - if (!isStreaming) { - setShowCancelHint(false) - lastEscTimeRef.current = 0 - if (escHintTimerRef.current) { - clearTimeout(escHintTimerRef.current) - escHintTimerRef.current = null - } - } - }, [isStreaming]) - - const revertedMessage = revertedContent ? { - text: revertedContent.text, - attachments: revertedContent.attachments as Attachment[], - } : undefined + const { showCloseDialog, handleCloseDialogConfirm, handleCloseDialogCancel } = useCloseServiceDialog() return ( -
- {/* Sidebar */} - setSidebarExpanded(true)} - onClose={() => setSidebarExpanded(false)} - contextLimit={currentModel?.contextLimit} - onOpenSettings={openSettings} - themeMode={themeMode} - onThemeChange={setThemeWithAnimation} - isWideMode={isWideMode} - onToggleWideMode={toggleWideMode} - projectDialogOpen={projectDialogOpen} - onProjectDialogClose={closeProjectDialog} - /> +
+ + + +
+ {isMobilePanelLayout ? ( + <> +
+
+ +
+ +
+
+
+ +
+ + - {/* Main Content Area: Chat Column + Right Panel */} -
- {/* Left Column: Chat + Bottom Panel */} -
- {/* Chat Area */} -
- {/* Header Overlay */} -
-
-
setSidebarExpanded(true)} - modelSelectorRef={modelSelectorRef} - /> + {sidebarExpanded && ( +
+ +
+ +
-
- - {/* Scrollable Area */} -
- + + ) : ( + <> + -
- - {/* Floating Input Box */} -
- {/* Double-Esc cancel hint */} - {showCancelHint && ( -
-
- Press Esc again to stop + +
+
+
+
+ +
- )} - -
- - {/* Permission Dialog */} - {pendingPermissionRequests.length > 0 && ( - handlePermissionReply(pendingPermissionRequests[0].id, reply, effectiveDirectory)} - queueLength={pendingPermissionRequests.length} - isReplying={isReplying} - currentSessionId={routeSessionId} - /> - )} - - {/* Question Dialog */} - {pendingPermissionRequests.length === 0 && pendingQuestionRequests.length > 0 && ( - handleQuestionReply(pendingQuestionRequests[0].id, answers, effectiveDirectory)} - onReject={() => handleQuestionReject(pendingQuestionRequests[0].id, effectiveDirectory)} - queueLength={pendingQuestionRequests.length} - isReplying={isReplying} - /> - )} -
- {/* Bottom Panel */} - + +
+ + )} +
- {/* Right Panel - 占满整个高度 */} - -
- - {/* Settings Dialog */} - - - {/* Command Palette */} - setCommandPaletteOpen(false)} - commands={commands} - /> + + + setCommandPaletteOpen(false)} + commands={commands} + /> + + + + + +
) } diff --git a/src/api/agent.ts b/src/api/agent.ts index 3f609c99..5e4a977a 100644 --- a/src/api/agent.ts +++ b/src/api/agent.ts @@ -1,17 +1,18 @@ // ============================================ // Agent API Functions -// 基于 OpenAPI: /agent 相关接口 +// 基于 @opencode-ai/sdk: /agent 相关接口 // ============================================ -import { get } from './http' +import { getSDKClient, unwrap } from './sdk' import { formatPathForApi } from '../utils/directoryUtils' import type { ApiAgent } from './types' /** - * GET /agent - 获取 agent 列表 + * 获取 agent 列表 */ export async function getAgents(directory?: string): Promise { - return get('/agent', { directory: formatPathForApi(directory) }) + const sdk = getSDKClient() + return unwrap(await sdk.app.agents({ directory: formatPathForApi(directory) })) } /** diff --git a/src/api/client.ts b/src/api/client.ts index 5ffc248a..61518832 100644 --- a/src/api/client.ts +++ b/src/api/client.ts @@ -1,19 +1,25 @@ // ============================================ // API Client for OpenCode Backend -// 基于 OpenAPI: /config, /project, /provider 相关接口 +// 基于 @opencode-ai/sdk: /config, /project, /provider 相关接口 // ============================================ -import { get, patch } from './http' +import { getSDKClient, unwrap } from './sdk' import { formatPathForApi } from '../utils/directoryUtils' -import type { - ProvidersResponse, - ModelInfo, - ApiProject, - ApiPath, -} from './types' +import type { ModelInfo, ApiProject, ApiPath } from './types' -// Re-export API_BASE for backward compatibility -export { API_BASE } from './http' +function isRecord(value: unknown): value is Record { + return !!value && typeof value === 'object' && !Array.isArray(value) +} + +function requireRecord(value: unknown, message: string): Record { + if (isRecord(value)) return value + throw new Error(message) +} + +function requireArray(value: unknown, message: string): T[] { + if (Array.isArray(value)) return value as T[] + throw new Error(message) +} // Re-export all types export * from './types' @@ -41,31 +47,45 @@ export * from './lsp' // ============================================ // Model API Functions -// 基于 OpenAPI: GET /config/providers +// 基于 SDK: config.providers() // ============================================ export async function getActiveModels(directory?: string): Promise { - const data = await get('/config/providers', { - directory: formatPathForApi(directory) - }) + const sdk = getSDKClient() + const data = requireRecord( + unwrap(await sdk.config.providers({ directory: formatPathForApi(directory) })), + 'Invalid OpenCode providers response', + ) + const providers = requireArray>(data.providers, 'Invalid OpenCode providers response') const models: ModelInfo[] = [] - for (const provider of data.providers) { - for (const [, model] of Object.entries(provider.models)) { + for (const provider of providers) { + const providerModels = isRecord(provider.models) ? provider.models : {} + for (const [, rawModel] of Object.entries(providerModels)) { + if (!isRecord(rawModel)) continue + const model = rawModel if (model.status === 'active') { - const variants = model.variants ? Object.keys(model.variants) : [] - + const limit = isRecord(model.limit) ? model.limit : {} + const capabilities = isRecord(model.capabilities) ? model.capabilities : {} + const inputCapabilities = isRecord(capabilities.input) ? capabilities.input : {} + const variants = isRecord(model.variants) ? Object.keys(model.variants) : [] + const modelId = typeof model.id === 'string' ? model.id : '' + if (!modelId) continue + models.push({ - id: model.id, - name: model.name, - providerId: provider.id, - providerName: provider.name, - family: model.family, - contextLimit: model.limit.context, - outputLimit: model.limit.output, - supportsReasoning: model.capabilities.reasoning, - supportsImages: model.capabilities.input.image, - supportsToolcall: model.capabilities.toolcall, + id: modelId, + name: typeof model.name === 'string' ? model.name : modelId, + providerId: typeof provider.id === 'string' ? provider.id : '', + providerName: typeof provider.name === 'string' ? provider.name : typeof provider.id === 'string' ? provider.id : '', + family: typeof model.family === 'string' ? model.family : '', + contextLimit: typeof limit.context === 'number' ? limit.context : 0, + outputLimit: typeof limit.output === 'number' ? limit.output : 0, + supportsReasoning: capabilities.reasoning === true, + supportsImages: inputCapabilities.image === true, + supportsPdf: inputCapabilities.pdf === true, + supportsAudio: inputCapabilities.audio === true, + supportsVideo: inputCapabilities.video === true, + supportsToolcall: capabilities.toolcall === true, variants, }) } @@ -76,45 +96,63 @@ export async function getActiveModels(directory?: string): Promise } export async function getDefaultModels(directory?: string): Promise> { - const data = await get('/config/providers', { - directory: formatPathForApi(directory) - }) - return data.default + const sdk = getSDKClient() + const data = requireRecord( + unwrap(await sdk.config.providers({ directory: formatPathForApi(directory) })), + 'Invalid OpenCode providers response', + ) + const defaults = requireRecord(data.default, 'Invalid OpenCode default model response') + return Object.fromEntries(Object.entries(defaults).filter((entry): entry is [string, string] => typeof entry[1] === 'string')) } // ============================================ // Project API Functions -// 基于 OpenAPI: /project, /project/current, /project/{projectID} +// 基于 SDK: project.* // ============================================ /** - * GET /project/current - 获取当前项目 + * 获取当前项目 */ export async function getCurrentProject(directory?: string): Promise { - return get('/project/current', { directory: formatPathForApi(directory) }) + const sdk = getSDKClient() + return unwrap(await sdk.project.current({ directory: formatPathForApi(directory) })) } /** - * GET /project - 获取项目列表 + * 获取项目列表 */ export async function getProjects(directory?: string): Promise { - return get('/project', { directory: formatPathForApi(directory) }) + const sdk = getSDKClient() + return requireArray(unwrap(await sdk.project.list({ directory: formatPathForApi(directory) })), 'Invalid OpenCode project list response') +} + +/** + * 初始化 Git 仓库 + */ +export async function initGitProject(directory?: string): Promise { + const sdk = getSDKClient() + return unwrap(await sdk.project.initGit({ directory: formatPathForApi(directory) })) } /** - * PATCH /project/{projectID} - 更新项目 + * 更新项目 */ export async function updateProject( - projectId: string, + projectId: string, params: { name?: string icon?: { url?: string; override?: string; color?: string } }, - directory?: string + directory?: string, ): Promise { - return patch(`/project/${projectId}`, { - directory: formatPathForApi(directory) - }, params) + const sdk = getSDKClient() + return unwrap( + await sdk.project.update({ + projectID: projectId, + directory: formatPathForApi(directory), + ...params, + }), + ) } // ============================================ @@ -122,5 +160,6 @@ export async function updateProject( // ============================================ export async function getPath(): Promise { - return get('/path') + const sdk = getSDKClient() + return requireRecord(unwrap(await sdk.path.get()), 'Invalid OpenCode path response') as unknown as ApiPath } diff --git a/src/api/command.test.ts b/src/api/command.test.ts new file mode 100644 index 00000000..8d03f54f --- /dev/null +++ b/src/api/command.test.ts @@ -0,0 +1,48 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { getCommands } from './command' + +const listMock = vi.fn() + +vi.mock('./sdk', () => ({ + getSDKClient: () => ({ + command: { + list: (...args: unknown[]) => listMock(...args), + }, + }), + unwrap: (result: { data?: unknown }) => result.data, +})) + +vi.mock('../store/serverStore', () => ({ + serverStore: { + getActiveServerId: () => 'test-server', + }, +})) + +describe('getCommands', () => { + beforeEach(() => { + listMock.mockReset() + }) + + it('marks frontend and api commands with stable sources', async () => { + listMock.mockResolvedValue({ data: [{ name: 'review', description: 'Run project review' }] }) + + const commands = await getCommands('/workspace/project') + + expect(commands).toEqual([ + { name: 'review', description: 'Run project review', source: 'api' }, + { name: 'new', description: 'Create a new chat session', source: 'frontend' }, + { name: 'compact', description: 'Compact session by summarizing conversation history', source: 'frontend' }, + ]) + }) + + it('keeps API commands as api commands even if names overlap frontend commands', async () => { + listMock.mockResolvedValue({ data: [{ name: 'compact', description: 'Native compact command' }] }) + + const commands = await getCommands('/workspace/project-overlap') + + expect(commands).toEqual([ + { name: 'compact', description: 'Native compact command', source: 'api' }, + { name: 'new', description: 'Create a new chat session', source: 'frontend' }, + ]) + }) +}) diff --git a/src/api/command.ts b/src/api/command.ts index a20623a8..6e3aec8c 100644 --- a/src/api/command.ts +++ b/src/api/command.ts @@ -2,38 +2,95 @@ // Command API - 命令列表和执行 // ============================================ -import { get, post } from './http' +import { getSDKClient, unwrap } from './sdk' import { formatPathForApi } from '../utils/directoryUtils' +import { serverStore } from '../store/serverStore' +import i18n from '../i18n' export interface Command { name: string description?: string keybind?: string + source: 'frontend' | 'api' +} + +type ApiCommand = Omit + +// Frontend-added slash commands that do not come from GET /command. +// These are executed locally or via dedicated session actions. +function getFrontendCommands(): Command[] { + return [ + { name: 'new', description: i18n.t('commands:slashCommand.newSessionDesc'), source: 'frontend' }, + { name: 'compact', description: i18n.t('commands:slashCommand.compactDesc'), source: 'frontend' }, + ] +} + +const COMMAND_CACHE_TTL_MS = 10_000 + +const commandCache = new Map() +const commandInflight = new Map>() + +function getCommandCacheKey(directory?: string): string { + return `${serverStore.getActiveServerId()}::${i18n.resolvedLanguage || i18n.language}::${formatPathForApi(directory) ?? ''}` +} + +async function fetchCommands(directory?: string): Promise { + let apiCommands: ApiCommand[] = [] + try { + const sdk = getSDKClient() + apiCommands = unwrap(await sdk.command.list({ directory: formatPathForApi(directory) })) ?? [] + } catch { + // Backend unreachable — frontend commands still available + } + const frontendCommands = getFrontendCommands() + const commandsFromApi: Command[] = apiCommands.map(command => ({ ...command, source: 'api' })) + const apiNames = new Set(commandsFromApi.map(c => c.name)) + return [...commandsFromApi, ...frontendCommands.filter(c => !apiNames.has(c.name))] } -/** - * 获取可用命令列表 - */ export async function getCommands(directory?: string): Promise { - return get('/command', { directory: formatPathForApi(directory) }) + const key = getCommandCacheKey(directory) + const now = Date.now() + const cached = commandCache.get(key) + if (cached && cached.expiresAt > now) { + return cached.data + } + + const inflight = commandInflight.get(key) + if (inflight) { + return inflight + } + + const request = fetchCommands(directory) + .then(data => { + commandCache.set(key, { data, expiresAt: Date.now() + COMMAND_CACHE_TTL_MS }) + return data + }) + .finally(() => { + commandInflight.delete(key) + }) + + commandInflight.set(key, request) + return request +} + +export async function prefetchCommands(directory?: string): Promise { + await getCommands(directory) } -/** - * POST /session/{sessionID}/command - 执行斜杠命令 - * @param sessionId Session ID - * @param command 命令名(不含 /,如 "help") - * @param args 命令参数 - * @param directory 工作目录 - */ export async function executeCommand( sessionId: string, command: string, args: string = '', - directory?: string + directory?: string, ): Promise { - return post( - `/session/${sessionId}/command`, - { directory: formatPathForApi(directory) }, - { command, arguments: args } + const sdk = getSDKClient() + return unwrap( + await sdk.session.command({ + sessionID: sessionId, + directory: formatPathForApi(directory), + command, + arguments: args, + }), ) } diff --git a/src/api/config.ts b/src/api/config.ts index b0c2d85c..e11f8f14 100644 --- a/src/api/config.ts +++ b/src/api/config.ts @@ -2,27 +2,48 @@ // Config API - 配置管理 // ============================================ -import { get, patch } from './http' +import { getSDKClient, unwrap } from './sdk' import { formatPathForApi } from '../utils/directoryUtils' import type { Config } from '../types/api/config' +import type { ProvidersResponse } from '../types/api/model' /** * 获取当前配置 */ export async function getConfig(directory?: string): Promise { - return get('/config', { directory: formatPathForApi(directory) }) + const sdk = getSDKClient() + return unwrap(await sdk.config.get({ directory: formatPathForApi(directory) })) +} + +/** + * 获取用户全局配置(官方桌面设置写入的配置源) + */ +export async function getGlobalConfig(): Promise { + const sdk = getSDKClient() + return unwrap(await sdk.global.config.get()) } /** * 更新配置 */ -export async function updateConfig(config: Partial, directory?: string): Promise { - return patch('/config', { directory: formatPathForApi(directory) }, config) +export async function updateConfig(config: Config, directory?: string): Promise { + const sdk = getSDKClient() + return unwrap(await sdk.config.update({ directory: formatPathForApi(directory), config })) +} + +/** + * 更新用户全局配置。 + * 官方接口是 deep merge patch:支持新增/修改,不支持删除任意字段。 + */ +export async function updateGlobalConfig(config: Config): Promise { + const sdk = getSDKClient() + return unwrap(await sdk.global.config.update({ config })) } /** * 获取 provider 配置列表 */ -export async function getProviderConfigs(directory?: string): Promise> { - return get>('/config/providers', { directory: formatPathForApi(directory) }) +export async function getProviderConfigs(directory?: string): Promise { + const sdk = getSDKClient() + return unwrap(await sdk.config.providers({ directory: formatPathForApi(directory) })) } diff --git a/src/api/events.test.ts b/src/api/events.test.ts new file mode 100644 index 00000000..b502d053 --- /dev/null +++ b/src/api/events.test.ts @@ -0,0 +1,247 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { EventTypes } from '../types/api/event' + +vi.mock('./http', () => ({ + getApiBaseUrl: () => 'http://example.test', + getAuthHeader: () => ({}), +})) + +vi.mock('../utils/tauri', () => ({ + isTauri: () => false, +})) + +const encoder = new TextEncoder() + +function concatBytes(...parts: Uint8Array[]): Uint8Array { + const total = parts.reduce((sum, part) => sum + part.length, 0) + const result = new Uint8Array(total) + let offset = 0 + + for (const part of parts) { + result.set(part, offset) + offset += part.length + } + + return result +} + +function createEventChunks(delta: string, splitAt: number): Uint8Array[] { + const marker = '__DELTA__' + const raw = `data: ${JSON.stringify({ + directory: 'global', + payload: { + type: EventTypes.MESSAGE_PART_DELTA, + properties: { + messageID: 'session-1', + partID: 'part-1', + field: 'text', + delta: marker, + }, + }, + })}\n\n` + + const [before, after] = raw.split(marker) + const deltaBytes = encoder.encode(delta) + + return [ + concatBytes(encoder.encode(before), deltaBytes.slice(0, splitAt)), + concatBytes(deltaBytes.slice(splitAt), encoder.encode(after)), + ] +} + +function createStream(chunks: Uint8Array[]): ReadableStream { + return new ReadableStream({ + start(controller) { + for (const chunk of chunks) { + controller.enqueue(chunk) + } + controller.close() + }, + }) +} + +function createFetchResponse(chunks: Uint8Array[]): Pick { + return { + ok: true, + body: createStream(chunks) as Response['body'], + } +} + +function createDeferred() { + let resolve!: (value: T) => void + let reject!: (reason?: unknown) => void + const promise = new Promise((res, rej) => { + resolve = res + reject = rej + }) + return { promise, resolve, reject } +} + +function createEventChunk(payload: object): Uint8Array[] { + return [encoder.encode(`data: ${JSON.stringify({ directory: 'global', payload })}\n\n`)] +} + +describe('subscribeToEvents', () => { + beforeEach(() => { + vi.resetModules() + }) + + afterEach(() => { + vi.restoreAllMocks() + vi.unstubAllGlobals() + }) + + it('preserves Chinese text when UTF-8 bytes are split across chunks', async () => { + const fetchMock = vi.fn().mockResolvedValue(createFetchResponse(createEventChunks('中文', 2))) + vi.stubGlobal('fetch', fetchMock) + + const { subscribeToEvents } = await import('./events') + + const received = await new Promise((resolve, reject) => { + const unsubscribe = subscribeToEvents({ + onPartDelta(data) { + unsubscribe() + resolve(data.delta) + }, + onError(error) { + unsubscribe() + reject(error) + }, + }) + }) + + expect(received).toBe('中文') + }) + + it('preserves four-byte characters when split in the middle', async () => { + const fetchMock = vi.fn().mockResolvedValue(createFetchResponse(createEventChunks('𠮷😀', 3))) + vi.stubGlobal('fetch', fetchMock) + + const { subscribeToEvents } = await import('./events') + + const received = await new Promise((resolve, reject) => { + const unsubscribe = subscribeToEvents({ + onPartDelta(data) { + unsubscribe() + resolve(data.delta) + }, + onError(error) { + unsubscribe() + reject(error) + }, + }) + }) + + expect(received).toBe('𠮷😀') + }) + + it('dispatches server.connected payloads with timestamp', async () => { + const fetchMock = vi.fn().mockResolvedValue( + createFetchResponse( + createEventChunk({ + type: EventTypes.SERVER_CONNECTED, + properties: { timestamp: '2026-04-22T15:00:00.000Z' }, + }), + ), + ) + vi.stubGlobal('fetch', fetchMock) + + const { subscribeToEvents } = await import('./events') + + const received = await new Promise((resolve, reject) => { + const unsubscribe = subscribeToEvents({ + onServerConnected(data) { + unsubscribe() + resolve(data.timestamp) + }, + onError(error) { + unsubscribe() + reject(error) + }, + }) + }) + + expect(received).toBe('2026-04-22T15:00:00.000Z') + }) + + it('ignores stale server.connected events from an old browser SSE generation after reconnect', async () => { + const firstFetch = createDeferred>() + const secondFetch = createDeferred>() + const fetchMock = vi + .fn() + .mockImplementationOnce(() => firstFetch.promise) + .mockImplementationOnce(() => secondFetch.promise) + vi.stubGlobal('fetch', fetchMock) + + const { subscribeToEvents, reconnectSSE } = await import('./events') + const received: string[] = [] + + const unsubscribe = subscribeToEvents({ + onServerConnected(data) { + if (typeof data.timestamp === 'string') { + received.push(data.timestamp) + } + }, + }) + + reconnectSSE() + + secondFetch.resolve( + createFetchResponse( + createEventChunk({ + type: EventTypes.SERVER_CONNECTED, + properties: { timestamp: 'new-server-time' }, + }), + ), + ) + + await vi.waitFor(() => { + expect(received).toEqual(['new-server-time']) + }) + + firstFetch.resolve( + createFetchResponse( + createEventChunk({ + type: EventTypes.SERVER_CONNECTED, + properties: { timestamp: 'stale-server-time' }, + }), + ), + ) + + await Promise.resolve() + await Promise.resolve() + + expect(received).toEqual(['new-server-time']) + unsubscribe() + }) + + it('ignores stale browser fetch failures from an old SSE generation after reconnect', async () => { + const firstFetch = createDeferred>() + const secondFetch = createDeferred>() + const fetchMock = vi + .fn() + .mockImplementationOnce(() => firstFetch.promise) + .mockImplementationOnce(() => secondFetch.promise) + vi.stubGlobal('fetch', fetchMock) + + const { subscribeToEvents, reconnectSSE, getConnectionInfo } = await import('./events') + const onError = vi.fn() + + const unsubscribe = subscribeToEvents({ + onError, + }) + + reconnectSSE() + + firstFetch.reject(new Error('stale failure')) + + await Promise.resolve() + await Promise.resolve() + + expect(onError).not.toHaveBeenCalled() + expect(getConnectionInfo().state).toBe('connecting') + + secondFetch.reject(Object.assign(new Error('aborted'), { name: 'AbortError' })) + unsubscribe() + }) +}) diff --git a/src/api/events.ts b/src/api/events.ts index 5c623254..6163fcab 100644 --- a/src/api/events.ts +++ b/src/api/events.ts @@ -3,20 +3,18 @@ // ============================================ import { getApiBaseUrl, getAuthHeader } from './http' +import { createSseTextParser } from './sse' +import { normalizeTodoItems } from './todo' +import { isTauri } from '../utils/tauri' import type { - ApiMessageWithParts, - ApiPart, - ApiSession, - ApiPermissionRequest, - PermissionReply, - ApiQuestionRequest, - GlobalEvent, + ApiMessage, EventCallbacks, - WorktreeReadyPayload, - WorktreeFailedPayload, - VcsBranchUpdatedPayload, + GlobalEvent, + ServerConnectedPayload, + SessionErrorPayload, TodoUpdatedPayload, } from './types' +import { EventTypes } from '../types/api/event' // ============================================ // Connection State @@ -42,7 +40,9 @@ const connectionListeners = new Set<(info: ConnectionInfo) => void>() function updateConnectionState(update: Partial) { connectionInfo = { ...connectionInfo, ...update } - connectionListeners.forEach(fn => fn(connectionInfo)) + connectionListeners.forEach(fn => { + fn(connectionInfo) + }) } export function getConnectionInfo(): ConnectionInfo { @@ -61,7 +61,13 @@ export function subscribeToConnectionState(fn: (info: ConnectionInfo) => void): // ============================================ const RECONNECT_DELAYS = [1000, 2000, 3000, 5000, 10000, 30000] +/** 后台时使用更激进的重连延迟,确保尽快恢复连接 */ +const BACKGROUND_RECONNECT_DELAYS = [500, 1000, 2000, 3000, 5000, 10000] const HEARTBEAT_TIMEOUT = 60000 +/** 后台时的心跳超时(更宽松,因为后台 timer 可能不准) */ +const BACKGROUND_HEARTBEAT_TIMEOUT = 120000 +/** 后台 keepalive 间隔:定期检查连接是否还活着 */ +const BACKGROUND_KEEPALIVE_INTERVAL = 30000 // 所有订阅者的 callbacks const allSubscribers = new Set() @@ -70,31 +76,91 @@ const allSubscribers = new Set() let singletonController: AbortController | null = null let heartbeatTimer: ReturnType | null = null let reconnectTimer: ReturnType | null = null +let keepaliveTimer: ReturnType | null = null let isConnecting = false +let lifecycleListenersRegistered = false +/** 连接代次,每次 reconnectSSE() 递增,旧代次的事件会被丢弃 */ +let connectionGeneration = 0 +/** 当前是否在后台 */ +let isInBackground = false +/** 是否因为切换服务器而触发的重连 */ +let isServerSwitch = false +/** 上一次 bridge_disconnect 的 Promise,用于串行化 Tauri 侧的 disconnect → connect */ +let pendingDisconnect: Promise = Promise.resolve() +/** Cooldown: 上一次 onReconnected 广播的时间戳 */ +let lastReconnectedBroadcast = 0 +const RECONNECTED_COOLDOWN = 2000 + +function finalizeConnectionAttempt(generation: number): boolean { + if (generation !== connectionGeneration) { + return false + } + isConnecting = false + return true +} + +/** + * 广播 onReconnected,带 cooldown 防止 SSE 快速重连时密集触发数据拉取 + */ +function broadcastReconnected(reason: 'network' | 'server-switch') { + const now = Date.now() + if (reason !== 'server-switch' && now - lastReconnectedBroadcast < RECONNECTED_COOLDOWN) { + if (import.meta.env.DEV) { + console.log('[SSE] onReconnected skipped (cooldown)') + } + return + } + lastReconnectedBroadcast = now + allSubscribers.forEach(cb => { + cb.onReconnected?.(reason) + }) +} + +/** + * 请求 Tauri 侧断开 SSE 连接 + * 返回 Promise,调用方可以 await 确保断开完成后再发起新连接 + * 多次并发调用会自动串行化 + */ +function disconnectTauri(): Promise { + if (!isTauri()) return Promise.resolve() + + const p = pendingDisconnect.then(() => + import('@tauri-apps/api/core') + .then(({ invoke }) => invoke('bridge_disconnect', { args: { bridgeId: 'sse' } }).then(() => undefined)) + .catch(() => {}), + ) + pendingDisconnect = p + return p +} function resetHeartbeat() { if (heartbeatTimer) clearTimeout(heartbeatTimer) - + updateConnectionState({ lastEventTime: Date.now() }) - + + // 后台时使用更宽松的超时,因为移动端后台 timer 可能被冻结/延迟 + const timeout = isInBackground ? BACKGROUND_HEARTBEAT_TIMEOUT : HEARTBEAT_TIMEOUT + heartbeatTimer = setTimeout(() => { - console.warn('[SSE] No events received for 60s, reconnecting...') + console.warn(`[SSE] No events received for ${timeout / 1000}s, reconnecting...`) updateConnectionState({ state: 'disconnected', error: 'Heartbeat timeout' }) scheduleReconnect() - }, HEARTBEAT_TIMEOUT) + }, timeout) } function scheduleReconnect() { if (reconnectTimer) clearTimeout(reconnectTimer) if (allSubscribers.size === 0) return // 没有订阅者就不重连 - + const attempt = connectionInfo.reconnectAttempt - const delay = RECONNECT_DELAYS[Math.min(attempt, RECONNECT_DELAYS.length - 1)] - + // 后台时使用更激进的重连策略 + const delays = isInBackground ? BACKGROUND_RECONNECT_DELAYS : RECONNECT_DELAYS + const delay = delays[Math.min(attempt, delays.length - 1)] + if (import.meta.env.DEV) { - console.log(`[SSE] Reconnecting in ${delay}ms (attempt ${attempt + 1})...`) + console.log(`[SSE] Reconnecting in ${delay}ms (attempt ${attempt + 1}, background: ${isInBackground})...`) } - + reconnectTimer = setTimeout(() => { updateConnectionState({ reconnectAttempt: attempt + 1 }) connectSingleton() @@ -103,54 +169,238 @@ function scheduleReconnect() { function connectSingleton() { if (isConnecting || allSubscribers.size === 0) return - if (connectionInfo.state === 'connected') return - + + // 如果状态声称 connected,验证连接是否真的活着 + if (connectionInfo.state === 'connected') { + const timeSinceLastEvent = Date.now() - connectionInfo.lastEventTime + // 后台时使用更宽松的超时判断 + const staleTimeout = isInBackground ? BACKGROUND_HEARTBEAT_TIMEOUT : HEARTBEAT_TIMEOUT + if (timeSinceLastEvent > staleTimeout) { + // 太久没收到事件,连接可能已死,强制断开再重连 + if (import.meta.env.DEV) { + console.log( + `[SSE] connectSingleton: state=connected but stale (${Math.round(timeSinceLastEvent / 1000)}s), forcing disconnect`, + ) + } + connectionGeneration++ + disconnectTauri() + if (singletonController) { + singletonController.abort() + singletonController = null + } + updateConnectionState({ state: 'disconnected' }) + } else { + return // 连接确实还活着 + } + } + isConnecting = true - singletonController = new AbortController() - + updateConnectionState({ state: 'connecting' }) if (import.meta.env.DEV) { console.log('[SSE] Connecting singleton...') } - // 构建请求头,如果有认证信息则添加 - const headers: Record = {} - const authHeader = getAuthHeader() - if (authHeader) { - headers['Authorization'] = authHeader + // 注册生命周期监听器(首次连接时) + registerLifecycleListeners() + + if (isTauri()) { + connectViaTauri() + } else { + connectViaBrowser() + } +} + +// ============================================ +// Tauri SSE Bridge (via Rust reqwest + Channel) +// ============================================ + +/** Unified bridge event from Rust (transparent proxy) */ +interface BridgeEvent { + event: 'connected' | 'data' | 'disconnected' | 'error' + data?: { + data?: string + code?: number + reason?: string + message?: string } - +} + +async function connectViaTauri() { + const myGeneration = connectionGeneration + + try { + // 等待上一次 disconnect 完成,避免 Rust 侧 connect/disconnect 竞争 + await pendingDisconnect + + const { invoke, Channel } = await import('@tauri-apps/api/core') + + const url = `${getApiBaseUrl()}/global/event` + const authHeaders = getAuthHeader() + const authHeader = authHeaders['Authorization'] || null + + const sseParser = createSseTextParser() + + const onEvent = new Channel() + + onEvent.onmessage = (msg: BridgeEvent) => { + // 代次不匹配,说明已经 reconnect 过了,忽略旧连接的事件 + if (myGeneration !== connectionGeneration) return + + switch (msg.event) { + case 'connected': { + isConnecting = false + + updateConnectionState({ + state: 'connected', + reconnectAttempt: 0, + error: undefined, + }) + resetHeartbeat() + if (import.meta.env.DEV) { + console.log('[SSE/Tauri] Connected') + } + // 每次连接成功都通知订阅者刷新数据 + // 覆盖场景:首次连接(先开 UI 后开 server)、网络重连、服务器切换 + const reason = isServerSwitch ? ('server-switch' as const) : ('network' as const) + isServerSwitch = false + broadcastReconnected(reason) + break + } + case 'data': { + resetHeartbeat() + if (!msg.data?.data) break + + for (const eventData of sseParser.push(msg.data.data)) { + const globalEvent = parseGlobalEvent(eventData) + if (globalEvent) { + broadcastEvent(globalEvent) + } + } + break + } + case 'disconnected': { + isConnecting = false + if (import.meta.env.DEV) { + console.log('[SSE/Tauri] Disconnected:', msg.data?.reason) + } + updateConnectionState({ state: 'disconnected' }) + scheduleReconnect() + break + } + case 'error': { + isConnecting = false + const errorMsg = msg.data?.message || 'Unknown error' + if (import.meta.env.DEV) { + console.warn('[SSE/Tauri] Error:', errorMsg) + } + updateConnectionState({ + state: 'error', + error: errorMsg, + }) + allSubscribers.forEach(cb => { + cb.onError?.(new Error(errorMsg)) + }) + scheduleReconnect() + break + } + } + } + + // 调用统一桥接命令 + invoke('bridge_connect', { + args: { bridgeId: 'sse', url, authHeader }, + onEvent, + }).catch((error: unknown) => { + if (!finalizeConnectionAttempt(myGeneration)) return + const errorMsg = error instanceof Error ? error.message : String(error) + if (import.meta.env.DEV) { + console.warn('[SSE/Tauri] invoke error:', errorMsg) + } + updateConnectionState({ + state: 'error', + error: errorMsg, + }) + allSubscribers.forEach(cb => { + cb.onError?.(new Error(errorMsg)) + }) + scheduleReconnect() + }) + } catch (error) { + if (!finalizeConnectionAttempt(myGeneration)) return + const errorMsg = error instanceof Error ? error.message : String(error) + console.warn('[SSE/Tauri] Failed to initialize:', errorMsg) + updateConnectionState({ state: 'error', error: errorMsg }) + scheduleReconnect() + } +} + +// ============================================ +// Browser SSE (via fetch + ReadableStream) +// ============================================ + +function connectViaBrowser() { + singletonController = new AbortController() + + // 捕获当前连接代次 + const myGeneration = connectionGeneration + fetch(`${getApiBaseUrl()}/global/event`, { - headers, signal: singletonController.signal, + headers: { + Accept: 'text/event-stream', + ...getAuthHeader(), + }, }) - .then(async (response) => { - isConnecting = false - + .then(async response => { + if (myGeneration !== connectionGeneration) { + await response.body?.cancel?.().catch(() => {}) + return + } + + finalizeConnectionAttempt(myGeneration) + if (!response.ok) { throw new Error(`Failed to subscribe: ${response.status}`) } - updateConnectionState({ - state: 'connected', + updateConnectionState({ + state: 'connected', reconnectAttempt: 0, - error: undefined + error: undefined, }) resetHeartbeat() if (import.meta.env.DEV) { console.log('[SSE] Singleton connected') } + // 每次连接成功都通知订阅者刷新数据 + // 覆盖场景:首次连接(先开 UI 后开 server)、网络重连、服务器切换 + const reason = isServerSwitch ? ('server-switch' as const) : ('network' as const) + isServerSwitch = false + broadcastReconnected(reason) + const reader = response.body?.getReader() if (!reader) { throw new Error('No response body') } const decoder = new TextDecoder() - let buffer = '' + const sseParser = createSseTextParser() while (true) { + // 代次不匹配,说明已经 reconnect 过了,停止读取旧流 + if (myGeneration !== connectionGeneration) { + reader.cancel().catch(() => {}) + break + } + const { done, value } = await reader.read() + if (myGeneration !== connectionGeneration) { + reader.cancel().catch(() => {}) + break + } + if (done) { if (import.meta.env.DEV) { console.log('[SSE] Stream ended, reconnecting...') @@ -162,35 +412,19 @@ function connectSingleton() { resetHeartbeat() - buffer += decoder.decode(value, { stream: true }) - - const lines = buffer.split('\n') - buffer = lines.pop() || '' - - let eventData = '' - - for (const line of lines) { - if (line.startsWith('data:')) { - eventData = line.slice(5).trim() - } else if (line === '' && eventData) { - try { - const globalEvent = JSON.parse(eventData) as GlobalEvent - // 广播给所有订阅者 - broadcastEvent(globalEvent) - } catch (e) { - // SSE parse error - logged only in development - if (import.meta.env.DEV) { - console.warn('[SSE] Failed to parse event:', e, eventData) - } - } - eventData = '' + for (const eventData of sseParser.push(decoder.decode(value, { stream: true }))) { + const globalEvent = parseGlobalEvent(eventData) + if (globalEvent) { + broadcastEvent(globalEvent) } } } }) - .catch((error) => { - isConnecting = false - + .catch(error => { + if (!finalizeConnectionAttempt(myGeneration)) { + return + } + if (error.name === 'AbortError') { return } @@ -198,98 +432,331 @@ function connectSingleton() { if (import.meta.env.DEV) { console.warn('[SSE] Event stream error:', error) } - updateConnectionState({ - state: 'error', - error: error.message || 'Connection failed' + updateConnectionState({ + state: 'error', + error: error.message || 'Connection failed', }) // 通知所有订阅者出错 - allSubscribers.forEach(cb => cb.onError?.(error)) + allSubscribers.forEach(cb => { + cb.onError?.(error) + }) scheduleReconnect() }) } +function parseGlobalEvent(raw: string): GlobalEvent | null { + try { + const parsed: unknown = JSON.parse(raw) + return isGlobalEvent(parsed) ? parsed : null + } catch (error) { + if (import.meta.env.DEV) { + console.warn('[SSE] Failed to parse event:', error, raw) + } + return null + } +} + +function isGlobalEvent(value: unknown): value is GlobalEvent { + if (!isRecord(value)) return false + if (typeof value.directory !== 'string') return false + if (!isRecord(value.payload)) return false + if (typeof value.payload.type !== 'string') return false + return 'properties' in value.payload +} + +function isRecord(value: unknown): value is Record { + return !!value && typeof value === 'object' +} + +function getMessageInfo(properties: unknown): ApiMessage | undefined { + if (!isRecord(properties)) return undefined + const message = properties.info ?? properties.message + return isRecord(message) ? (message as ApiMessage) : undefined +} + +// ============================================ +// Background Keepalive +// ============================================ + +/** + * 后台 keepalive:定期检查连接是否还活着 + * 移动端后台时 SSE 连接可能静默断开,timer 也可能被冻结 + * 这个轮询机制可以在 timer 恢复执行时及时发现连接已死 + */ +function startBackgroundKeepalive() { + stopBackgroundKeepalive() + + keepaliveTimer = setInterval(() => { + const now = Date.now() + const timeSinceLastEvent = now - connectionInfo.lastEventTime + const timeout = BACKGROUND_HEARTBEAT_TIMEOUT + + if (import.meta.env.DEV) { + console.log( + `[SSE] Background keepalive check: last event ${Math.round(timeSinceLastEvent / 1000)}s ago, state=${connectionInfo.state}`, + ) + } + + if (connectionInfo.state === 'connected' && timeSinceLastEvent > timeout) { + // 连接声称是 connected,但已经太久没收到事件了 — 连接可能已经静默断开 + console.warn('[SSE] Background keepalive: connection appears dead, forcing reconnect') + + // 断开旧连接 + disconnectTauri() + if (singletonController) { + singletonController.abort() + singletonController = null + } + isConnecting = false + connectionGeneration++ + + updateConnectionState({ state: 'disconnected', error: 'Background keepalive timeout' }) + scheduleReconnect() + } else if (connectionInfo.state === 'disconnected' || connectionInfo.state === 'error') { + // 已知断连状态,但可能 reconnectTimer 被后台冻结了 — 主动触发重连 + if (!reconnectTimer && !isConnecting) { + console.warn('[SSE] Background keepalive: detected stale disconnect, forcing reconnect') + updateConnectionState({ reconnectAttempt: 0 }) + connectSingleton() + } + } + }, BACKGROUND_KEEPALIVE_INTERVAL) +} + +function stopBackgroundKeepalive() { + if (keepaliveTimer) { + clearInterval(keepaliveTimer) + keepaliveTimer = null + } +} + function disconnectSingleton() { if (heartbeatTimer) clearTimeout(heartbeatTimer) if (reconnectTimer) clearTimeout(reconnectTimer) + stopBackgroundKeepalive() + + // Tauri: 调用 Rust 侧断开命令 + disconnectTauri() + + // Browser: abort fetch if (singletonController) { singletonController.abort() singletonController = null } + isConnecting = false updateConnectionState({ state: 'disconnected' }) } +// ============================================ +// Lifecycle Listeners (Visibility + Network) +// ============================================ + +function handleVisibilityChange() { + if (document.visibilityState === 'visible') { + // 页面恢复前台 + isInBackground = false + stopBackgroundKeepalive() + + if (import.meta.env.DEV) { + console.log( + `[SSE] Page became visible, state=${connectionInfo.state}, lastEvent=${Math.round((Date.now() - connectionInfo.lastEventTime) / 1000)}s ago`, + ) + } + + if (allSubscribers.size === 0) return + + if (connectionInfo.state !== 'connected') { + // 明确断连,立即重连 + if (import.meta.env.DEV) { + console.log('[SSE] Page visible: not connected, forcing reconnect...') + } + forceReconnectNow() + } else { + // 状态是 connected,但连接可能已经在后台静默断开 + // 检查最后一次收到事件的时间 + const timeSinceLastEvent = Date.now() - connectionInfo.lastEventTime + if (timeSinceLastEvent > HEARTBEAT_TIMEOUT) { + // 太久没收到事件了,连接大概率已死 + console.warn( + `[SSE] Page visible: connection may be stale (last event ${Math.round(timeSinceLastEvent / 1000)}s ago), forcing reconnect`, + ) + forceReconnectNow() + } else { + // 连接看起来还活着,重置心跳为前台模式 + resetHeartbeat() + } + } + } else { + // 页面进入后台 + isInBackground = true + + if (import.meta.env.DEV) { + console.log('[SSE] Page entering background, switching to background mode') + } + + // 不再清除心跳!保持心跳运行,但切换为后台模式(更长超时) + // 心跳 timer 可能在后台被冻结,但 keepalive 轮询会在 timer 恢复时补上 + resetHeartbeat() + + // 启动后台 keepalive 轮询 + if (allSubscribers.size > 0) { + startBackgroundKeepalive() + } + } +} + +/** + * 强制立即重连:断开旧连接、重置计数器、立即发起新连接 + */ +function forceReconnectNow() { + if (reconnectTimer) clearTimeout(reconnectTimer) + reconnectTimer = null + updateConnectionState({ reconnectAttempt: 0 }) + + // 断开旧连接 + connectionGeneration++ + disconnectTauri() + if (singletonController) { + singletonController.abort() + singletonController = null + } + isConnecting = false + + connectSingleton() +} + +function handleOnline() { + if (import.meta.env.DEV) { + console.log('[SSE] Network online, forcing reconnect...') + } + if (connectionInfo.state !== 'connected' && allSubscribers.size > 0) { + forceReconnectNow() + } +} + +function handleOffline() { + if (import.meta.env.DEV) { + console.log('[SSE] Network offline') + } + // 标记为断连,但不尝试重连(没网重连也没用) + if (connectionInfo.state === 'connected' || connectionInfo.state === 'connecting') { + connectionGeneration++ + disconnectTauri() + if (singletonController) { + singletonController.abort() + singletonController = null + } + isConnecting = false + if (heartbeatTimer) clearTimeout(heartbeatTimer) + if (reconnectTimer) clearTimeout(reconnectTimer) + stopBackgroundKeepalive() + updateConnectionState({ state: 'disconnected', error: 'Network offline' }) + } +} + +function registerLifecycleListeners() { + if (lifecycleListenersRegistered) return + lifecycleListenersRegistered = true + + document.addEventListener('visibilitychange', handleVisibilityChange) + window.addEventListener('online', handleOnline) + window.addEventListener('offline', handleOffline) +} + +function unregisterLifecycleListeners() { + if (!lifecycleListenersRegistered) return + lifecycleListenersRegistered = false + + document.removeEventListener('visibilitychange', handleVisibilityChange) + window.removeEventListener('online', handleOnline) + window.removeEventListener('offline', handleOffline) +} + // 广播事件给所有订阅者 function broadcastEvent(globalEvent: GlobalEvent) { - const { type, properties } = globalEvent.payload - // 广播给所有订阅者 allSubscribers.forEach(callbacks => { - handleEventForSubscriber(type, properties, callbacks) + handleEventForSubscriber(globalEvent.payload, callbacks) }) } -function handleEventForSubscriber( - type: string, - properties: unknown, - callbacks: EventCallbacks -) { - switch (type) { - case 'message.updated': { - const data = properties as { info: ApiMessageWithParts['info'] } - callbacks.onMessageUpdated?.(data.info) +function handleEventForSubscriber(payload: GlobalEvent['payload'], callbacks: EventCallbacks) { + switch (payload.type) { + case EventTypes.MESSAGE_UPDATED: { + const message = getMessageInfo(payload.properties) + if (message) callbacks.onMessageUpdated?.(message) + break + } + case EventTypes.MESSAGE_PART_UPDATED: { + callbacks.onPartUpdated?.(payload.properties.part) break } - case 'message.part.updated': { - const data = properties as { part: ApiPart } - callbacks.onPartUpdated?.(data.part) + case EventTypes.MESSAGE_PART_DELTA: { + callbacks.onPartDelta?.(payload.properties) break } - case 'message.part.removed': - callbacks.onPartRemoved?.(properties as { id: string; messageID: string; sessionID: string }) + case EventTypes.MESSAGE_PART_REMOVED: + callbacks.onPartRemoved?.(payload.properties) + break + case EventTypes.SESSION_UPDATED: { + callbacks.onSessionUpdated?.(payload.properties.info) break - case 'session.updated': { - const data = properties as { info: ApiSession } - callbacks.onSessionUpdated?.(data.info) + } + case EventTypes.SESSION_CREATED: { + callbacks.onSessionCreated?.(payload.properties.info) break } - case 'session.created': { - const data = properties as { info: ApiSession } - callbacks.onSessionCreated?.(data.info) + case EventTypes.SESSION_DELETED: { + callbacks.onSessionDeleted?.(payload.properties.sessionID) break } - case 'session.error': - callbacks.onSessionError?.(properties as { sessionID: string; name: string; data: unknown }) + case EventTypes.PROJECT_UPDATED: { + callbacks.onProjectUpdated?.(payload.properties) break - case 'session.idle': - callbacks.onSessionIdle?.(properties as { sessionID: string }) + } + case EventTypes.SESSION_ERROR: + callbacks.onSessionError?.(normalizeSessionError(payload.properties)) break - case 'permission.asked': - callbacks.onPermissionAsked?.(properties as ApiPermissionRequest) + case EventTypes.SESSION_IDLE: + callbacks.onSessionIdle?.(payload.properties) break - case 'permission.replied': - callbacks.onPermissionReplied?.(properties as { sessionID: string; requestID: string; reply: PermissionReply }) + case EventTypes.SESSION_STATUS: + callbacks.onSessionStatus?.(payload.properties) break - case 'question.asked': - callbacks.onQuestionAsked?.(properties as ApiQuestionRequest) + case EventTypes.PERMISSION_ASKED: + callbacks.onPermissionAsked?.(payload.properties) break - case 'question.replied': - callbacks.onQuestionReplied?.(properties as { sessionID: string; requestID: string; answers: string[][] }) + case EventTypes.PERMISSION_REPLIED: + callbacks.onPermissionReplied?.(payload.properties) break - case 'question.rejected': - callbacks.onQuestionRejected?.(properties as { sessionID: string; requestID: string }) + case EventTypes.QUESTION_ASKED: + callbacks.onQuestionAsked?.(payload.properties) break - case 'worktree.ready': - callbacks.onWorktreeReady?.(properties as WorktreeReadyPayload) + case EventTypes.QUESTION_REPLIED: + callbacks.onQuestionReplied?.(payload.properties) break - case 'worktree.failed': - callbacks.onWorktreeFailed?.(properties as WorktreeFailedPayload) + case EventTypes.QUESTION_REJECTED: + callbacks.onQuestionRejected?.(payload.properties) break - case 'vcs.branch.updated': - callbacks.onVcsBranchUpdated?.(properties as VcsBranchUpdatedPayload) + case EventTypes.WORKTREE_READY: + callbacks.onWorktreeReady?.(payload.properties) break - case 'todo.updated': - callbacks.onTodoUpdated?.(properties as TodoUpdatedPayload) + case EventTypes.WORKTREE_FAILED: + callbacks.onWorktreeFailed?.(payload.properties) + break + case EventTypes.VCS_BRANCH_UPDATED: + callbacks.onVcsBranchUpdated?.(payload.properties) + break + case EventTypes.TODO_UPDATED: { + callbacks.onTodoUpdated?.({ + sessionID: payload.properties.sessionID, + todos: normalizeTodoItems(payload.properties.todos), + } satisfies TodoUpdatedPayload) + break + } + case EventTypes.SERVER_CONNECTED: + callbacks.onServerConnected?.(normalizeServerConnected(payload.properties)) break default: // 忽略其他事件类型 @@ -297,28 +764,113 @@ function handleEventForSubscriber( } } +function normalizeServerConnected(properties: unknown): ServerConnectedPayload { + if (!isRecord(properties)) return {} + return { + timestamp: properties.timestamp, + } +} + +function normalizeSessionError(properties: unknown): SessionErrorPayload { + if (!isRecord(properties)) { + return { sessionID: '', name: 'UnknownError', data: properties } + } + + const sessionID = typeof properties.sessionID === 'string' ? properties.sessionID : '' + + if (typeof properties.name === 'string') { + return { + sessionID, + name: properties.name, + data: properties.data, + } + } + + const sdkError = properties.error + if (isRecord(sdkError)) { + return { + sessionID, + name: typeof sdkError.name === 'string' ? sdkError.name : 'UnknownError', + data: 'data' in sdkError ? sdkError.data : sdkError, + } + } + + return { + sessionID, + name: 'UnknownError', + data: sdkError, + } +} + // ============================================ // Public API // ============================================ +/** + * 强制重连 SSE(用于切换服务器等场景) + * 断开当前连接 → 重置状态 → 立即重连(新 URL 由 getApiBaseUrl() 动态解析) + */ +export function reconnectSSE() { + if (allSubscribers.size === 0) return // 没有订阅者不需要重连 + + if (import.meta.env.DEV) { + console.log('[SSE] reconnectSSE() called, forcing reconnect to new server...') + } + + // 断开现有连接 + if (heartbeatTimer) clearTimeout(heartbeatTimer) + if (reconnectTimer) clearTimeout(reconnectTimer) + reconnectTimer = null + stopBackgroundKeepalive() + + // 标记为服务器切换,重连成功时 onReconnected 会携带 'server-switch' reason + isServerSwitch = true + + // 递增连接代次,使旧连接的事件回调自动失效 + connectionGeneration++ + + disconnectTauri() + if (singletonController) { + singletonController.abort() + singletonController = null + } + isConnecting = false + + // 重置重连计数 + updateConnectionState({ + state: 'disconnected', + reconnectAttempt: 0, + error: undefined, + }) + + // 立即重连(getApiBaseUrl() 会读取新的 activeServer) + connectSingleton() +} + +export function disconnectSSE(error?: string) { + disconnectSingleton() + updateConnectionState({ state: error ? 'error' : 'disconnected', error, reconnectAttempt: 0 }) +} + /** * 订阅 SSE 事件(单例模式,多个订阅者共享一个连接) */ export function subscribeToEvents(callbacks: EventCallbacks): () => void { allSubscribers.add(callbacks) - + // 如果是第一个订阅者,启动连接 if (allSubscribers.size === 1) { connectSingleton() } - + // 返回取消订阅函数 return () => { allSubscribers.delete(callbacks) - - // 如果没有订阅者了,断开连接 + + // 如果没有订阅者了,断开连接并清理监听器 if (allSubscribers.size === 0) { disconnectSingleton() + unregisterLifecycleListeners() } } } diff --git a/src/api/file.ts b/src/api/file.ts index 7ed06a0f..b83c1c96 100644 --- a/src/api/file.ts +++ b/src/api/file.ts @@ -1,18 +1,39 @@ // ============================================ // File Search API Functions -// 基于 OpenAPI: /file, /find/file, /find/symbol 相关接口 +// 基于 @opencode-ai/sdk: /file, /find/file, /find/symbol 相关接口 // ============================================ -import { get } from './http' +import { getSDKClient, unwrap } from './sdk' import { formatPathForApi } from '../utils/directoryUtils' import type { FileNode, FileContent, FileStatusItem, SymbolInfo } from './types' +import { serverStore } from '../store/serverStore' + +const ROOT_DIRECTORY_CACHE_TTL_MS = 10_000 + +const rootDirectoryCache = new Map() +const rootDirectoryInflight = new Map>() + +function isRootDirectoryPath(path: string): boolean { + return path === '' || path === '.' || path === './' +} + +function getRootDirectoryCacheKey(directory?: string): string { + return `${serverStore.getActiveServerId()}::${formatPathForApi(directory) ?? ''}` +} + +async function fetchDirectory(path: string, directory?: string): Promise { + const sdk = getSDKClient() + const isAbsolute = /^[a-zA-Z]:/.test(path) || path.startsWith('/') + + if (isAbsolute && !directory) { + return unwrap(await sdk.file.list({ directory: formatPathForApi(path), path: '' })) + } + + return unwrap(await sdk.file.list({ path, directory: formatPathForApi(directory) })) +} /** - * GET /find/file - 搜索文件或目录 - * @param query 搜索关键词 - * @param options.directory 工作目录(项目目录) - * @param options.type 搜索类型:file 或 directory - * @param options.limit 返回结果数量限制 + * 搜索文件或目录 */ export async function searchFiles( query: string, @@ -20,76 +41,84 @@ export async function searchFiles( directory?: string type?: 'file' | 'directory' limit?: number - } = {} + } = {}, ): Promise { - return get('/find/file', { - query, - directory: formatPathForApi(options.directory), - type: options.type, - limit: options.limit, - }) + const sdk = getSDKClient() + return unwrap( + await sdk.find.files({ + query, + directory: formatPathForApi(options.directory), + type: options.type, + limit: options.limit, + }), + ) as string[] } /** - * GET /file - 列出目录内容 - * @param path 要列出的路径(相对于 directory) - * @param directory 工作目录(项目目录) + * 列出目录内容 */ export async function listDirectory(path: string, directory?: string): Promise { - // 智能处理:如果 path 是绝对路径,将其作为 directory 传递,path 设为空 - // Windows: C: 或 C:/ 开头 - // Unix: / 开头 - const isAbsolute = /^[a-zA-Z]:/.test(path) || path.startsWith('/') - - if (isAbsolute && !directory) { - return get('/file', { directory: formatPathForApi(path), path: '' }) - } else { - return get('/file', { path, directory: formatPathForApi(directory) }) + if (!isRootDirectoryPath(path)) { + return fetchDirectory(path, directory) + } + + const key = getRootDirectoryCacheKey(directory) + const now = Date.now() + const cached = rootDirectoryCache.get(key) + if (cached && cached.expiresAt > now) { + return cached.data } + + const inflight = rootDirectoryInflight.get(key) + if (inflight) { + return inflight + } + + const request = fetchDirectory(path === '' ? '.' : path, directory) + .then(data => { + rootDirectoryCache.set(key, { data, expiresAt: Date.now() + ROOT_DIRECTORY_CACHE_TTL_MS }) + return data + }) + .finally(() => { + rootDirectoryInflight.delete(key) + }) + + rootDirectoryInflight.set(key, request) + return request +} + +export async function prefetchRootDirectory(directory?: string): Promise { + await listDirectory('.', directory) } /** - * GET /file/content - 读取文件内容 - * @param path 文件路径(相对于 directory) - * @param directory 工作目录(项目目录) + * 读取文件内容 */ export async function getFileContent(path: string, directory?: string): Promise { - return get('/file/content', { - path, - directory: formatPathForApi(directory), - }) + const sdk = getSDKClient() + return unwrap(await sdk.file.read({ path, directory: formatPathForApi(directory) })) } /** - * GET /file/status - 获取文件 git 状态 - * @param directory 工作目录(项目目录) + * 获取文件 git 状态 */ export async function getFileStatus(directory?: string): Promise { - return get('/file/status', { - directory: formatPathForApi(directory), - }) + const sdk = getSDKClient() + return unwrap(await sdk.file.status({ directory: formatPathForApi(directory) })) } /** - * GET /find/symbol - 搜索代码符号 - * @param query 搜索关键词 - * @param directory 工作目录(项目目录) + * 搜索代码符号 */ export async function searchSymbols(query: string, directory?: string): Promise { - return get('/find/symbol', { query, directory: formatPathForApi(directory) }) + const sdk = getSDKClient() + return unwrap(await sdk.find.symbols({ query, directory: formatPathForApi(directory) })) } /** * 搜索目录(便捷方法) - * @param query 搜索关键词 - * @param baseDirectory 基础目录(从哪里开始搜索) - * @param limit 返回结果数量限制 */ -export async function searchDirectories( - query: string, - baseDirectory?: string, - limit: number = 50 -): Promise { +export async function searchDirectories(query: string, baseDirectory?: string, limit: number = 50): Promise { return searchFiles(query, { directory: baseDirectory, type: 'directory', diff --git a/src/api/global.ts b/src/api/global.ts index 27a649a5..2b07e95a 100644 --- a/src/api/global.ts +++ b/src/api/global.ts @@ -2,31 +2,32 @@ // Global API - 全局管理 // ============================================ -import { get, post } from './http' +import type { GlobalHealthResponse as HealthInfo } from '@opencode-ai/sdk/v2/client' +import { getSDKClient, unwrap } from './sdk' import { formatPathForApi } from '../utils/directoryUtils' -export interface HealthInfo { - healthy: boolean - version: string -} - /** * 获取服务器健康状态 */ export async function getHealth(): Promise { - return get('/global/health') + const sdk = getSDKClient() + return unwrap(await sdk.global.health()) } /** * 释放所有资源 */ export async function disposeGlobal(): Promise { - return post('/global/dispose') + const sdk = getSDKClient() + unwrap(await sdk.global.dispose()) + return true } /** * 释放当前实例 */ export async function disposeInstance(directory?: string): Promise { - return post('/instance/dispose', { directory: formatPathForApi(directory) }) + const sdk = getSDKClient() + unwrap(await sdk.instance.dispose({ directory: formatPathForApi(directory) })) + return true } diff --git a/src/api/http.test.ts b/src/api/http.test.ts new file mode 100644 index 00000000..99ee10be --- /dev/null +++ b/src/api/http.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, it } from 'vitest' +import { buildQueryString } from './http' + +// ============================================ +// buildQueryString 测试 +// ============================================ + +describe('buildQueryString', () => { + it('returns empty string for empty params', () => { + expect(buildQueryString({})).toBe('') + }) + + it('skips undefined values', () => { + expect(buildQueryString({ a: 'hello', b: undefined })).toBe('?a=hello') + }) + + it('handles string, number, boolean values', () => { + const result = buildQueryString({ name: 'test', page: 1, active: true }) + expect(result).toBe('?name=test&page=1&active=true') + }) + + it('encodes special characters in values', () => { + const result = buildQueryString({ directory: 'C:\\Program Files\\app' }) + expect(result).toBe('?directory=C%3A%5CProgram%20Files%5Capp') + }) + + it('encodes special characters in keys', () => { + const result = buildQueryString({ 'key with spaces': 'value' }) + expect(result).toBe('?key%20with%20spaces=value') + }) + + it('encodes ampersand and equals in values', () => { + const result = buildQueryString({ q: 'a=1&b=2' }) + expect(result).toBe('?q=a%3D1%26b%3D2') + }) + + it('encodes unicode characters', () => { + const result = buildQueryString({ path: '/home/用户/project' }) + expect(result).toBe('?path=%2Fhome%2F%E7%94%A8%E6%88%B7%2Fproject') + }) +}) diff --git a/src/api/http.ts b/src/api/http.ts index 184f350d..0c4768ba 100644 --- a/src/api/http.ts +++ b/src/api/http.ts @@ -1,9 +1,8 @@ // ============================================ -// HTTP Client Utilities -// 统一的 HTTP 请求工具 +// HTTP Utilities +// 仅保留给 SSE / PTY WebSocket 使用的 URL 与 auth 辅助函数 // ============================================ -import { API_BASE_URL } from '../constants' import { serverStore, makeBasicAuthHeader } from '../store/serverStore' /** @@ -16,173 +15,29 @@ export function getApiBaseUrl(): string { /** * 获取当前活动服务器的 Authorization header - * 如果有认证信息则返回 Basic Auth header,否则返回 undefined + * 如果服务器配置了密码则返回 Basic Auth header,否则返回 undefined */ -export function getAuthHeader(): string | undefined { +export function getAuthHeader(): Record { const auth = serverStore.getActiveAuth() - if (auth) { - return makeBasicAuthHeader(auth) + if (auth?.password) { + return { Authorization: makeBasicAuthHeader(auth) } } - return undefined + return {} } -/** @deprecated 使用 getApiBaseUrl() 代替 */ -export const API_BASE = API_BASE_URL - -// ============================================ -// URL Building -// ============================================ - type QueryValue = string | number | boolean | undefined /** - * 构建查询字符串(不进行 URL 编码,直接拼接) - * 后端不解码,所以我们不编码 + * 构建查询字符串 + * 值会进行 URL 编码以安全处理空格、特殊字符等 + * (Go 后端的 r.URL.Query().Get() 会自动解码) */ export function buildQueryString(params: Record): string { const parts: string[] = [] for (const [key, value] of Object.entries(params)) { if (value !== undefined) { - parts.push(`${key}=${value}`) + parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`) } } return parts.length > 0 ? '?' + parts.join('&') : '' } - -/** - * 构建完整 URL - */ -export function buildUrl( - path: string, - params: Record = {} -): string { - return `${getApiBaseUrl()}${path}${buildQueryString(params)}` -} - -// ============================================ -// HTTP Methods -// ============================================ - -export interface RequestOptions { - method?: 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE' - body?: unknown - headers?: Record -} - -/** - * 通用 HTTP 请求函数 - * - * 直接请求后端服务器,支持 CORS 跨域 - * - 后端需要设置 Access-Control-Allow-Origin - * - 如果需要发送凭证(cookies),后端还需设置 Access-Control-Allow-Credentials - */ -export async function request( - path: string, - params: Record = {}, - options: RequestOptions = {} -): Promise { - const { method = 'GET', body, headers = {} } = options - - // 自动添加认证 header - const authHeader = getAuthHeader() - const requestHeaders: Record = { - ...headers, - } - if (authHeader) { - requestHeaders['Authorization'] = authHeader - } - - const init: RequestInit = { - method, - headers: requestHeaders, - } - - if (body !== undefined) { - init.headers = { - ...init.headers, - 'Content-Type': 'application/json', - } - init.body = JSON.stringify(body) - } - - const response = await fetch(buildUrl(path, params), init) - - if (!response.ok) { - let errorMsg = `Request failed: ${response.status}` - try { - const errorText = await response.text() - if (errorText) { - errorMsg += ` - ${errorText}` - } - } catch { - // ignore - } - throw new Error(errorMsg) - } - - // 204 No Content - if (response.status === 204) { - return undefined as T - } - - const text = await response.text() - if (!text) { - return undefined as T - } - - return JSON.parse(text) -} - -/** - * GET 请求 - */ -export async function get( - path: string, - params: Record = {} -): Promise { - return request(path, params, { method: 'GET' }) -} - -/** - * POST 请求 - */ -export async function post( - path: string, - params: Record = {}, - body?: unknown -): Promise { - return request(path, params, { method: 'POST', body }) -} - -/** - * PATCH 请求 - */ -export async function patch( - path: string, - params: Record = {}, - body?: unknown -): Promise { - return request(path, params, { method: 'PATCH', body }) -} - -/** - * PUT 请求 - */ -export async function put( - path: string, - params: Record = {}, - body?: unknown -): Promise { - return request(path, params, { method: 'PUT', body }) -} - -/** - * DELETE 请求 - */ -export async function del( - path: string, - params: Record = {}, - body?: unknown -): Promise { - return request(path, params, { method: 'DELETE', body }) -} diff --git a/src/api/lsp.ts b/src/api/lsp.ts index ac23c5dd..f924284f 100644 --- a/src/api/lsp.ts +++ b/src/api/lsp.ts @@ -2,7 +2,8 @@ // LSP API - Language Server Protocol 状态 // ============================================ -import { get } from './http' +import type { FormatterStatus as SDKFormatterStatus, LspStatus as SDKLspStatus } from '@opencode-ai/sdk/v2/client' +import { getSDKClient, unwrap } from './sdk' import { formatPathForApi } from '../utils/directoryUtils' export interface LSPStatus { @@ -15,7 +16,17 @@ export interface LSPStatus { * 获取 LSP 服务状态 */ export async function getLspStatus(directory?: string): Promise { - return get('/lsp', { directory: formatPathForApi(directory) }) + const sdk = getSDKClient() + const result = unwrap(await sdk.lsp.status({ directory: formatPathForApi(directory) })) + // SDK 返回 LspStatus[](id/name/root/status),转换为 UI 层期望的格式 + if (Array.isArray(result) && result.length > 0) { + const first = result[0] + return { + running: first.status === 'connected', + language: first.name, + } + } + return { running: false } } export interface FormatterStatus { @@ -27,5 +38,15 @@ export interface FormatterStatus { * 获取格式化器状态 */ export async function getFormatterStatus(directory?: string): Promise { - return get('/formatter', { directory: formatPathForApi(directory) }) + const sdk = getSDKClient() + const result = unwrap(await sdk.formatter.status({ directory: formatPathForApi(directory) })) + // SDK 返回 FormatterStatus[](name/extensions/enabled),转换为 UI 层期望的格式 + if (Array.isArray(result) && result.length > 0) { + const first = result[0] + return { + available: first.enabled === true, + name: first.name, + } + } + return { available: false } } diff --git a/src/api/mcp.ts b/src/api/mcp.ts index 2650fa02..dc3f33eb 100644 --- a/src/api/mcp.ts +++ b/src/api/mcp.ts @@ -2,73 +2,72 @@ // MCP API - Model Context Protocol 服务器管理 // ============================================ -import { get, post, del } from './http' +import { getSDKClient, unwrap } from './sdk' import { formatPathForApi } from '../utils/directoryUtils' -import type { - MCPStatusResponse, - McpServerConfig, -} from '../types/api/mcp' +import type { MCPStatusResponse, McpServerConfig } from '../types/api/mcp' /** * 获取所有 MCP 服务器状态 */ export async function getMcpStatus(directory?: string): Promise { - return get('/mcp', { directory: formatPathForApi(directory) }) + const sdk = getSDKClient() + return unwrap(await sdk.mcp.status({ directory: formatPathForApi(directory) })) } /** * 添加 MCP 服务器 */ -export async function addMcpServer( - name: string, - config: McpServerConfig, - directory?: string -): Promise { - return post('/mcp', { directory: formatPathForApi(directory) }, { name, config }) +export async function addMcpServer(name: string, config: McpServerConfig, directory?: string): Promise { + const sdk = getSDKClient() + unwrap(await sdk.mcp.add({ name, config, directory: formatPathForApi(directory) })) } /** * 连接到 MCP 服务器 */ export async function connectMcpServer(name: string, directory?: string): Promise { - return post(`/mcp/${encodeURIComponent(name)}/connect`, { directory: formatPathForApi(directory) }) + const sdk = getSDKClient() + unwrap(await sdk.mcp.connect({ name, directory: formatPathForApi(directory) })) } /** * 断开 MCP 服务器连接 */ export async function disconnectMcpServer(name: string, directory?: string): Promise { - return post(`/mcp/${encodeURIComponent(name)}/disconnect`, { directory: formatPathForApi(directory) }) + const sdk = getSDKClient() + unwrap(await sdk.mcp.disconnect({ name, directory: formatPathForApi(directory) })) } /** * 开始 MCP 认证流程 */ export async function startMcpAuth(name: string, directory?: string): Promise<{ url: string }> { - return post<{ url: string }>(`/mcp/${encodeURIComponent(name)}/auth`, { directory: formatPathForApi(directory) }) + const sdk = getSDKClient() + const result = unwrap(await sdk.mcp.auth.start({ name, directory: formatPathForApi(directory) })) + // SDK 返回 { authorizationUrl: string },转换为我们期望的 { url: string } + return { url: result.authorizationUrl } } /** * 移除 MCP 认证 */ export async function removeMcpAuth(name: string, directory?: string): Promise { - return del(`/mcp/${encodeURIComponent(name)}/auth`, { directory: formatPathForApi(directory) }) + const sdk = getSDKClient() + unwrap(await sdk.mcp.auth.remove({ name, directory: formatPathForApi(directory) })) } /** * 完成 MCP OAuth 认证(使用授权码) */ export async function completeMcpAuth(name: string, code: string, directory?: string): Promise { - return post( - `/mcp/${encodeURIComponent(name)}/auth/callback`, - { directory: formatPathForApi(directory) }, - { code } - ) + const sdk = getSDKClient() + unwrap(await sdk.mcp.auth.callback({ name, code, directory: formatPathForApi(directory) })) } /** - * 启动完整的 OAuth 认证流程(打开浏览器并等待回调) + * 启动完整的 OAuth 认证流程 */ export async function authenticateMcp(name: string, directory?: string): Promise { - return post(`/mcp/${encodeURIComponent(name)}/auth/authenticate`, { directory: formatPathForApi(directory) }) + const sdk = getSDKClient() + unwrap(await sdk.mcp.auth.authenticate({ name, directory: formatPathForApi(directory) })) } diff --git a/src/api/message.ts b/src/api/message.ts index 566bdafe..acb4cf67 100644 --- a/src/api/message.ts +++ b/src/api/message.ts @@ -1,37 +1,68 @@ // ============================================ // Message API Functions -// 基于 OpenAPI: /session/{sessionID}/message 相关接口 +// 基于 @opencode-ai/sdk: /session/{sessionID}/message 相关接口 // ============================================ -import { get, post } from './http' +import { getSDKClient, unwrap } from './sdk' import { formatPathForApi } from '../utils/directoryUtils' import type { ApiMessageWithParts, + AgentPartInput, + ApiAgentPart, ApiTextPart, ApiFilePart, - ApiAgentPart, Attachment, + FilePartInput, RevertedMessage, SendMessageParams, SendMessageResponse, + TextPartInput, } from './types' +type PromptParams = Parameters['session']['prompt']>[0] +type UserContentSource = { + parts: Array< + | ApiTextPart + | ApiFilePart + | ApiAgentPart + | { + type: string + } + > +} + +function isTextUserContentPart(part: UserContentSource['parts'][number]): part is ApiTextPart { + return part.type === 'text' && 'text' in part +} + +function isFileUserContentPart(part: UserContentSource['parts'][number]): part is ApiFilePart { + return part.type === 'file' && 'mime' in part && 'url' in part +} + +function isAgentUserContentPart(part: UserContentSource['parts'][number]): part is ApiAgentPart { + return part.type === 'agent' && 'name' in part +} + // ============================================ // Message Query // ============================================ /** - * GET /session/{sessionID}/message - 获取 session 的消息列表 + * 获取 session 的消息列表 */ export async function getSessionMessages( - sessionId: string, + sessionId: string, limit?: number, - directory?: string + directory?: string, ): Promise { - return get(`/session/${sessionId}/message`, { - directory: formatPathForApi(directory), - limit - }) + const sdk = getSDKClient() + return unwrap( + await sdk.session.messages({ + sessionID: sessionId, + directory: formatPathForApi(directory), + limit, + }), + ) } /** @@ -49,47 +80,55 @@ export async function getSessionMessageCount(sessionId: string): Promise /** * 从 API 消息中提取用户消息内容(文本+附件) */ -export function extractUserMessageContent(apiMessage: ApiMessageWithParts): RevertedMessage { - const { parts } = apiMessage - - const textParts = parts.filter((p): p is ApiTextPart => p.type === 'text' && !p.synthetic) +export function extractUserMessageContent(message: UserContentSource): RevertedMessage { + const { parts } = message + + const textParts = parts.filter((part): part is ApiTextPart => isTextUserContentPart(part) && !part.synthetic) const text = textParts.map(p => p.text).join('\n') - + const attachments: Attachment[] = [] - + + const getSourcePath = (source: ApiFilePart['source']): string | undefined => { + if (!source || !('path' in source)) return undefined + return source.path + } + for (const part of parts) { - if (part.type === 'file') { - const fp = part as ApiFilePart - const isFolder = fp.mime === 'application/x-directory' + if (isFileUserContentPart(part)) { + const isFolder = part.mime === 'application/x-directory' + const sourcePath = getSourcePath(part.source) attachments.push({ - id: fp.id || crypto.randomUUID(), + id: part.id || crypto.randomUUID(), type: isFolder ? 'folder' : 'file', - displayName: fp.filename || fp.source?.path || 'file', - url: fp.url, - mime: fp.mime, - relativePath: fp.source?.path, - textRange: fp.source?.text ? { - value: fp.source.text.value, - start: fp.source.text.start, - end: fp.source.text.end, - } : undefined, + displayName: part.filename || sourcePath || 'file', + url: part.url, + mime: part.mime, + relativePath: sourcePath, + textRange: part.source?.text + ? { + value: part.source.text.value, + start: part.source.text.start, + end: part.source.text.end, + } + : undefined, }) - } else if (part.type === 'agent') { - const ap = part as ApiAgentPart + } else if (isAgentUserContentPart(part)) { attachments.push({ - id: ap.id || crypto.randomUUID(), + id: part.id || crypto.randomUUID(), type: 'agent', - displayName: ap.name, - agentName: ap.name, - textRange: ap.source ? { - value: ap.source.value, - start: ap.source.start, - end: ap.source.end, - } : undefined, + displayName: part.name, + agentName: part.name, + textRange: part.source + ? { + value: part.source.value, + start: part.source.start, + end: part.source.end, + } + : undefined, }) } } - + return { text, attachments } } @@ -102,15 +141,15 @@ export function extractUserMessageContent(apiMessage: ApiMessageWithParts): Reve */ function toFileUrl(path: string): string { if (!path) return '' - + if (path.startsWith('file://')) { return path } - + if (path.startsWith('data:')) { return path } - + const normalized = path.replace(/\\/g, '/') if (/^[a-zA-Z]:/.test(normalized)) { return `file:///${normalized}` @@ -122,70 +161,85 @@ function toFileUrl(path: string): string { } /** - * POST /session/{sessionID}/message - 发送消息 + * 构建 SDK 发送消息所需的参数 */ -export async function sendMessage(params: SendMessageParams): Promise { +function buildPromptParams(params: SendMessageParams): PromptParams { const { sessionId, text, attachments, model, agent, variant, directory } = params - const parts: Array<{ type: string; [key: string]: unknown }> = [] - + const parts: NonNullable = [] + // 文本 part - parts.push({ + const textPart: TextPartInput = { type: 'text', text, - }) - + } + parts.push(textPart) + // 附件 parts for (const attachment of attachments) { if (attachment.type === 'agent') { - parts.push({ + const agentPart: AgentPartInput = { type: 'agent', - name: attachment.agentName, - source: attachment.textRange ? { - value: attachment.textRange.value, - start: attachment.textRange.start, - end: attachment.textRange.end, - } : undefined, - }) + name: attachment.agentName || attachment.displayName, + source: attachment.textRange + ? { + value: attachment.textRange.value, + start: attachment.textRange.start, + end: attachment.textRange.end, + } + : undefined, + } + parts.push(agentPart) } else { const fileUrl = toFileUrl(attachment.url || '') if (!fileUrl) { console.warn('Skipping attachment with empty URL:', attachment) continue } - - parts.push({ + + const filePart: FilePartInput = { type: 'file', mime: attachment.mime || (attachment.type === 'folder' ? 'application/x-directory' : 'text/plain'), url: fileUrl, filename: attachment.displayName, - source: attachment.textRange ? { - text: { - value: attachment.textRange.value, - start: attachment.textRange.start, - end: attachment.textRange.end, - }, - type: 'file', - path: attachment.relativePath || attachment.displayName, - } : undefined, - }) + source: attachment.textRange + ? { + text: { + value: attachment.textRange.value, + start: attachment.textRange.start, + end: attachment.textRange.end, + }, + type: 'file', + path: attachment.relativePath || attachment.displayName, + } + : undefined, + } + parts.push(filePart) } } - const requestBody: Record = { + return { + sessionID: sessionId, + directory: formatPathForApi(directory), parts, model, + agent, + variant, } - - if (agent) { - requestBody.agent = agent - } - - if (variant) { - requestBody.variant = variant - } +} - return post(`/session/${sessionId}/message`, { - directory: formatPathForApi(directory) - }, requestBody) +/** + * 同步发送消息(等待完成) + */ +export async function sendMessage(params: SendMessageParams): Promise { + const sdk = getSDKClient() + return unwrap(await sdk.session.prompt(buildPromptParams(params))) +} + +/** + * 异步发送消息 — 立即返回,AI 响应通过 SSE 推送 + */ +export async function sendMessageAsync(params: SendMessageParams): Promise { + const sdk = getSDKClient() + unwrap(await sdk.session.promptAsync(buildPromptParams(params))) } diff --git a/src/api/permission.ts b/src/api/permission.ts index 647f9c2e..1b6cc593 100644 --- a/src/api/permission.ts +++ b/src/api/permission.ts @@ -1,49 +1,58 @@ // ============================================ // Permission & Question API Functions -// 基于 OpenAPI: /permission, /question 相关接口 +// 基于 @opencode-ai/sdk: /permission, /question 相关接口 // ============================================ -import { get, post } from './http' +import { getSDKClient, unwrap } from './sdk' import { formatPathForApi } from '../utils/directoryUtils' -import type { - ApiPermissionRequest, - PermissionReply, - ApiQuestionRequest, - QuestionAnswer, -} from './types' +import type { ApiPermissionRequest, PermissionReply, ApiQuestionRequest, QuestionAnswer } from './types' // ============================================ // Permission API // ============================================ /** - * GET /permission - 获取待处理的权限请求列表 - * directory 会根据 pathMode 自动转换格式 + * 获取待处理的权限请求列表 */ -export async function getPendingPermissions( - sessionId?: string, - directory?: string -): Promise { - const permissions = await get('/permission', { - directory: formatPathForApi(directory) - }) - return sessionId - ? permissions.filter((p: ApiPermissionRequest) => p.sessionID === sessionId) - : permissions +export async function getPendingPermissions(sessionId?: string, directory?: string): Promise { + const sdk = getSDKClient() + const permissions = unwrap(await sdk.permission.list({ directory: formatPathForApi(directory) })) + return sessionId ? permissions.filter((p: ApiPermissionRequest) => p.sessionID === sessionId) : permissions } /** - * POST /permission/{requestID}/reply - 回复权限请求 + * 回复权限请求 */ export async function replyPermission( requestId: string, reply: PermissionReply, message?: string, - directory?: string + directory?: string, + sessionId?: string, ): Promise { - return post(`/permission/${requestId}/reply`, { - directory: formatPathForApi(directory) - }, { reply, message }) + const sdk = getSDKClient() + + if (sessionId) { + unwrap( + await sdk.permission.respond({ + sessionID: sessionId, + permissionID: requestId, + directory: formatPathForApi(directory), + response: reply, + }), + ) + return true + } + + unwrap( + await sdk.permission.reply({ + requestID: requestId, + directory: formatPathForApi(directory), + reply, + message, + }), + ) + return true } // ============================================ @@ -51,42 +60,43 @@ export async function replyPermission( // ============================================ /** - * GET /question - 获取待处理的问题请求列表 - * directory 会根据 pathMode 自动转换格式 + * 获取待处理的问题请求列表 */ -export async function getPendingQuestions( - sessionId?: string, - directory?: string -): Promise { - const questions = await get('/question', { - directory: formatPathForApi(directory) - }) - return sessionId - ? questions.filter((q: ApiQuestionRequest) => q.sessionID === sessionId) - : questions +export async function getPendingQuestions(sessionId?: string, directory?: string): Promise { + const sdk = getSDKClient() + const questions = unwrap(await sdk.question.list({ directory: formatPathForApi(directory) })) + return sessionId ? questions.filter((q: ApiQuestionRequest) => q.sessionID === sessionId) : questions } /** - * POST /question/{requestID}/reply - 回复问题请求 + * 回复问题请求 */ export async function replyQuestion( requestId: string, answers: QuestionAnswer[], - directory?: string + directory?: string, ): Promise { - return post(`/question/${requestId}/reply`, { - directory: formatPathForApi(directory) - }, { answers }) + const sdk = getSDKClient() + unwrap( + await sdk.question.reply({ + requestID: requestId, + directory: formatPathForApi(directory), + answers, + }), + ) + return true } /** - * POST /question/{requestID}/reject - 拒绝问题请求 + * 拒绝问题请求 */ -export async function rejectQuestion( - requestId: string, - directory?: string -): Promise { - return post(`/question/${requestId}/reject`, { - directory: formatPathForApi(directory) - }) +export async function rejectQuestion(requestId: string, directory?: string): Promise { + const sdk = getSDKClient() + unwrap( + await sdk.question.reject({ + requestID: requestId, + directory: formatPathForApi(directory), + }), + ) + return true } diff --git a/src/api/pty.ts b/src/api/pty.ts index fc00979d..92b35f9b 100644 --- a/src/api/pty.ts +++ b/src/api/pty.ts @@ -2,65 +2,137 @@ // PTY API - 终端管理 // ============================================ -import { get, post, put, del, getApiBaseUrl, buildQueryString } from './http' +import { getSDKClient, unwrap } from './sdk' +import { getApiBaseUrl, buildQueryString } from './http' import { formatPathForApi } from '../utils/directoryUtils' +import { serverStore } from '../store/serverStore' import type { Pty, PtyCreateParams, PtyUpdateParams } from '../types/api/pty' +type LegacyPty = Pty & { running?: boolean; status?: Pty['status'] } +export interface ShellInfo { + path: string + name: string + acceptable: boolean +} + +interface PtyConnectUrlOptions { + /** + * false = 不在 URL 里放认证(Tauri bridge 通过 header 传) + * true = 在 URL 里放认证(浏览器原生 WebSocket 无法设 header) + */ + includeAuthInUrl?: boolean + cursor?: number +} + +function normalizePty(pty: LegacyPty): Pty { + if (pty.status) return pty as Pty + return { + ...pty, + status: pty.running ? 'running' : 'exited', + } as Pty +} + /** * 获取所有 PTY 会话列表 */ export async function listPtySessions(directory?: string): Promise { - return get('/pty', { directory: formatPathForApi(directory) }) + const sdk = getSDKClient() + return unwrap(await sdk.pty.list({ directory: formatPathForApi(directory) })).map(pty => + normalizePty(pty as LegacyPty), + ) +} + +/** + * 获取当前机器可用 shell 列表,用于 opencode config.shell 的候选项。 + */ +export async function listAvailableShells(directory?: string): Promise { + const sdk = getSDKClient() + return unwrap(await sdk.pty.shells({ directory: formatPathForApi(directory) })) } /** * 创建新的 PTY 会话 */ -export async function createPtySession( - params: PtyCreateParams, - directory?: string -): Promise { - return post('/pty', { directory: formatPathForApi(directory) }, params) +export async function createPtySession(params: PtyCreateParams, directory?: string): Promise { + const sdk = getSDKClient() + return normalizePty(unwrap(await sdk.pty.create({ directory: formatPathForApi(directory), ...params })) as LegacyPty) } /** * 获取单个 PTY 会话信息 */ export async function getPtySession(ptyId: string, directory?: string): Promise { - return get(`/pty/${ptyId}`, { directory: formatPathForApi(directory) }) + const sdk = getSDKClient() + return normalizePty(unwrap(await sdk.pty.get({ ptyID: ptyId, directory: formatPathForApi(directory) })) as LegacyPty) } /** * 更新 PTY 会话 */ -export async function updatePtySession( - ptyId: string, - params: PtyUpdateParams, - directory?: string -): Promise { - return put(`/pty/${ptyId}`, { directory: formatPathForApi(directory) }, params) +export async function updatePtySession(ptyId: string, params: PtyUpdateParams, directory?: string): Promise { + const sdk = getSDKClient() + return normalizePty( + unwrap(await sdk.pty.update({ ptyID: ptyId, directory: formatPathForApi(directory), ...params })) as LegacyPty, + ) } /** * 删除 PTY 会话 */ export async function removePtySession(ptyId: string, directory?: string): Promise { - return del(`/pty/${ptyId}`, { directory: formatPathForApi(directory) }) + const sdk = getSDKClient() + unwrap(await sdk.pty.remove({ ptyID: ptyId, directory: formatPathForApi(directory) })) + return true } /** * 获取 PTY 连接 WebSocket URL - * - * 动态从当前活动服务器获取地址,支持多后端连接 + * + * 浏览器 WebSocket 不支持自定义 header,认证方式: + * - 跨域:auth_token query parameter(与官方 opencode app 一致) + * - 同源:浏览器会复用页面的 Basic auth 凭据 + * - Tauri bridge:不走这里,通过 Rust 的 HTTP header 传认证 */ -export function getPtyConnectUrl(ptyId: string, directory?: string): string { - // 从 HTTP base URL 转换为 WebSocket URL +export function getPtyConnectUrl(ptyId: string, directory?: string, options?: PtyConnectUrlOptions): string { const httpBase = getApiBaseUrl() - // http:// -> ws://, https:// -> wss:// const wsBase = httpBase.replace(/^http/, 'ws') - + const includeAuthInUrl = options?.includeAuthInUrl ?? true + const cursor = + typeof options?.cursor === 'number' && Number.isSafeInteger(options.cursor) && options.cursor >= 0 + ? options.cursor + : undefined + + const auth = serverStore.getActiveAuth() const formatted = formatPathForApi(directory) - const queryString = buildQueryString({ directory: formatted }) - - return `${wsBase}/pty/${ptyId}/connect${queryString}` + + // Tauri bridge 不需要在 URL 里放认证 + if (!includeAuthInUrl) { + return `${wsBase}/pty/${ptyId}/connect${buildQueryString({ directory: formatted, cursor })}` + } + + // 浏览器原生 WebSocket: + // 跨域时用 auth_token query parameter + userinfo fallback + // 同源时浏览器会自动复用 Basic auth + const isCrossOrigin = (() => { + try { + return new URL(httpBase).origin !== location.origin + } catch { + return true + } + })() + + const queryParams: Record = { directory: formatted, cursor } + + let wsUrl = wsBase + if (auth?.password) { + if (isCrossOrigin) { + // auth_token = base64(username:password),与官方 opencode app 一致 + queryParams.auth_token = btoa(`${auth.username}:${auth.password}`) + } + // 同时设 userinfo 作为 fallback(部分浏览器直连时能用) + const creds = `${encodeURIComponent(auth.username)}:${encodeURIComponent(auth.password)}@` + wsUrl = wsBase.replace('://', `://${creds}`) + } + + return `${wsUrl}/pty/${ptyId}/connect${buildQueryString(queryParams)}` } diff --git a/src/api/ptyBridge.ts b/src/api/ptyBridge.ts new file mode 100644 index 00000000..d9843996 --- /dev/null +++ b/src/api/ptyBridge.ts @@ -0,0 +1,92 @@ +import { getAuthHeader } from './http' +import { getPtyConnectUrl } from './pty' + +/** Unified bridge event from Rust */ +interface BridgeEvent { + event: 'connected' | 'data' | 'disconnected' | 'error' + data?: { + data?: string + code?: number + reason?: string + message?: string + } +} + +interface ConnectTauriPtyParams { + ptyId: string + directory?: string + cursor?: number + onConnected: () => void + onMessage: (chunk: string) => void + onDisconnected: (info: { code?: number; reason?: string }) => void + onError: (message: string) => void +} + +export interface TauriPtyConnection { + send: (data: string) => void + close: () => void +} + +export async function connectTauriPty({ + ptyId, + directory, + cursor, + onConnected, + onMessage, + onDisconnected, + onError, +}: ConnectTauriPtyParams): Promise { + const { invoke, Channel } = await import('@tauri-apps/api/core') + const url = getPtyConnectUrl(ptyId, directory, { includeAuthInUrl: false, cursor }) + const authHeader = getAuthHeader()['Authorization'] || null + const onEvent = new Channel() + let closed = false + + onEvent.onmessage = msg => { + if (closed) return + + switch (msg.event) { + case 'connected': + onConnected() + break + case 'data': + if (msg.data?.data) { + onMessage(msg.data.data) + } + break + case 'disconnected': + closed = true + onDisconnected({ code: msg.data?.code, reason: msg.data?.reason }) + break + case 'error': + onError(msg.data?.message || 'Unknown bridge error') + break + } + } + + void invoke('bridge_connect', { + args: { bridgeId: ptyId, url, authHeader }, + onEvent, + }).catch((error: unknown) => { + if (closed) return + closed = true + const message = error instanceof Error ? error.message : String(error) + onDisconnected({ reason: message }) + }) + + return { + send(data: string) { + if (closed) return + void invoke('bridge_send', { args: { bridgeId: ptyId, data } }).catch((error: unknown) => { + if (closed) return + const message = error instanceof Error ? error.message : String(error) + onError(message) + }) + }, + close() { + if (closed) return + closed = true + void invoke('bridge_disconnect', { args: { bridgeId: ptyId } }).catch(() => {}) + }, + } +} diff --git a/src/api/sdk.test.ts b/src/api/sdk.test.ts new file mode 100644 index 00000000..a95bc143 --- /dev/null +++ b/src/api/sdk.test.ts @@ -0,0 +1,75 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { createOpencodeClientMock, getActiveBaseUrlMock, getActiveAuthMock, isTauriMock } = vi.hoisted(() => ({ + createOpencodeClientMock: vi.fn((config: unknown) => ({ config })), + getActiveBaseUrlMock: vi.fn(() => 'http://127.0.0.1:4096'), + getActiveAuthMock: vi.fn(() => null), + isTauriMock: vi.fn(() => false), +})) + +vi.mock('@opencode-ai/sdk/v2/client', () => ({ + createOpencodeClient: createOpencodeClientMock, +})) + +vi.mock('../store/serverStore', () => ({ + makeBasicAuthHeader: vi.fn(() => 'Basic token'), + serverStore: { + getActiveBaseUrl: getActiveBaseUrlMock, + getActiveAuth: getActiveAuthMock, + }, +})) + +vi.mock('../utils/tauri', () => ({ + isTauri: isTauriMock, +})) + +type MockClient = { + config: { + fetch: (input: RequestInfo | URL, init?: RequestInit) => Promise + } +} + +describe('sdk request lifecycle', () => { + beforeEach(async () => { + vi.restoreAllMocks() + getActiveBaseUrlMock.mockReturnValue('http://127.0.0.1:4096') + getActiveAuthMock.mockReturnValue(null) + isTauriMock.mockReturnValue(false) + const { abortInFlightApiRequests, invalidateSDKClient } = await import('./sdk') + abortInFlightApiRequests('reset test state') + invalidateSDKClient() + }) + + it('aborts in-flight SDK requests when the server endpoint changes', async () => { + const { abortInFlightApiRequests, getSDKClient } = await import('./sdk') + let signal: AbortSignal | undefined + + vi.spyOn(globalThis, 'fetch').mockImplementation((_input, init) => { + signal = init?.signal ?? undefined + return new Promise((_resolve, reject) => { + signal?.addEventListener('abort', () => reject(signal?.reason), { once: true }) + }) + }) + + const client = getSDKClient() as unknown as MockClient + const request = client.config.fetch('http://127.0.0.1:4096/project/current') + + abortInFlightApiRequests('Server endpoint changed') + + await expect(request).rejects.toMatchObject({ name: 'AbortError' }) + expect(signal?.aborted).toBe(true) + }) + + it('prevents stale SDK clients from starting new requests after endpoint changes', async () => { + const { abortInFlightApiRequests, getSDKClient } = await import('./sdk') + const client = getSDKClient() as unknown as MockClient + const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response('{}')) + + abortInFlightApiRequests('Server endpoint changed') + + await expect(client.config.fetch('http://127.0.0.1:4096/project/current')).rejects.toMatchObject({ + name: 'AbortError', + }) + expect(fetchSpy).not.toHaveBeenCalled() + }) +}) diff --git a/src/api/sdk.ts b/src/api/sdk.ts new file mode 100644 index 00000000..156b599c --- /dev/null +++ b/src/api/sdk.ts @@ -0,0 +1,153 @@ +// ============================================ +// SDK Client - 基于 @opencode-ai/sdk 的统一客户端 +// +// 职责: +// 1. 根据当前活动服务器动态创建 SDK client +// 2. 整合 baseUrl / auth / tauri fetch +// 3. 为上层 API 模块提供统一的 client 获取方式 +// ============================================ + +import { createOpencodeClient, type OpencodeClient } from '@opencode-ai/sdk/v2/client' +import { serverStore, makeBasicAuthHeader } from '../store/serverStore' +import { isTauri } from '../utils/tauri' + +// Tauri fetch 缓存 +let _tauriFetch: typeof globalThis.fetch | null = null +let _tauriFetchLoading: Promise | null = null +let _apiRequestGeneration = 0 +const _apiRequestControllers = new Set() + +async function getTauriFetch(): Promise { + if (_tauriFetch) return _tauriFetch + if (_tauriFetchLoading) return _tauriFetchLoading + _tauriFetchLoading = import('@tauri-apps/plugin-http').then(mod => { + _tauriFetch = mod.fetch as unknown as typeof globalThis.fetch + return _tauriFetch + }) + return _tauriFetchLoading +} + +function getFetchImpl(): typeof globalThis.fetch { + return isTauri() && _tauriFetch ? _tauriFetch : globalThis.fetch +} + +function createAbortError(message: string) { + return new DOMException(message, 'AbortError') +} + +async function trackedFetch(input: RequestInfo | URL, init: RequestInit | undefined, generation: number): Promise { + const controller = new AbortController() + const externalSignal = init?.signal + const abortFromExternal = () => controller.abort(externalSignal?.reason) + + if (externalSignal?.aborted) { + abortFromExternal() + } else { + externalSignal?.addEventListener('abort', abortFromExternal, { once: true }) + } + + _apiRequestControllers.add(controller) + + try { + if (generation !== _apiRequestGeneration) { + throw createAbortError('Stale API request') + } + + return await getFetchImpl()(input, { + ...init, + signal: controller.signal, + }) + } finally { + externalSignal?.removeEventListener('abort', abortFromExternal) + _apiRequestControllers.delete(controller) + } +} + +export function abortInFlightApiRequests(reason = 'Server endpoint changed'): void { + _apiRequestGeneration++ + for (const controller of _apiRequestControllers) { + controller.abort(createAbortError(reason)) + } + _apiRequestControllers.clear() +} + +// Client 缓存:按 "baseUrl + authHash" 缓存实例,避免重复创建 +let _cachedClient: OpencodeClient | null = null +let _cachedKey = '' + +function buildCacheKey(): string { + const baseUrl = serverStore.getActiveBaseUrl() + const auth = serverStore.getActiveAuth() + const authPart = auth?.password ? `${auth.username}:${auth.password}` : '' + return `${baseUrl}|${authPart}` +} + +function buildHeaders(): Record { + const headers: Record = {} + const auth = serverStore.getActiveAuth() + if (auth?.password) { + headers['Authorization'] = makeBasicAuthHeader(auth) + } + return headers +} + +/** + * 同步获取 SDK client(浏览器环境 or tauri fetch 已加载) + * 如果 tauri fetch 还没加载完,先用原生 fetch + */ +export function getSDKClient(): OpencodeClient { + const key = buildCacheKey() + if (_cachedClient && _cachedKey === key) { + return _cachedClient + } + + const baseUrl = serverStore.getActiveBaseUrl() + const headers = buildHeaders() + const generation = _apiRequestGeneration + + _cachedClient = createOpencodeClient({ + baseUrl, + headers, + fetch: (input, init) => trackedFetch(input, init, generation), + }) + _cachedKey = key + return _cachedClient +} + +/** + * 异步获取 SDK client(确保 tauri fetch 已加载) + * 在应用初始化时应该先调一次这个 + */ +export async function getSDKClientAsync(): Promise { + if (isTauri()) { + await getTauriFetch() + } + // 使 cache 失效以便用新的 tauri fetch 重建 + _cachedClient = null + _cachedKey = '' + return getSDKClient() +} + +/** + * 强制重建 client(服务器切换时调用) + */ +export function invalidateSDKClient(): void { + _cachedClient = null + _cachedKey = '' +} + +/** + * 从 SDK 返回值中提取 data,如果有 error 则抛出 + * + * SDK 默认返回 { data, error, request, response } + * 我们的上层 API 函数期望直接返回数据,所以需要 unwrap + */ +export function unwrap(result: { data?: T; error?: unknown }): T { + if (result.error != null) { + const err = result.error + if (err instanceof Error) throw err + if (typeof err === 'string') throw new Error(err) + throw new Error(JSON.stringify(err)) + } + return result.data as T +} diff --git a/src/api/session.ts b/src/api/session.ts index e4e71fcc..0b887ba0 100644 --- a/src/api/session.ts +++ b/src/api/session.ts @@ -1,89 +1,150 @@ // ============================================ // Session API Functions -// 基于 OpenAPI: /session 相关接口 +// 基于 @opencode-ai/sdk: /session 相关接口 // ============================================ -import { get, post, patch, del } from './http' +import { getSDKClient, unwrap } from './sdk' +import { normalizeTodoItems } from './todo' import { formatPathForApi } from '../utils/directoryUtils' -import type { ApiSession, SessionListParams, FileDiff } from './types' +import { getSessionMessages } from './message' +import { normalizeFileDiffs } from '../types/api/file' +import type { ApiSession, SessionListParams, FileDiff, ApiMessageWithParts, ApiUserMessage } from './types' +import type { SessionStatusMap } from '../types/api/session' +import type { TodoItem } from '../types/api/event' -// ... existing code ... +function normalizeSessionList(value: unknown): ApiSession[] { + if (Array.isArray(value)) return value as ApiSession[] + throw new Error('Invalid OpenCode session list response') +} + +// ============================================ +// Session Status & Diff +// ============================================ /** - * GET /session/{sessionID}/diff - 获取 session 的 diff + * 获取所有 session 的当前状态 */ -export async function getSessionDiff( - sessionId: string, - directory?: string, - messageId?: string -): Promise { - const params: Record = {} - const formattedDir = formatPathForApi(directory) - if (formattedDir) { - params.directory = formattedDir - } - if (messageId) { - params.messageID = messageId - } - return get(`/session/${sessionId}/diff`, params) +export async function getSessionStatus(directory?: string): Promise { + const sdk = getSDKClient() + return unwrap(await sdk.session.status({ directory: formatPathForApi(directory) })) +} + +/** + * 获取 session 的 diff + * 返回可在 UI 中渲染的 SnapshotFileDiff(过滤缺少 file 的异常项) + */ +export async function getSessionDiff(sessionId: string, directory?: string, messageId?: string): Promise { + const sdk = getSDKClient() + return normalizeFileDiffs( + unwrap( + await sdk.session.diff({ + sessionID: sessionId, + directory: formatPathForApi(directory), + messageID: messageId, + }), + ), + ) +} + +function isUserMessage(message: ApiMessageWithParts): message is ApiMessageWithParts & { info: ApiUserMessage } { + return message.info.role === 'user' +} + +/** + * 获取当前可见用户消息对应的本轮 diff + */ +export async function getLastTurnDiff(sessionId: string, directory?: string): Promise { + const [session, messages] = await Promise.all([ + getSession(sessionId, directory), + getSessionMessages(sessionId, undefined, directory), + ]) + + const userMessages = messages.filter(isUserMessage) + const revertMessageId = session.revert?.messageID + const visibleUserMessages = revertMessageId + ? userMessages.filter(message => message.info.id < revertMessageId) + : userMessages + + return normalizeFileDiffs(visibleUserMessages.at(-1)?.info.summary?.diffs) } -// ============================================ -// Session Actions // ============================================ // Session CRUD // ============================================ /** - * GET /session - 获取 session 列表 - * directory 会根据 pathMode 自动转换格式 + * 获取 session 列表 */ export async function getSessions(params: SessionListParams = {}): Promise { + const sdk = getSDKClient() const { directory, roots, start, search, limit } = params - return get('/session', { - directory: formatPathForApi(directory), - roots, - start, - search, - limit - }) + return normalizeSessionList( + unwrap( + await sdk.session.list({ + directory: formatPathForApi(directory), + roots, + start, + search, + limit, + }), + ), + ) } /** - * GET /session/{sessionID} - 获取单个 session + * 获取单个 session */ export async function getSession(sessionId: string, directory?: string): Promise { - return get(`/session/${sessionId}`, { directory: formatPathForApi(directory) }) + const sdk = getSDKClient() + return unwrap(await sdk.session.get({ sessionID: sessionId, directory: formatPathForApi(directory) })) } /** - * POST /session - 创建 session + * 创建 session */ -export async function createSession(params: { - directory?: string - title?: string - parentID?: string -} = {}): Promise { +export async function createSession( + params: { + directory?: string + title?: string + parentID?: string + } = {}, +): Promise { + const sdk = getSDKClient() const { directory, title, parentID } = params - return post('/session', { directory: formatPathForApi(directory) }, { title, parentID }) + return unwrap( + await sdk.session.create({ + directory: formatPathForApi(directory), + title, + parentID, + }), + ) } /** - * PATCH /session/{sessionID} - 更新 session + * 更新 session */ export async function updateSession( sessionId: string, params: { title?: string; time?: { archived?: number } }, - directory?: string + directory?: string, ): Promise { - return patch(`/session/${sessionId}`, { directory: formatPathForApi(directory) }, params) + const sdk = getSDKClient() + return unwrap( + await sdk.session.update({ + sessionID: sessionId, + directory: formatPathForApi(directory), + ...params, + }), + ) } /** - * DELETE /session/{sessionID} - 删除 session + * 删除 session */ export async function deleteSession(sessionId: string, directory?: string): Promise { - return del(`/session/${sessionId}`, { directory: formatPathForApi(directory) }) + const sdk = getSDKClient() + unwrap(await sdk.session.delete({ sessionID: sessionId, directory: formatPathForApi(directory) })) + return true } // ============================================ @@ -91,88 +152,110 @@ export async function deleteSession(sessionId: string, directory?: string): Prom // ============================================ /** - * POST /session/{sessionID}/abort - 中止 session + * 中止 session */ export async function abortSession(sessionId: string, directory?: string): Promise { - return post(`/session/${sessionId}/abort`, { directory: formatPathForApi(directory) }) + const sdk = getSDKClient() + unwrap(await sdk.session.abort({ sessionID: sessionId, directory: formatPathForApi(directory) })) + return true } /** - * POST /session/{sessionID}/revert - 回退消息 + * 回退消息 */ export async function revertMessage( sessionId: string, messageId: string, partId?: string, - directory?: string + directory?: string, ): Promise { - const body: { messageID: string; partID?: string } = { messageID: messageId } - if (partId) { - body.partID = partId - } - return post(`/session/${sessionId}/revert`, { directory: formatPathForApi(directory) }, body) + const sdk = getSDKClient() + return unwrap( + await sdk.session.revert({ + sessionID: sessionId, + directory: formatPathForApi(directory), + messageID: messageId, + partID: partId, + }), + ) } /** - * POST /session/{sessionID}/unrevert - 恢复已回退的消息 + * 恢复已回退的消息 */ export async function unrevertSession(sessionId: string, directory?: string): Promise { - return post(`/session/${sessionId}/unrevert`, { directory: formatPathForApi(directory) }) + const sdk = getSDKClient() + return unwrap(await sdk.session.unrevert({ sessionID: sessionId, directory: formatPathForApi(directory) })) } /** - * POST /session/{sessionID}/share - 分享 session + * 分享 session */ export async function shareSession(sessionId: string, directory?: string): Promise { - return post(`/session/${sessionId}/share`, { directory: formatPathForApi(directory) }) + const sdk = getSDKClient() + return unwrap(await sdk.session.share({ sessionID: sessionId, directory: formatPathForApi(directory) })) } /** - * DELETE /session/{sessionID}/share - 取消分享 session + * 取消分享 session */ export async function unshareSession(sessionId: string, directory?: string): Promise { - return del(`/session/${sessionId}/share`, { directory: formatPathForApi(directory) }) + const sdk = getSDKClient() + return unwrap(await sdk.session.unshare({ sessionID: sessionId, directory: formatPathForApi(directory) })) } /** - * POST /session/{sessionID}/fork - Fork session + * Fork session */ -export async function forkSession( - sessionId: string, - messageId?: string, - directory?: string -): Promise { - return post(`/session/${sessionId}/fork`, { directory: formatPathForApi(directory) }, { messageID: messageId }) +export async function forkSession(sessionId: string, messageId?: string, directory?: string): Promise { + const sdk = getSDKClient() + return unwrap( + await sdk.session.fork({ + sessionID: sessionId, + directory: formatPathForApi(directory), + messageID: messageId, + }), + ) } /** - * POST /session/{sessionID}/summarize - 总结 session + * 总结 session */ export async function summarizeSession( sessionId: string, params: { providerID: string; modelID: string; auto?: boolean }, - directory?: string + directory?: string, ): Promise { - return post(`/session/${sessionId}/summarize`, { directory: formatPathForApi(directory) }, params) + const sdk = getSDKClient() + unwrap( + await sdk.session.summarize({ + sessionID: sessionId, + directory: formatPathForApi(directory), + ...params, + }), + ) + return true } /** - * GET /session/{sessionID}/children - 获取子 session + * 获取子 session */ export async function getSessionChildren(sessionId: string, directory?: string): Promise { - return get(`/session/${sessionId}/children`, { directory: formatPathForApi(directory) }) + const sdk = getSDKClient() + return unwrap(await sdk.session.children({ sessionID: sessionId, directory: formatPathForApi(directory) })) } /** - * GET /session/{sessionID}/todo - 获取 session 的 todo 列表 + * Session Todo */ -export interface ApiTodo { - id: string - content: string - status: 'pending' | 'in_progress' | 'completed' | 'cancelled' - priority: 'high' | 'medium' | 'low' -} +export type ApiTodo = TodoItem +/** + * 获取 session 的 todo 列表 + * SDK 的 Todo 没有 id 字段,用 index+content+status 合成 + */ export async function getSessionTodos(sessionId: string, directory?: string): Promise { - return get(`/session/${sessionId}/todo`, { directory: formatPathForApi(directory) }) + const sdk = getSDKClient() + const todos = unwrap(await sdk.session.todo({ sessionID: sessionId, directory: formatPathForApi(directory) })) + return normalizeTodoItems(todos) } diff --git a/src/api/skill.ts b/src/api/skill.ts index a44de0c0..512d28f3 100644 --- a/src/api/skill.ts +++ b/src/api/skill.ts @@ -2,7 +2,7 @@ // Skill API // ============================================ -import { get } from './http' +import { getSDKClient, unwrap } from './sdk' import { formatPathForApi } from '../utils/directoryUtils' import type { SkillList } from '../types/api/skill' @@ -10,5 +10,6 @@ import type { SkillList } from '../types/api/skill' * 获取所有可用 Skills */ export async function getSkills(directory?: string): Promise { - return get('/skill', { directory: formatPathForApi(directory) }) + const sdk = getSDKClient() + return unwrap(await sdk.app.skills({ directory: formatPathForApi(directory) })) } diff --git a/src/api/sse.test.ts b/src/api/sse.test.ts new file mode 100644 index 00000000..67c30ec9 --- /dev/null +++ b/src/api/sse.test.ts @@ -0,0 +1,19 @@ +import { describe, expect, it } from 'vitest' +import { createSseTextParser } from './sse' + +describe('createSseTextParser', () => { + it('keeps multiline event data across chunk boundaries', () => { + const parser = createSseTextParser() + + expect(parser.push('data: {"text":"中')).toEqual([]) + expect(parser.push('文"}\n')).toEqual([]) + expect(parser.push('data: 第二行\n\n')).toEqual(['{"text":"中文"}\n第二行']) + }) + + it('normalizes CRLF-delimited SSE blocks', () => { + const parser = createSseTextParser() + + expect(parser.push('data: 你好\r\n')).toEqual([]) + expect(parser.push('data: 世界\r\n\r\n')).toEqual(['你好\n世界']) + }) +}) diff --git a/src/api/sse.ts b/src/api/sse.ts new file mode 100644 index 00000000..9a7ff0f6 --- /dev/null +++ b/src/api/sse.ts @@ -0,0 +1,36 @@ +export interface SseTextParser { + push(chunk: string): string[] +} + +export function createSseTextParser(): SseTextParser { + let buffer = '' + + return { + push(chunk: string) { + if (!chunk) return [] + + buffer += chunk + buffer = buffer.replace(/\r\n/g, '\n').replace(/\r/g, '\n') + + const blocks = buffer.split('\n\n') + buffer = blocks.pop() ?? '' + + const events: string[] = [] + + for (const block of blocks) { + const dataLines: string[] = [] + + for (const line of block.split('\n')) { + if (!line.startsWith('data:')) continue + dataLines.push(line[5] === ' ' ? line.slice(6) : line.slice(5)) + } + + if (dataLines.length > 0) { + events.push(dataLines.join('\n')) + } + } + + return events + }, + } +} diff --git a/src/api/todo.ts b/src/api/todo.ts new file mode 100644 index 00000000..8978467a --- /dev/null +++ b/src/api/todo.ts @@ -0,0 +1,34 @@ +import type { Todo as SDKTodo } from '@opencode-ai/sdk/v2/client' +import type { TodoItem } from '../types/api/event' + +function buildTodoId(todo: SDKTodo, index: number): string { + const content = String(todo.content ?? '').slice(0, 32) + const status = String(todo.status ?? '') + const priority = String(todo.priority ?? '') + return `todo-${index}-${content}-${status}-${priority}` +} + +function isTodoStatus(status: string): status is TodoItem['status'] { + return status === 'pending' || status === 'in_progress' || status === 'completed' || status === 'cancelled' +} + +function isTodoPriority(priority: string): priority is TodoItem['priority'] { + return priority === 'high' || priority === 'medium' || priority === 'low' +} + +function normalizeTodoStatus(status: string): TodoItem['status'] { + return isTodoStatus(status) ? status : 'pending' +} + +function normalizeTodoPriority(priority: string): TodoItem['priority'] { + return isTodoPriority(priority) ? priority : 'medium' +} + +export function normalizeTodoItems(todos: SDKTodo[] | null | undefined): TodoItem[] { + return (todos ?? []).map((todo, index) => ({ + id: buildTodoId(todo, index), + content: todo.content, + status: normalizeTodoStatus(todo.status), + priority: normalizeTodoPriority(todo.priority), + })) +} diff --git a/src/api/tool.ts b/src/api/tool.ts index 660eee11..ed362e82 100644 --- a/src/api/tool.ts +++ b/src/api/tool.ts @@ -2,7 +2,7 @@ // Tool API - 工具管理 // ============================================ -import { get } from './http' +import { getSDKClient, unwrap } from './sdk' import { formatPathForApi } from '../utils/directoryUtils' import type { ToolIDs, ToolList } from '../types/api/tool' @@ -10,12 +10,14 @@ import type { ToolIDs, ToolList } from '../types/api/tool' * 获取工具 ID 列表 */ export async function getToolIds(directory?: string): Promise { - return get('/experimental/tool/ids', { directory: formatPathForApi(directory) }) + const sdk = getSDKClient() + return unwrap(await sdk.tool.ids({ directory: formatPathForApi(directory) })) } /** * 获取工具列表(带详细信息) */ -export async function getTools(directory?: string): Promise { - return get('/experimental/tool', { directory: formatPathForApi(directory) }) +export async function getTools(provider: string, model: string, directory?: string): Promise { + const sdk = getSDKClient() + return unwrap(await sdk.tool.list({ provider, model, directory: formatPathForApi(directory) })) } diff --git a/src/api/types.ts b/src/api/types.ts index 81a50293..efa6d6aa 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -1,48 +1,16 @@ -// ============================================ // API Types - 向后兼容层 -// ============================================ -// -// 此文件从 types/api/ 重新导出所有类型,保持向后兼容 -// 新代码应该直接从 @/types 或 @/types/api 导入 -// -// ============================================ -// Re-export from types/api -// ============================================ +export type * from '../types/api' -// Common types -export type { - TimeInfo, - TokenUsage, - ModelRef, - PathInfo, - ErrorInfo, - TextRange, - BadRequestError, - NotFoundError, -} from '../types/api/common' - -// Model types - with aliases for backward compatibility -export type { - Model as ApiModel, - Provider as ApiProvider, - ProvidersResponse, -} from '../types/api/model' - -// Project types - with aliases -export type { - Project as ApiProject, - PathResponse as ApiPath, -} from '../types/api/project' +export type { ModelInfo, FileCapabilities, Attachment, AttachmentType } from '../types/ui' -// Session types - with aliases +export type { Model as ApiModel, Provider as ApiProvider, ProvidersResponse } from '../types/api/model' +export type { Project as ApiProject, PathResponse as ApiPath } from '../types/api/project' export type { Session as ApiSession, SessionListParams, SessionRevert as SessionRevertState, } from '../types/api/session' - -// Message types - with aliases export type { Message as ApiMessage, UserMessage as ApiUserMessage, @@ -62,8 +30,6 @@ export type { CompactionPart as ApiCompactionPart, SubtaskPart as ApiSubtaskPart, } from '../types/api/message' - -// Permission types - with aliases export type { PermissionRequest as ApiPermissionRequest, PermissionReply, @@ -72,48 +38,8 @@ export type { QuestionRequest as ApiQuestionRequest, QuestionAnswer, } from '../types/api/permission' - -// File types -export type { - FileNode, - FileContent, - FileStatusItem, - FileDiff, - FilePatch, - PatchHunk, - Symbol as SymbolInfo, -} from '../types/api/file' - -// Agent types - with aliases -export type { - Agent as ApiAgent, - AgentPermission as ApiAgentPermission, -} from '../types/api/agent' - -// Event types -export type { - GlobalEvent, - EventCallbacks, - WorktreeReadyPayload, - WorktreeFailedPayload, - VcsBranchUpdatedPayload, - TodoUpdatedPayload, - TodoItem, -} from '../types/api/event' - -// ============================================ -// UI Types (from types/ui.ts) -// ============================================ - -export type { - ModelInfo, - Attachment, - AttachmentType, -} from '../types/ui' - -// ============================================ -// Send Message Types (kept here for now) -// ============================================ +export type { Agent as ApiAgent, AgentPermission as ApiAgentPermission } from '../types/api/agent' +export type { Symbol as SymbolInfo } from '../types/api/file' import type { Attachment } from '../types/ui' @@ -132,7 +58,6 @@ export interface SendMessageParams { } agent?: string variant?: string - /** 工作目录(项目目录) */ directory?: string } diff --git a/src/api/vcs.ts b/src/api/vcs.ts index 5b452f6f..3e0dffd1 100644 --- a/src/api/vcs.ts +++ b/src/api/vcs.ts @@ -2,18 +2,29 @@ // VCS API - 版本控制信息 // ============================================ -import { get } from './http' -import type { VcsInfo } from '../types/api/vcs' +import { getSDKClient, unwrap } from './sdk' +import type { FileDiff } from './types' +import type { VcsDiffMode, VcsInfo } from '../types/api/vcs' import { formatPathForApi } from '../utils/directoryUtils' +import { normalizeFileDiffs } from '../types/api/file' /** * 获取 VCS 信息 */ export async function getVcsInfo(directory?: string): Promise { try { - return await get('/vcs', { directory: formatPathForApi(directory) }) + const sdk = getSDKClient() + return unwrap(await sdk.vcs.get({ directory: formatPathForApi(directory) })) } catch { // VCS 不可用时返回 null return null } } + +/** + * 获取 Git 或分支维度的 diff + */ +export async function getVcsDiff(mode: VcsDiffMode, directory?: string): Promise { + const sdk = getSDKClient() + return normalizeFileDiffs(unwrap(await sdk.vcs.diff({ mode, directory: formatPathForApi(directory) }))) +} diff --git a/src/api/worktree.ts b/src/api/worktree.ts index 21a100a1..6246d596 100644 --- a/src/api/worktree.ts +++ b/src/api/worktree.ts @@ -1,49 +1,41 @@ // ============================================ // Worktree API - Git Worktree 管理 -// 基于 OpenAPI 规范 v0.0.3 // ============================================ -import { get, post, del } from './http' +import { getSDKClient, unwrap } from './sdk' import type { Worktree, WorktreeCreateInput, WorktreeRemoveInput, WorktreeResetInput } from '../types/api/worktree' import { formatPathForApi } from '../utils/directoryUtils' /** * 获取所有 worktree 列表 - * GET /experimental/worktree -> string[] */ export async function listWorktrees(directory?: string): Promise { - return get('/experimental/worktree', { directory: formatPathForApi(directory) }) + const sdk = getSDKClient() + return unwrap(await sdk.worktree.list({ directory: formatPathForApi(directory) })) } /** * 创建新的 worktree - * POST /experimental/worktree -> Worktree */ -export async function createWorktree( - params: WorktreeCreateInput, - directory?: string -): Promise { - return post('/experimental/worktree', { directory: formatPathForApi(directory) }, params) +export async function createWorktree(params: WorktreeCreateInput, directory?: string): Promise { + const sdk = getSDKClient() + return unwrap(await sdk.worktree.create({ directory: formatPathForApi(directory), worktreeCreateInput: params })) } /** * 删除 worktree - * DELETE /experimental/worktree -> boolean */ -export async function removeWorktree( - params: WorktreeRemoveInput, - directory?: string -): Promise { - return del('/experimental/worktree', { directory: formatPathForApi(directory) }, params) +export async function removeWorktree(params: WorktreeRemoveInput, directory?: string): Promise { + const sdk = getSDKClient() + unwrap(await sdk.worktree.remove({ directory: formatPathForApi(directory), worktreeRemoveInput: params })) + return true } /** * 重置 worktree - * POST /experimental/worktree/reset -> boolean */ -export async function resetWorktree( - params: WorktreeResetInput, - directory?: string -): Promise { - return post('/experimental/worktree/reset', { directory: formatPathForApi(directory) }, params) +export async function resetWorktree(params: WorktreeResetInput, directory?: string): Promise { + const sdk = getSDKClient() + unwrap(await sdk.worktree.reset({ directory: formatPathForApi(directory), worktreeResetInput: params })) + return true } diff --git a/src/assets/icons/download.svg b/src/assets/icons/download.svg new file mode 100644 index 00000000..eec11a1d --- /dev/null +++ b/src/assets/icons/download.svg @@ -0,0 +1 @@ + diff --git a/src/components/BottomPanel.tsx b/src/components/BottomPanel.tsx index b4d993ed..4b29e026 100644 --- a/src/components/BottomPanel.tsx +++ b/src/components/BottomPanel.tsx @@ -1,28 +1,51 @@ -import { memo, useCallback, useState, useEffect } from 'react' -import { Terminal } from './Terminal' +import { lazy, memo, Suspense, useCallback, useState, useEffect, useRef } from 'react' +import { useTranslation } from 'react-i18next' import { TerminalIcon } from './Icons' import { PanelContainer } from './PanelContainer' import { layoutStore, useLayoutStore, type TerminalTab, type PanelTab } from '../store/layoutStore' +import { serverStore } from '../store/serverStore' import { createPtySession, removePtySession, listPtySessions } from '../api/pty' -import { SessionChangesPanel } from './SessionChangesPanel' -import { FileExplorer } from './FileExplorer' -import { McpPanel } from './McpPanel' -import { SkillPanel } from './SkillPanel' -import { WorktreePanel } from './WorktreePanel' import { useMessageStore } from '../store' import { ResizablePanel } from './ui/ResizablePanel' +import { logger } from '../utils/logger' +import { normalizeToForwardSlash, uiErrorHandler } from '../utils' +import { useChatViewport } from '../features/chat/chatViewport' + +const Terminal = lazy(() => import('./Terminal').then(module => ({ default: module.Terminal }))) +const SessionChangesPanel = lazy(() => + import('./SessionChangesPanel').then(module => ({ default: module.SessionChangesPanel })), +) +const FileExplorer = lazy(() => import('./FileExplorer').then(module => ({ default: module.FileExplorer }))) +const McpPanel = lazy(() => import('./McpPanel').then(module => ({ default: module.McpPanel }))) +const SkillPanel = lazy(() => import('./SkillPanel').then(module => ({ default: module.SkillPanel }))) +const WorktreePanel = lazy(() => import('./WorktreePanel').then(module => ({ default: module.WorktreePanel }))) interface BottomPanelProps { directory?: string } +function PanelFallback() { + const { t } = useTranslation(['components', 'common']) + return ( +
+ {t('bottomPanel.loadingPanel')} +
+ ) +} + export const BottomPanel = memo(function BottomPanel({ directory }: BottomPanelProps) { - const { bottomPanelOpen, bottomPanelHeight, previewFile } = useLayoutStore() + const { t } = useTranslation(['components', 'common']) + const { bottomPanelOpen, bottomPanelHeight } = useLayoutStore() const { sessionId } = useMessageStore() - + const { interaction, layout } = useChatViewport() + const [isRestoring, setIsRestoring] = useState(false) - const [restored, setRestored] = useState(false) - + const normalizedDirectory = directory ? normalizeToForwardSlash(directory) : undefined + + useEffect(() => { + layoutStore.setCurrentTerminalDirectory(normalizedDirectory) + }, [normalizedDirectory]) + // 追踪面板 resize 状态 const [isPanelResizing, setIsPanelResizing] = useState(false) useEffect(() => { @@ -36,45 +59,54 @@ export const BottomPanel = memo(function BottomPanel({ directory }: BottomPanelP } }, []) - // 页面加载时恢复已有的 PTY sessions + // 目录变化时(包括全局模式),重新拉取该目录的 PTY 会话 + const prevDirectoryRef = useRef(undefined) + const hasRestoredDirectoryRef = useRef(false) + const restoreRequestIdRef = useRef(0) useEffect(() => { - if (restored || !directory) return - setRestored(true) + // 目录没变就不重复拉取 + if (hasRestoredDirectoryRef.current && prevDirectoryRef.current === normalizedDirectory) return + hasRestoredDirectoryRef.current = true + prevDirectoryRef.current = normalizedDirectory - const restoreSessions = async () => { + const restoreSessions = async (requestId: number) => { try { setIsRestoring(true) - const sessions = await listPtySessions(directory) - console.log('[BottomPanel] Found existing PTY sessions:', sessions) - - if (sessions.length > 0) { - for (const pty of sessions) { - if (!layoutStore.getTerminalTabs().some(t => t.id === pty.id)) { - const tab: TerminalTab = { - id: pty.id, - title: pty.title || 'Terminal', - status: pty.running ? 'connecting' : 'exited', - } - layoutStore.addTerminalTab(tab, false) - } - } - } + + // 拉取新目录下的 PTY 会话 + const sessions = await listPtySessions(normalizedDirectory) + if (restoreRequestIdRef.current !== requestId) return + logger.log('[BottomPanel] PTY sessions for', normalizedDirectory, ':', sessions) + + layoutStore.syncTerminalSessions( + normalizedDirectory, + sessions.map(pty => ({ + id: pty.id, + title: pty.title || 'Terminal', + status: pty.status === 'running' ? 'connecting' : 'exited', + })), + ) } catch (error) { - console.error('[BottomPanel] Failed to restore sessions:', error) + uiErrorHandler('restore terminal sessions', error) } finally { - setIsRestoring(false) + if (restoreRequestIdRef.current === requestId) { + setIsRestoring(false) + } } } - restoreSessions() - }, [directory, restored]) + void restoreSessions(++restoreRequestIdRef.current) + return serverStore.onServerChange(() => { + void restoreSessions(++restoreRequestIdRef.current) + }) + }, [normalizedDirectory]) // 创建新终端 const handleNewTerminal = useCallback(async () => { try { - console.log('[BottomPanel] Creating PTY session, directory:', directory) - const pty = await createPtySession({ cwd: directory }, directory) - console.log('[BottomPanel] PTY created:', pty) + logger.log('[BottomPanel] Creating PTY session, directory:', normalizedDirectory) + const pty = await createPtySession({ cwd: normalizedDirectory }, normalizedDirectory) + logger.log('[BottomPanel] PTY created:', pty) const tab: TerminalTab = { id: pty.id, title: pty.title || 'Terminal', @@ -82,92 +114,123 @@ export const BottomPanel = memo(function BottomPanel({ directory }: BottomPanelP } layoutStore.addTerminalTab(tab) } catch (error) { - console.error('[BottomPanel] Failed to create terminal:', error) + uiErrorHandler('create terminal', error) } - }, [directory]) + }, [normalizedDirectory]) // 关闭终端 - const handleCloseTerminal = useCallback(async (ptyId: string) => { - try { - await removePtySession(ptyId, directory) - } catch { - // ignore - may already be closed - } - }, [directory]) + const handleCloseTerminal = useCallback( + async (ptyId: string) => { + try { + await removePtySession(ptyId, normalizedDirectory) + } catch { + // ignore - may already be closed + } + }, + [normalizedDirectory], + ) // 渲染内容 - const renderContent = useCallback((activeTab: PanelTab | null) => { - if (isRestoring) { - return ( -
- - Restoring sessions... -
- ) - } - - if (!activeTab) { - return ( -
- - No content - -
- ) - } - - switch (activeTab.type) { - case 'terminal': + const renderContent = useCallback( + (activeTab: PanelTab | null) => { + if (isRestoring) { return ( - +
+ + {t('terminal.restoringSessions')} +
) - case 'files': + } + + if (!activeTab) { return ( - +
+ + {t('common:noContent')} + +
) - case 'changes': - if (!sessionId) { - return ( -
- No active session + } + + return ( + <> + {/* Keep files mounted so expanded folders and previews survive tab switches. */} +
+ }> + + +
+ + {sessionId ? ( +
+ }> + +
- ) - } - return - case 'mcp': - return - case 'skill': - return - case 'worktree': - return - default: - return null - } - }, [isRestoring, handleNewTerminal, directory, previewFile, sessionId, isPanelResizing]) + ) : activeTab.type === 'changes' ? ( +
+ {t('rightPanel.noActiveSession')} +
+ ) : null} + + {activeTab.type === 'terminal' ? ( + }> + + + ) : null} + + {activeTab.type === 'mcp' ? ( + }> + + + ) : null} + + {activeTab.type === 'skill' ? ( + }> + + + ) : null} + + {activeTab.type === 'worktree' ? ( + }> + + + ) : null} + + ) + }, + [isRestoring, handleNewTerminal, directory, sessionId, isPanelResizing, t], + ) return ( layoutStore.setBottomPanelHeight(h)} + onClose={() => layoutStore.closeBottomPanel()} > @@ -186,26 +249,78 @@ interface TerminalContentProps { directory?: string } -const TerminalContent = memo(function TerminalContent({ +const TerminalContent = memo(function TerminalContent({ activeTab, directory }: TerminalContentProps) { + const { panelTabs } = useLayoutStore() + + // 获取所有 bottom 位置的 terminal tabs + const terminalTabs = panelTabs.filter(t => t.position === 'bottom' && t.type === 'terminal') + + return ( + <> + {terminalTabs.map(tab => ( + + ))} + + ) +}) + +interface FilesContentProps { + activeTab: PanelTab + directory?: string + isPanelResizing?: boolean + sessionId?: string | null +} + +const FilesContent = memo(function FilesContent({ activeTab, directory, -}: TerminalContentProps) { + isPanelResizing = false, + sessionId, +}: FilesContentProps) { const { panelTabs } = useLayoutStore() - - // 获取所有 bottom 位置的 terminal tabs - const terminalTabs = panelTabs.filter( - t => t.position === 'bottom' && t.type === 'terminal' + const fileTabs = panelTabs.filter(t => t.position === 'bottom' && t.type === 'files') + + return ( + <> + {fileTabs.map(tab => ( +
+ +
+ ))} + ) +}) + +interface ChangesContentProps { + activeTab: PanelTab + directory?: string + sessionId: string + isPanelResizing?: boolean +} + +const ChangesContent = memo(function ChangesContent({ + activeTab, + directory, + sessionId, + isPanelResizing = false, +}: ChangesContentProps) { + const { panelTabs } = useLayoutStore() + const changeTabs = panelTabs.filter(t => t.position === 'bottom' && t.type === 'changes') return ( <> - {terminalTabs.map((tab) => ( - + {changeTabs.map(tab => ( +
+ +
))} ) diff --git a/src/components/CircularProgress.tsx b/src/components/CircularProgress.tsx new file mode 100644 index 00000000..3ac04ec0 --- /dev/null +++ b/src/components/CircularProgress.tsx @@ -0,0 +1,63 @@ +// ============================================ +// CircularProgress - 环形进度指示器 +// ============================================ + +interface CircularProgressProps { + /** 进度值 0-1 */ + progress: number + /** 整体尺寸 (px) */ + size: number + /** 描边宽度,默认 3 */ + strokeWidth?: number + /** 轨道 className(通过 text-xxx 控制颜色) */ + trackClassName?: string + /** 进度弧 className */ + progressClassName?: string + /** SVG 外层 className */ + className?: string +} + +export function CircularProgress({ + progress, + size, + strokeWidth = 3, + trackClassName = 'text-text-500/30', + progressClassName = 'text-accent-main-100', + className = '', +}: CircularProgressProps) { + const r = (size - strokeWidth) / 2 + const c = 2 * Math.PI * r + const offset = c * (1 - Math.min(Math.max(progress, 0), 1)) + + return ( + + ) +} diff --git a/src/components/CloseServiceDialog.tsx b/src/components/CloseServiceDialog.tsx new file mode 100644 index 00000000..44690d9c --- /dev/null +++ b/src/components/CloseServiceDialog.tsx @@ -0,0 +1,67 @@ +// ============================================ +// Close Service Dialog +// 关闭应用时询问是否同时关闭 opencode 服务 +// ============================================ + +import { useState } from 'react' +import { useTranslation } from 'react-i18next' +import { Dialog } from './ui/Dialog' +import { Button } from './ui/Button' +import { PlugIcon, SpinnerIcon } from './Icons' + +interface CloseServiceDialogProps { + isOpen: boolean + onConfirm: (stopService: boolean) => void + onCancel: () => void +} + +export function CloseServiceDialog({ isOpen, onConfirm, onCancel }: CloseServiceDialogProps) { + const { t } = useTranslation(['components', 'common']) + const [closing, setClosing] = useState(false) + + const handleConfirm = (stopService: boolean) => { + setClosing(true) + onConfirm(stopService) + } + + return ( + +
+ {/* Icon */} +
+ +
+ + {/* Title */} +

+ {t('closeService.title')} +

+ + {/* Description */} +

+ {t('closeService.description')} +

+ + {/* Actions */} + {closing ? ( +
+ + {t('common:closing')} +
+ ) : ( +
+ + + +
+ )} +
+
+ ) +} diff --git a/src/components/CodeBlock.test.tsx b/src/components/CodeBlock.test.tsx new file mode 100644 index 00000000..08096404 --- /dev/null +++ b/src/components/CodeBlock.test.tsx @@ -0,0 +1,188 @@ +import { render, screen } from '@testing-library/react' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { CodeBlock } from './CodeBlock' + +const useInputCapabilitiesMock = vi.fn(() => ({ + canHover: true, + hasCoarsePointer: false, + hasTouch: false, + preferTouchUi: false, +})) +type HighlightMockOutput = { highlightedCode?: string; output: { content: string; color?: string }[][] | null } +const useSyntaxHighlightMock = vi.fn( + (_code: string, _options: unknown): HighlightMockOutput => ({ + output: [[{ content: 'highlighted', color: '#fff' }]], + }), +) +const useStreamingSyntaxHighlightMock = vi.fn( + (_code: string, _options: unknown): HighlightMockOutput => ({ + output: null, + }), +) +const useInViewMock = vi.fn(() => ({ ref: vi.fn(), inView: false })) +const themeSnapshot = { codeWordWrap: false } + +vi.mock('../hooks/useInputCapabilities', () => ({ + useInputCapabilities: () => useInputCapabilitiesMock(), +})) + +vi.mock('../hooks/useSyntaxHighlight', () => ({ + useSyntaxHighlight: (code: string, options: unknown) => useSyntaxHighlightMock(code, options), + useStreamingSyntaxHighlight: (code: string, options: unknown) => useStreamingSyntaxHighlightMock(code, options), +})) + +vi.mock('../hooks/useInView', () => ({ + useInView: () => useInViewMock(), +})) + +vi.mock('../store/themeStore', () => ({ + themeStore: { + subscribe: () => () => {}, + getSnapshot: () => themeSnapshot, + }, +})) + +vi.mock('./ui', () => ({ + CopyButton: ({ className }: { className?: string }) => ( + + ), +})) + +describe('CodeBlock', () => { + beforeEach(() => { + useInputCapabilitiesMock.mockReset() + useInputCapabilitiesMock.mockReturnValue({ + canHover: true, + hasCoarsePointer: false, + hasTouch: false, + preferTouchUi: false, + }) + useSyntaxHighlightMock.mockClear() + useSyntaxHighlightMock.mockReturnValue({ output: [[{ content: 'highlighted', color: '#fff' }]] }) + useStreamingSyntaxHighlightMock.mockClear() + useStreamingSyntaxHighlightMock.mockReturnValue({ output: null }) + useInViewMock.mockReset() + useInViewMock.mockReturnValue({ ref: vi.fn(), inView: false }) + }) + + it('requires tap-to-reveal copy button for unlabeled touch-ui code blocks', () => { + useInputCapabilitiesMock.mockReturnValue({ + canHover: false, + hasCoarsePointer: true, + hasTouch: true, + preferTouchUi: true, + }) + + const { container } = render() + + expect(container.firstChild).toHaveAttribute('tabindex', '0') + expect(screen.getByRole('button', { name: 'Copy to clipboard' }).parentElement?.className).toContain( + '[@media(hover:none)]:opacity-0', + ) + }) + + it('keeps labeled touch-ui code blocks unchanged', () => { + useInputCapabilitiesMock.mockReturnValue({ + canHover: false, + hasCoarsePointer: true, + hasTouch: true, + preferTouchUi: true, + }) + + const { container } = render() + + expect(container.firstChild).not.toHaveAttribute('tabindex') + expect(screen.getByText('ts')).toBeInTheDocument() + }) + + it('renders current plain code while highlight is deferred', () => { + render() + + expect(screen.getByText('const value = 1')).toBeInTheDocument() + expect(screen.queryByText('highlighted')).not.toBeInTheDocument() + expect(useSyntaxHighlightMock).toHaveBeenCalledWith( + 'const value = 1', + expect.objectContaining({ enabled: false, lang: 'ts', mode: 'tokens' }), + ) + }) + + it('renders highlighted tokens as selectable plain pre content', () => { + useInViewMock.mockReturnValue({ ref: vi.fn(), inView: true }) + + const { container } = render() + + const pre = container.querySelector('pre') + expect(pre).toHaveTextContent('highlighted') + expect(pre).not.toHaveAttribute('tabindex') + expect(pre?.className).toContain('select-text') + expect(useSyntaxHighlightMock).toHaveBeenCalledWith( + 'const value = 1', + expect.objectContaining({ enabled: true, lang: 'ts', mode: 'tokens' }), + ) + }) + + it('keeps highlighted prefix while streaming suffix waits for new tokens', () => { + useInViewMock.mockReturnValue({ ref: vi.fn(), inView: true }) + useSyntaxHighlightMock.mockImplementation((highlightCode: string): HighlightMockOutput => { + if (highlightCode === 'const') return { output: [[{ content: 'const', color: '#fff' }]] } + return { output: null } + }) + + const { container, rerender } = render() + + rerender() + + const pre = container.querySelector('pre') + expect(pre).toHaveTextContent('const value') + expect(pre?.className).toContain('shiki-wrapper') + }) + + it('enables highlighting when visible', () => { + useInViewMock.mockReturnValue({ ref: vi.fn(), inView: true }) + + render() + + expect(useSyntaxHighlightMock).toHaveBeenCalledWith( + 'const value = 1', + expect.objectContaining({ delayMs: 0, enabled: true, lang: 'ts', mode: 'tokens' }), + ) + }) + + it('can force streaming highlight before in-view observation fires', () => { + render() + + expect(useSyntaxHighlightMock).toHaveBeenCalledWith( + 'const value = 1', + expect.objectContaining({ delayMs: 0, enabled: true, lang: 'ts', mode: 'tokens' }), + ) + }) + + it('uses incremental streaming highlighting instead of full tokenization', () => { + useStreamingSyntaxHighlightMock.mockReturnValue({ output: [[{ content: 'streamed', color: '#fff' }]] }) + + render() + + expect(screen.getByText('streamed')).toBeInTheDocument() + expect(useSyntaxHighlightMock).toHaveBeenCalledWith( + 'const value = 1', + expect.objectContaining({ enabled: false, lang: 'ts', mode: 'tokens' }), + ) + expect(useStreamingSyntaxHighlightMock).toHaveBeenCalledWith( + 'const value = 1', + expect.objectContaining({ enabled: true, lang: 'ts' }), + ) + }) + + it('keeps the live suffix when streaming tokens lag behind code', () => { + useStreamingSyntaxHighlightMock.mockReturnValue({ + highlightedCode: 'const', + output: [[{ content: 'const', color: '#fff' }]], + }) + + const { container } = render() + + expect(container.querySelector('pre')).toHaveTextContent('const value') + }) +}) diff --git a/src/components/CodeBlock.tsx b/src/components/CodeBlock.tsx index 70a3fede..069ba0f2 100644 --- a/src/components/CodeBlock.tsx +++ b/src/components/CodeBlock.tsx @@ -1,98 +1,230 @@ -import { memo, useMemo } from 'react' -import { useSyntaxHighlight } from '../hooks/useSyntaxHighlight' +import { memo, useCallback, useDeferredValue, useMemo, useState, useSyncExternalStore } from 'react' +import { useInputCapabilities } from '../hooks/useInputCapabilities' +import { useStreamingSyntaxHighlight, useSyntaxHighlight, type HighlightTokens } from '../hooks/useSyntaxHighlight' +import { themeStore } from '../store/themeStore' import { CopyButton } from './ui' import { useInView } from '../hooks/useInView' +/** Languages that carry no useful information — hide the label */ +const HIDDEN_LANGS = new Set(['text', 'plain', 'txt', 'plaintext']) + +const TokenSpan = memo( + function TokenSpan({ token }: { token: HighlightTokens[number][number] }) { + return {token.content} + }, + (prev, next) => prev.token === next.token, +) + +const TokenLine = memo( + function TokenLine({ line, trailingNewline }: { line: HighlightTokens[number]; trailingNewline: boolean }) { + return ( + + {line.map((token, tokenIndex) => ( + + ))} + {trailingNewline ? '\n' : null} + + ) + }, + (prev, next) => prev.line === next.line && prev.trailingNewline === next.trailingNewline, +) + +function renderHighlightedTokens(tokens: HighlightTokens) { + return tokens.map((line, lineIndex) => ( + + )) +} + +function renderIncrementalTokens(tokens: HighlightTokens, suffix: string) { + return ( + <> + {renderHighlightedTokens(tokens)} + {suffix ? {suffix} : null} + + ) +} + interface CodeBlockProps { code: string language?: string className?: string style?: React.CSSProperties - /** 是否显示语言标签和复制按钮的 header */ - showHeader?: boolean + /** Display variant: 'default' shows chrome (label + copy), 'reasoning' is minimal */ + variant?: 'default' | 'reasoning' /** 最大高度 */ maxHeight?: number + /** 长行自动换行 */ + wordwrap?: boolean + /** Render plain code and skip syntax highlighting. */ + deferHighlight?: boolean + /** Start highlighting even before in-view observation catches up. */ + forceHighlight?: boolean + /** Use incremental Shiki tokenization for streaming code. */ + streamingHighlight?: boolean } -export const CodeBlock = memo(function CodeBlock({ - code, - language, - className = '', +export const CodeBlock = memo(function CodeBlock({ + code, + language, + className = '', style, - showHeader = true, + variant = 'default', maxHeight, + wordwrap, + deferHighlight = false, + forceHighlight = false, + streamingHighlight = false, }: CodeBlockProps) { + const { codeWordWrap } = useSyncExternalStore(themeStore.subscribe, themeStore.getSnapshot) + const { preferTouchUi } = useInputCapabilities() + const resolvedWordWrap = wordwrap ?? codeWordWrap + const isReasoning = variant === 'reasoning' + const highlightCode = useDeferredValue(code) + // Lazy load highlighting when close to viewport const { ref, inView } = useInView({ triggerOnce: true, rootMargin: '200px' }) - + // Auto-detect tree structure if language is missing or text const effectiveLanguage = useMemo(() => { if (language && language !== 'text') return language - + // Check for tree structure characters - if (code.includes('├──') || code.includes('└──') || (code.includes('│') && code.includes('──'))) { - return 'yaml' // YAML formatting often looks good for trees + if ( + highlightCode.includes('├──') || + highlightCode.includes('└──') || + (highlightCode.includes('│') && highlightCode.includes('──')) + ) { + return 'yaml' } - + return language || 'text' - }, [code, language]) - - const { output: html } = useSyntaxHighlight(code, { lang: effectiveLanguage, enabled: inView }) + }, [highlightCode, language]) + + const shouldHighlight = !deferHighlight && (inView || forceHighlight) + const shouldStreamHighlight = shouldHighlight && streamingHighlight + + const { output: highlightedTokens } = useSyntaxHighlight(highlightCode, { + lang: effectiveLanguage, + enabled: shouldHighlight && !shouldStreamHighlight, + delayMs: 0, + mode: 'tokens', + }) + const { output: streamingTokens, highlightedCode: streamingHighlightedCode = code } = useStreamingSyntaxHighlight( + code, + { + lang: effectiveLanguage, + enabled: shouldStreamHighlight, + }, + ) + const tokens = deferHighlight ? null : (streamingTokens ?? highlightedTokens) + const [lastHighlight, setLastHighlight] = useState<{ code: string; tokens: HighlightTokens } | null>(null) + const tokenSourceCode = shouldStreamHighlight && streamingTokens ? streamingHighlightedCode : highlightCode + if (tokens && lastHighlight?.code !== tokenSourceCode) { + setLastHighlight({ code: tokenSourceCode, tokens }) + } + const activeHighlight = deferHighlight ? null : tokens ? { code: tokenSourceCode, tokens } : lastHighlight + + const displayedHighlight = + activeHighlight && code.startsWith(activeHighlight.code) + ? { + tokens: activeHighlight.tokens, + suffix: code.slice(activeHighlight.code.length), + } + : null const containerStyle = maxHeight ? { ...style, maxHeight } : style + const showLabel = !isReasoning && language && !HIDDEN_LANGS.has(language.toLowerCase()) + + // --- Shared sub-components --- + + const wrapClasses = resolvedWordWrap ? 'whitespace-pre-wrap break-words [overflow-wrap:anywhere]' : '' + + const scrollClasses = resolvedWordWrap + ? 'overflow-y-auto overflow-x-hidden custom-scrollbar select-text' + : 'overflow-auto custom-scrollbar select-text' + + // Padding: reasoning is tighter; default reserves top for label row and right for copy button + const contentPad = isReasoning ? 'p-3' : showLabel ? 'pt-0 pb-3.5 px-3.5' : 'p-4' + const requireTapToRevealCopy = preferTouchUi && !isReasoning && !showLabel + + const handleTouchStartCapture = useCallback( + (event: React.TouchEvent) => { + if (!requireTapToRevealCopy) return + if (event.target instanceof HTMLElement && event.target.closest('button')) return + event.currentTarget.focus() + }, + [requireTapToRevealCopy], + ) + + const fontSize = isReasoning ? 'text-[length:var(--fs-sm)]' : 'text-[length:var(--fs-md)]' + const lineHeight = isReasoning ? 'leading-5' : 'leading-6' + const textColor = isReasoning ? 'text-text-300' : 'text-text-200' - if (!showHeader) { - // 无 header 的紧凑模式 + const content = displayedHighlight ? ( +
+      
+        {renderIncrementalTokens(displayedHighlight.tokens, displayedHighlight.suffix)}
+      
+    
+ ) : ( +
+      {code}
+    
+ ) + + // --- Reasoning: minimal shell --- + if (isReasoning) { return ( -
-
- {!html ? ( -
-              {code}
-            
- ) : ( -
- )} +
+ {content}
) } + // --- Default: light panel with floating chrome --- return ( -
- {/* Header with Language and Copy */} -
- - {language || 'text'} - - -
- - {/* Scrollable Content */} -
- {!html ? ( -
-            {code}
-          
- ) : ( -
+
+ {language} +
+
+ +
+
+ ) : ( +
+ - )} +
+ )} + + {/* Scrollable content */} +
+ {content}
) diff --git a/src/components/CodeMirrorReadonly.tsx b/src/components/CodeMirrorReadonly.tsx new file mode 100644 index 00000000..f7a4be7a --- /dev/null +++ b/src/components/CodeMirrorReadonly.tsx @@ -0,0 +1,115 @@ +import { useCallback, useEffect, useMemo, useRef } from 'react' +import { EditorState, type Extension } from '@codemirror/state' +import { openSearchPanel } from '@codemirror/search' +import { EditorView } from '@codemirror/view' +import type { HighlightTokens } from '../hooks/useSyntaxHighlight' +import { createReadonlyCodeMirrorExtensions, dispatchShikiTokens } from './codeMirrorReadonlyExtensions' +import { getLineCount, getLineNumberColumnWidth } from '../utils/lineNumberUtils' + +interface CodeMirrorReadonlyProps { + code: string + tokensRef: React.RefObject + tokensVersion: number + wordWrap: boolean + lineHeight: number + maxHeight?: number + isResizing?: boolean + isVisible?: boolean + showLineNumbers?: boolean + className?: string + extraExtensions?: Extension[] +} + +export function CodeMirrorReadonly({ + code, + tokensRef, + tokensVersion, + wordWrap, + lineHeight, + maxHeight, + isResizing = false, + isVisible = true, + showLineNumbers = true, + className = '', + extraExtensions = [], +}: CodeMirrorReadonlyProps) { + const hostRef = useRef(null) + const viewRef = useRef(null) + const constrainedHeight = maxHeight !== undefined + const lineNumberWidth = useMemo(() => getLineNumberColumnWidth(getLineCount(code)), [code]) + + const extensions = useMemo( + () => + createReadonlyCodeMirrorExtensions({ + wordWrap, + lineHeight, + showLineNumbers, + maxHeight, + editable: !constrainedHeight, + lineNumberWidth, + extraExtensions, + }), + [wordWrap, lineHeight, showLineNumbers, maxHeight, constrainedHeight, lineNumberWidth, extraExtensions], + ) + + useEffect(() => { + const host = hostRef.current + if (!host) return + + const view = new EditorView({ + parent: host, + state: EditorState.create({ doc: code, extensions }), + }) + + viewRef.current = view + dispatchShikiTokens(view, tokensRef.current) + + return () => { + view.destroy() + if (viewRef.current === view) viewRef.current = null + } + }, [code, extensions, tokensRef]) + + useEffect(() => { + const view = viewRef.current + if (!view) return + dispatchShikiTokens(view, tokensRef.current) + }, [tokensRef, tokensVersion]) + + useEffect(() => { + const view = viewRef.current + if (!view || !isVisible) return + + let secondFrameId: number | null = null + const firstFrameId = requestAnimationFrame(() => { + view.requestMeasure() + secondFrameId = requestAnimationFrame(() => view.requestMeasure()) + }) + const transitionTimerId = window.setTimeout(() => view.requestMeasure(), 320) + + return () => { + cancelAnimationFrame(firstFrameId) + if (secondFrameId !== null) cancelAnimationFrame(secondFrameId) + clearTimeout(transitionTimerId) + } + }, [isVisible]) + + const handleKeyDownCapture = useCallback((event: React.KeyboardEvent) => { + if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'f') { + const view = viewRef.current + if (!view) return + event.preventDefault() + openSearchPanel(view) + } + }, []) + + return ( +
+
+
+ ) +} diff --git a/src/components/CodePreview.test.tsx b/src/components/CodePreview.test.tsx new file mode 100644 index 00000000..c30af0b5 --- /dev/null +++ b/src/components/CodePreview.test.tsx @@ -0,0 +1,95 @@ +import { act, fireEvent, render, screen } from '@testing-library/react' +import { EditorView } from '@codemirror/view' +import { describe, expect, it, vi } from 'vitest' +import { CodePreview } from './CodePreview' + +vi.mock('../store/themeStore', () => ({ + themeStore: { + subscribe: () => () => {}, + getSnapshot: () => mockThemeSnapshot, + }, +})) + +vi.mock('../hooks/useSyntaxHighlight', () => ({ + useSyntaxHighlightRef: () => ({ + tokensRef: { current: null }, + version: 0, + }), +})) + +const mockThemeSnapshot = { + codeWordWrap: false, + codeFontScale: 0, +} + +describe('CodePreview', () => { + it('renders code through CodeMirror with line numbers', () => { + const { container } = render() + + expect(screen.getByText('first line')).toBeInTheDocument() + expect(screen.getByText('second line')).toBeInTheDocument() + expect(screen.getByText('1')).toBeInTheDocument() + expect(screen.getByText('2')).toBeInTheDocument() + expect(container.querySelector('.cm-editor')).toBeInTheDocument() + }) + + it('keeps the editor focusable while read-only', () => { + const { container } = render( + , + ) + + expect(container.querySelector('.cm-content')).toHaveAttribute('contenteditable', 'true') + }) + + it('disables editable focus for constrained inline previews', () => { + const { container } = render() + + expect(container.querySelector('.cm-content')).toHaveAttribute('contenteditable', 'false') + }) + + it('opens CodeMirror search from the preview Ctrl+F fallback', () => { + const { container } = render() + + fireEvent.keyDown(container.firstElementChild as Element, { key: 'f', ctrlKey: true }) + + expect(screen.getByPlaceholderText('Find')).toBeInTheDocument() + expect(screen.getByRole('button', { name: 'Match case' })).toBeInTheDocument() + expect(screen.getByRole('button', { name: 'Use regular expression' })).toBeInTheDocument() + expect(screen.getByRole('button', { name: 'Match whole word' })).toBeInTheDocument() + expect(screen.getByRole('button', { name: 'Clear search' })).toBeInTheDocument() + expect(screen.getByText('No results')).toBeInTheDocument() + }) + + it('requests a CodeMirror measure when becoming visible again', async () => { + vi.useFakeTimers() + const requestMeasureSpy = vi.spyOn(EditorView.prototype, 'requestMeasure') + + try { + const { rerender } = render() + + await act(async () => { + await Promise.resolve() + }) + requestMeasureSpy.mockClear() + + rerender() + + await act(async () => { + vi.advanceTimersByTime(16) + await Promise.resolve() + }) + + expect(requestMeasureSpy).toHaveBeenCalled() + + await act(async () => { + vi.advanceTimersByTime(320) + await Promise.resolve() + }) + + expect(requestMeasureSpy.mock.calls.length).toBeGreaterThanOrEqual(2) + } finally { + requestMeasureSpy.mockRestore() + vi.useRealTimers() + } + }) +}) diff --git a/src/components/CodePreview.tsx b/src/components/CodePreview.tsx new file mode 100644 index 00000000..f5229a38 --- /dev/null +++ b/src/components/CodePreview.tsx @@ -0,0 +1,43 @@ +import { useSyncExternalStore } from 'react' +import { CodeMirrorReadonly } from './CodeMirrorReadonly' +import { codeLineHeight } from './codeMirrorReadonlyExtensions' +import { useSyntaxHighlightRef } from '../hooks/useSyntaxHighlight' +import { themeStore } from '../store/themeStore' + +interface CodePreviewProps { + code: string + language: string + maxHeight?: number + isResizing?: boolean + isVisible?: boolean + wordWrap?: boolean +} + +export function CodePreview({ + code, + language, + maxHeight, + isResizing = false, + isVisible = true, + wordWrap, +}: CodePreviewProps) { + const { codeWordWrap, codeFontScale } = useSyncExternalStore(themeStore.subscribe, themeStore.getSnapshot) + const resolvedWordWrap = wordWrap ?? codeWordWrap + const { tokensRef, version } = useSyntaxHighlightRef(code, { + lang: language, + enabled: language !== 'text', + }) + + return ( + + ) +} diff --git a/src/components/CommandPalette.test.tsx b/src/components/CommandPalette.test.tsx new file mode 100644 index 00000000..89b7d404 --- /dev/null +++ b/src/components/CommandPalette.test.tsx @@ -0,0 +1,58 @@ +import { act, fireEvent, render, screen } from '@testing-library/react' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { CommandPalette, type CommandItem } from './CommandPalette' + +describe('CommandPalette', () => { + beforeEach(() => { + vi.useFakeTimers() + vi.spyOn(window, 'requestAnimationFrame').mockImplementation(cb => { + return window.setTimeout(() => cb(performance.now()), 0) + }) + vi.spyOn(window, 'cancelAnimationFrame').mockImplementation(id => { + clearTimeout(id) + }) + }) + + afterEach(() => { + vi.restoreAllMocks() + vi.useRealTimers() + }) + + it('resets query when reopened and executes selected command', () => { + const onClose = vi.fn() + const action = vi.fn() + const commands: CommandItem[] = [{ id: 'open-settings', label: 'Open Settings', action }] + + const { rerender } = render() + + rerender() + act(() => { + vi.runAllTimers() + }) + + const input = screen.getByPlaceholderText('Type a command...') as HTMLInputElement + fireEvent.change(input, { target: { value: 'settings' } }) + expect(input.value).toBe('settings') + + fireEvent.click(screen.getByText('Open Settings')) + expect(onClose).toHaveBeenCalledTimes(1) + + act(() => { + vi.runAllTimers() + }) + expect(action).toHaveBeenCalledTimes(1) + + rerender() + act(() => { + vi.runAllTimers() + }) + expect(screen.queryByPlaceholderText('Type a command...')).not.toBeInTheDocument() + + rerender() + act(() => { + vi.runAllTimers() + }) + + expect((screen.getByPlaceholderText('Type a command...') as HTMLInputElement).value).toBe('') + }) +}) diff --git a/src/components/CommandPalette.tsx b/src/components/CommandPalette.tsx index 190c769a..664c039e 100644 --- a/src/components/CommandPalette.tsx +++ b/src/components/CommandPalette.tsx @@ -5,8 +5,10 @@ import { useState, useEffect, useRef, useMemo, useCallback } from 'react' import { createPortal } from 'react-dom' +import { useTranslation } from 'react-i18next' import { SearchIcon } from './Icons' import { formatKeybinding, parseKeybinding } from '../store/keybindingStore' +import { useDelayedRender } from '../hooks/useDelayedRender' // ============================================ // Types @@ -16,11 +18,11 @@ export interface CommandItem { id: string label: string description?: string - shortcut?: string // 快捷键显示文本 + shortcut?: string // 快捷键显示文本 category?: string icon?: React.ReactNode action: () => void - when?: () => boolean // 条件可见 + when?: () => boolean // 条件可见 } interface CommandPaletteProps { @@ -35,10 +37,12 @@ interface CommandPaletteProps { function Kbd({ children }: { children: React.ReactNode }) { return ( - + shadow-[0_1px_0_0_var(--border-200)]" + > {children} ) @@ -48,7 +52,7 @@ function ShortcutDisplay({ shortcut }: { shortcut: string }) { const parsed = parseKeybinding(shortcut) const formatted = formatKeybinding(parsed) const parts = formatted.split(' + ') - + return (
{parts.map((part, i) => ( @@ -63,67 +67,87 @@ function ShortcutDisplay({ shortcut }: { shortcut: string }) { // ============================================ export function CommandPalette({ isOpen, onClose, commands }: CommandPaletteProps) { + const { t } = useTranslation(['components', 'common']) const [query, setQuery] = useState('') const [selectedIndex, setSelectedIndex] = useState(0) const inputRef = useRef(null) const listRef = useRef(null) - const [shouldRender, setShouldRender] = useState(false) const [isVisible, setIsVisible] = useState(false) + const shouldRender = useDelayedRender(isOpen, 150) // Animation mount/unmount useEffect(() => { + let frameId: number | null = null + if (isOpen) { - setShouldRender(true) - setQuery('') - setSelectedIndex(0) - } else { - setIsVisible(false) - const timer = setTimeout(() => setShouldRender(false), 150) - return () => clearTimeout(timer) + frameId = requestAnimationFrame(() => { + setQuery('') + setSelectedIndex(0) + }) + } + + return () => { + if (frameId !== null) { + cancelAnimationFrame(frameId) + } } }, [isOpen]) useEffect(() => { + let frameId: number | null = null + if (shouldRender && isOpen) { - const timer = setTimeout(() => { + frameId = requestAnimationFrame(() => { setIsVisible(true) inputRef.current?.focus() - }, 10) - return () => clearTimeout(timer) + }) + } else { + frameId = requestAnimationFrame(() => { + setIsVisible(false) + }) + } + + return () => { + if (frameId !== null) { + cancelAnimationFrame(frameId) + } } }, [shouldRender, isOpen]) // Filter commands const filteredCommands = useMemo(() => { const visible = commands.filter(cmd => !cmd.when || cmd.when()) - + if (!query.trim()) return visible - + const q = query.toLowerCase() - return visible.filter(cmd => - cmd.label.toLowerCase().includes(q) || - (cmd.description?.toLowerCase().includes(q)) || - (cmd.category?.toLowerCase().includes(q)) || - cmd.id.toLowerCase().includes(q) - ).sort((a, b) => { - // 精确前缀匹配优先 - const aStart = a.label.toLowerCase().startsWith(q) ? 0 : 1 - const bStart = b.label.toLowerCase().startsWith(q) ? 0 : 1 - return aStart - bStart - }) + return visible + .filter( + cmd => + cmd.label.toLowerCase().includes(q) || + cmd.description?.toLowerCase().includes(q) || + cmd.category?.toLowerCase().includes(q) || + cmd.id.toLowerCase().includes(q), + ) + .sort((a, b) => { + // 精确前缀匹配优先 + const aStart = a.label.toLowerCase().startsWith(q) ? 0 : 1 + const bStart = b.label.toLowerCase().startsWith(q) ? 0 : 1 + return aStart - bStart + }) }, [commands, query]) - // Reset selection when filter changes - useEffect(() => { - setSelectedIndex(0) - }, [query]) + const activeIndex = filteredCommands.length === 0 ? 0 : Math.min(selectedIndex, filteredCommands.length - 1) // Execute command - const executeCommand = useCallback((cmd: CommandItem) => { - onClose() - // 延迟执行,让面板关闭动画先完成 - requestAnimationFrame(() => cmd.action()) - }, [onClose]) + const executeCommand = useCallback( + (cmd: CommandItem) => { + onClose() + // 延迟执行,让面板关闭动画先完成 + requestAnimationFrame(() => cmd.action()) + }, + [onClose], + ) // Keyboard navigation useEffect(() => { @@ -133,20 +157,16 @@ export function CommandPalette({ isOpen, onClose, commands }: CommandPaletteProp switch (e.key) { case 'ArrowDown': e.preventDefault() - setSelectedIndex(prev => - prev < filteredCommands.length - 1 ? prev + 1 : 0 - ) + setSelectedIndex(prev => (prev < filteredCommands.length - 1 ? prev + 1 : 0)) break case 'ArrowUp': e.preventDefault() - setSelectedIndex(prev => - prev > 0 ? prev - 1 : filteredCommands.length - 1 - ) + setSelectedIndex(prev => (prev > 0 ? prev - 1 : filteredCommands.length - 1)) break case 'Enter': e.preventDefault() - if (filteredCommands[selectedIndex]) { - executeCommand(filteredCommands[selectedIndex]) + if (filteredCommands[activeIndex]) { + executeCommand(filteredCommands[activeIndex]) } break case 'Escape': @@ -159,68 +179,83 @@ export function CommandPalette({ isOpen, onClose, commands }: CommandPaletteProp document.addEventListener('keydown', handleKeyDown, { capture: true }) return () => document.removeEventListener('keydown', handleKeyDown, { capture: true }) - }, [isOpen, filteredCommands, selectedIndex, executeCommand, onClose]) + }, [isOpen, filteredCommands, activeIndex, executeCommand, onClose]) // Scroll selected item into view useEffect(() => { if (!listRef.current) return - const el = listRef.current.querySelector(`[data-index="${selectedIndex}"]`) + const el = listRef.current.querySelector(`[data-index="${activeIndex}"]`) el?.scrollIntoView({ block: 'nearest' }) - }, [selectedIndex]) + }, [activeIndex]) if (!shouldRender) return null return createPortal( -
{ - if (e.target === e.currentTarget) onClose() + onPointerDown={(e: React.PointerEvent) => { + // 触摸设备不走背景关闭 + if (e.pointerType === 'touch') return + if (e.target === e.currentTarget) { + ;(e.currentTarget as HTMLElement).dataset.backdropDown = '1' + } + }} + onClick={e => { + if (e.target === e.currentTarget && (e.currentTarget as HTMLElement).dataset.backdropDown === '1') { + onClose() + } + delete (e.currentTarget as HTMLElement).dataset.backdropDown }} > -
e.stopPropagation()} + onClick={e => e.stopPropagation()} > {/* Search Input */} -
+
setQuery(e.target.value)} - placeholder="Type a command..." - className="flex-1 py-3.5 text-sm bg-transparent text-text-100 placeholder:text-text-400 + onChange={e => { + setQuery(e.target.value) + setSelectedIndex(0) + }} + placeholder={t('commandPalette.placeholder')} + className="flex-1 py-3.5 text-[length:var(--fs-base)] bg-transparent text-text-100 placeholder:text-text-400 outline-none border-none" autoComplete="off" spellCheck={false} /> {query && ( - )} +
{/* Command List */} -
+
{filteredCommands.length === 0 ? ( -
- No commands found -
+
{t('commandPalette.noCommandsFound')}
) : ( filteredCommands.map((cmd, index) => ( )} - + {/* Exit code */} {stats?.exit !== undefined && ( - - exit {stats.exit} + + {t('contentBlock.exitCode', { code: stats.exit })} )}
{/* Body - grid collapse animation */} -
+
- {/* Loading skeleton */} - {isLoading && !hasContent && ( -
-
-
-
- )} - {/* Content */} - {hasContent && ( + {shouldRenderContent && hasContent && (
{content && } - + {isDiff && resolvedDiff ? ( ) : content?.trim() ? ( - - ) : stats?.exit !== undefined ? ( -
- {stats.exit === 0 ? 'Completed successfully' : 'No output'} -
+ ) : null}
)}
- - {/* Fullscreen Viewer - 支持 diff 和代码 */} - {isDiff && diff ? ( - setFullscreenOpen(false)} - diff={diff} - filePath={filePath} - language={lang} - diffStats={diffStats || undefined} - /> - ) : content?.trim() ? ( - setFullscreenOpen(false)} - content={content} - filePath={filePath} - language={lang} - /> - ) : null}
) }) diff --git a/src/components/DesktopTitlebar.test.tsx b/src/components/DesktopTitlebar.test.tsx new file mode 100644 index 00000000..250a3b94 --- /dev/null +++ b/src/components/DesktopTitlebar.test.tsx @@ -0,0 +1,58 @@ +import { render, screen } from '@testing-library/react' +import { describe, expect, it, vi } from 'vitest' +import { DesktopTitlebar } from './DesktopTitlebar' + +const { + useThemeMock, + useUpdateStoreMock, + hasUpdateAvailableMock, + getDesktopPlatformMock, + usesCustomDesktopTitlebarMock, +} = vi.hoisted(() => ({ + useThemeMock: vi.fn(() => ({ mode: 'dark', resolvedTheme: 'dark' })), + useUpdateStoreMock: vi.fn(() => ({})), + hasUpdateAvailableMock: vi.fn(() => false), + getDesktopPlatformMock: vi.fn(() => 'windows'), + usesCustomDesktopTitlebarMock: vi.fn(() => true), +})) + +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ t: (key: string) => key }), +})) + +vi.mock('../hooks/useTheme', () => ({ + useTheme: () => useThemeMock(), +})) + +vi.mock('../store/updateStore', () => ({ + useUpdateStore: () => useUpdateStoreMock(), + hasUpdateAvailable: () => hasUpdateAvailableMock(), +})) + +vi.mock('../utils/tauri', () => ({ + isTauri: () => false, + getDesktopPlatform: () => getDesktopPlatformMock(), + usesCustomDesktopTitlebar: () => usesCustomDesktopTitlebarMock(), +})) + +describe('DesktopTitlebar', () => { + it('preserves externally injected window controls across rerenders and remounts', () => { + const { rerender, unmount } = render() + + const controlsHost = document.querySelector('[data-tauri-decorum-tb]') as HTMLDivElement | null + expect(controlsHost).not.toBeNull() + + const injectedControl = document.createElement('button') + injectedControl.textContent = 'Minimize' + controlsHost!.appendChild(injectedControl) + + rerender() + + expect(screen.getByText('Minimize')).toBeInTheDocument() + + unmount() + render() + + expect(screen.getByText('Minimize')).toBeInTheDocument() + }) +}) diff --git a/src/components/DesktopTitlebar.tsx b/src/components/DesktopTitlebar.tsx new file mode 100644 index 00000000..afb4909c --- /dev/null +++ b/src/components/DesktopTitlebar.tsx @@ -0,0 +1,220 @@ +import { memo, useCallback, useEffect, useMemo, useRef } from 'react' +import { + DESKTOP_MACOS_TRAFFIC_LIGHTS_WIDTH, + DESKTOP_TITLEBAR_CONTROLS_Z_INDEX, + DESKTOP_TITLEBAR_HEIGHT, + DESKTOP_TITLEBAR_Z_INDEX, +} from '../constants' +import { ChevronLeftIcon, ChevronRightIcon, FolderOpenIcon, SettingsIcon, AppWindowIcon } from './Icons' +import { useTranslation } from 'react-i18next' +import { useTheme } from '../hooks/useTheme' +import { getDesktopPlatform, isTauri, usesCustomDesktopTitlebar } from '../utils/tauri' +import { useUpdateStore, hasUpdateAvailable } from '../store/updateStore' + +/* ---- 持久化标题栏控制按钮容器 ---- */ +const DECORUM_HOST_SELECTOR = '[data-tauri-decorum-tb]' +const DECORUM_BUTTON_SELECTOR = '.decorum-tb-btn, button[id^="decorum-tb-"]' +let persistentTbHost: HTMLDivElement | null = null + +function prepareTbHost(host: HTMLDivElement): HTMLDivElement { + host.setAttribute('data-tauri-decorum-tb', '') + host.className = 'desktop-titlebar-controls flex h-full min-w-[138px] shrink-0 items-stretch justify-end' + host.style.cssText = '' + host.style.zIndex = String(DESKTOP_TITLEBAR_CONTROLS_Z_INDEX) + return host +} + +function getOrCreateTbHost(): HTMLDivElement { + if (!persistentTbHost) { + const hosts = Array.from(document.querySelectorAll(DECORUM_HOST_SELECTOR)) + persistentTbHost = + hosts.find(host => host.querySelector(DECORUM_BUTTON_SELECTOR)) ?? hosts[0] ?? document.createElement('div') + + for (const host of hosts) { + if (host !== persistentTbHost && !host.querySelector(DECORUM_BUTTON_SELECTOR)) { + host.remove() + } + } + } + + return prepareTbHost(persistentTbHost) +} + +/* 标题栏图标按钮通用样式 — Windows 和 macOS 视觉节奏不同,按钮尺寸分开控制 */ +const TB_BTN = + 'inline-flex h-full w-8 items-center justify-center text-text-300 transition-colors hover:bg-bg-200/70 hover:text-text-100' +const TB_BTN_MAC = + 'inline-flex h-7 w-7 items-center justify-center rounded-md text-text-300 transition-colors hover:bg-bg-200/70 hover:text-text-100' +const TB_BTN_MAC_UPDATE = + 'inline-flex h-7 w-7 items-center justify-center rounded-md text-accent-main-100 transition-colors hover:bg-accent-main-100/10' + +const WindowsControlsHost = memo(function WindowsControlsHost() { + const mountRef = useRef(null) + + useEffect(() => { + const host = getOrCreateTbHost() + mountRef.current?.appendChild(host) + + return () => { + if (host.parentNode) { + host.parentNode.removeChild(host) + } + } + }, []) + + return
+}) + +export function DesktopTitlebar() { + const { t } = useTranslation('components') + const { mode, resolvedTheme } = useTheme() + const updateState = useUpdateStore() + const hasUpdate = hasUpdateAvailable(updateState) + const platform = useMemo(() => getDesktopPlatform(), []) + const isDesktopChrome = useMemo(() => usesCustomDesktopTitlebar(), []) + const titlebarButtonClass = platform === 'macos' ? TB_BTN_MAC : TB_BTN + + /* ---- 原生主题同步 ---- */ + useEffect(() => { + if (!isDesktopChrome) return + // 让 overlay 侧边栏知道标题栏高度 + document.documentElement.style.setProperty('--desktop-titlebar-height', `${DESKTOP_TITLEBAR_HEIGHT}px`) + return () => { + document.documentElement.style.removeProperty('--desktop-titlebar-height') + } + }, [isDesktopChrome]) + + useEffect(() => { + if (!isDesktopChrome) return + + let cancelled = false + const theme = mode === 'system' ? null : resolvedTheme + + void import('@tauri-apps/api/window').then(async ({ getCurrentWindow }) => { + if (cancelled) return + try { + await getCurrentWindow().setTheme(theme) + } catch { + // best effort + } + }) + + return () => { + cancelled = true + } + }, [isDesktopChrome, mode, resolvedTheme]) + + /* ---- 导航 ---- */ + const handleBack = useCallback(() => { + window.history.back() + }, []) + + const handleForward = useCallback(() => { + window.history.forward() + }, []) + + /* ---- 功能操作 ---- */ + const handleOpenProject = useCallback(() => { + window.dispatchEvent(new CustomEvent('titlebar:open-project')) + }, []) + + const handleOpenSettings = useCallback(() => { + window.dispatchEvent(new CustomEvent('titlebar:open-settings')) + }, []) + + const handleNewWindow = useCallback(() => { + if (!isTauri()) return + void import('@tauri-apps/api/core').then(({ invoke }) => { + invoke('open_new_window', { directory: null }).catch(() => { + // 静默 + }) + }) + }, []) + + if (!isDesktopChrome) return null + + return ( +
+ {/* ---- 左侧:平台占位 + 导航 + 分隔 + 功能按钮 ---- */} +
+ {platform === 'macos' ? ( +
+ ) : ( +
+ )} + + {/* 后退 / 前进 */} + + + + {/* 打开项目 */} + + + {/* 设置 */} + + + {/* 新建窗口 */} + +
+ + {/* ---- 中间:拖拽区 ---- */} +
+ + {/* ---- 右侧:Windows 控制按钮 / macOS 留白 ---- */} + {platform === 'windows' ? ( + + ) : ( +
+ )} +
+ ) +} diff --git a/src/components/DiffModal.tsx b/src/components/DiffModal.tsx deleted file mode 100644 index 248622a5..00000000 --- a/src/components/DiffModal.tsx +++ /dev/null @@ -1,198 +0,0 @@ -/** - * DiffModal - 全屏 Diff 查看器 - * - * VSCode 风格:全屏铺满 + 毛玻璃背景 + 顶部操作栏 - */ - -import { memo, useState, useEffect, useMemo } from 'react' -import { createPortal } from 'react-dom' -import { diffLines } from 'diff' -import { CloseIcon } from './Icons' -import { detectLanguage } from '../utils/languageUtils' -import { DiffViewer, extractContentFromUnifiedDiff, type ViewMode } from './DiffViewer' - -// ============================================ -// Types -// ============================================ - -interface DiffModalProps { - isOpen: boolean - onClose: () => void - diff: { before: string; after: string } | string - filePath?: string - language?: string - diffStats?: { additions: number; deletions: number } -} - -// ============================================ -// Main Component -// ============================================ - -export const DiffModal = memo(function DiffModal({ - isOpen, - onClose, - diff, - filePath, - language, - diffStats: providedStats, -}: DiffModalProps) { - const [shouldRender, setShouldRender] = useState(false) - const [isVisible, setIsVisible] = useState(false) - const [viewMode, setViewMode] = useState('split') - - useEffect(() => { - const checkWidth = () => setViewMode(window.innerWidth >= 1000 ? 'split' : 'unified') - checkWidth() - window.addEventListener('resize', checkWidth) - return () => window.removeEventListener('resize', checkWidth) - }, []) - - useEffect(() => { - if (isOpen) { - setShouldRender(true) - } else { - setIsVisible(false) - const timer = setTimeout(() => setShouldRender(false), 200) - return () => clearTimeout(timer) - } - }, [isOpen]) - - useEffect(() => { - if (shouldRender && isOpen) { - const timer = setTimeout(() => setIsVisible(true), 10) - return () => clearTimeout(timer) - } - }, [shouldRender, isOpen]) - - useEffect(() => { - const handleKeyDown = (e: KeyboardEvent) => { - if (e.key === 'Escape' && isOpen) onClose() - } - document.addEventListener('keydown', handleKeyDown) - return () => document.removeEventListener('keydown', handleKeyDown) - }, [isOpen, onClose]) - - const { before, after } = useMemo(() => { - if (typeof diff === 'object') return diff - return extractContentFromUnifiedDiff(diff) - }, [diff]) - - const lang = language || detectLanguage(filePath) || 'text' - const fileName = filePath?.split(/[/\\]/).pop() - - const diffStats = useMemo(() => { - if (providedStats) return providedStats - const changes = diffLines(before, after) - let additions = 0, deletions = 0 - for (const c of changes) { - if (c.added) additions += c.count || 0 - if (c.removed) deletions += c.count || 0 - } - return { additions, deletions } - }, [before, after, providedStats]) - - if (!shouldRender) return null - - return createPortal( -
- {/* 内容面板 - 全屏铺满但有不透明背景 */} -
- {/* Toolbar */} -
- {/* Left: file info */} -
- {fileName && ( - - {fileName} - - )} - {filePath && fileName && filePath !== fileName && ( - - {filePath} - - )} -
- {diffStats.additions > 0 && +{diffStats.additions}} - {diffStats.deletions > 0 && -{diffStats.deletions}} -
-
- - {/* Right: controls */} -
- -
- -
-
- - {/* Diff — 填满剩余空间 */} -
- -
-
-
, - document.body - ) -}) - -// ============================================ -// ViewModeSwitch -// ============================================ - -export function ViewModeSwitch({ - viewMode, - onChange, -}: { - viewMode: ViewMode - onChange: (mode: ViewMode) => void -}) { - return ( -
- - -
- ) -} diff --git a/src/components/DiffView.test.tsx b/src/components/DiffView.test.tsx new file mode 100644 index 00000000..64888777 --- /dev/null +++ b/src/components/DiffView.test.tsx @@ -0,0 +1,39 @@ +import type { ReactNode } from 'react' +import { fireEvent, render, screen } from '@testing-library/react' +import { describe, expect, it, vi } from 'vitest' +import { DiffView } from './DiffView' +import { FullscreenProvider } from '../contexts' + +vi.mock('../hooks/useSyntaxHighlight', () => ({ + useSyntaxHighlight: (code: string, options?: { mode?: 'html' | 'tokens' }) => ({ + output: options?.mode === 'tokens' ? code.split('\n').map(line => [{ content: line, color: '#fff' }]) : null, + isLoading: false, + }), +})) + +vi.mock('./FullscreenViewer', () => ({ + FullscreenViewer: ({ isOpen, children }: { isOpen: boolean; children?: ReactNode }) => + isOpen ?
{children}
: null, + ViewModeSwitch: () =>
switch
, +})) + +describe('DiffView', () => { + it('renders diff stats and can open fullscreen viewer', () => { + render( + + + , + ) + + expect(screen.getByText('app.ts')).toBeInTheDocument() + expect(screen.getByText('+1')).toBeInTheDocument() + expect(screen.getByText('-1')).toBeInTheDocument() + + fireEvent.click(screen.getByTitle('Fullscreen')) + expect(screen.getByTestId('fullscreen-viewer')).toBeInTheDocument() + }) +}) diff --git a/src/components/DiffView.tsx b/src/components/DiffView.tsx index 88edcc6a..6664a8a0 100644 --- a/src/components/DiffView.tsx +++ b/src/components/DiffView.tsx @@ -1,11 +1,13 @@ -import { memo, useState, useEffect, useMemo } from 'react' +import { memo, useState, useMemo, useEffect, useId } from 'react' +import { useTranslation } from 'react-i18next' import { diffLines } from 'diff' import { ChevronDownIcon, MaximizeIcon } from './Icons' import { clsx } from 'clsx' -import { useSyntaxHighlight } from '../hooks/useSyntaxHighlight' import { detectLanguage } from '../utils/languageUtils' -import { syntaxErrorHandler } from '../utils' -import { FullscreenViewer } from './FullscreenViewer' +import { extractContentFromUnifiedDiff } from '../utils/diffUtils' +import { ViewModeSwitch } from './FullscreenViewer' +import { DiffViewer, useDiffViewerData, type ViewMode } from './DiffViewer' +import { useFullscreenLayer } from '../contexts' interface DiffViewProps { /** Unified diff format string */ @@ -24,155 +26,17 @@ interface DiffViewProps { language?: string } -interface DiffLine { - type: 'add' | 'delete' | 'context' - content: string // HTML content string - oldLineNo?: number - newLineNo?: number -} - -// Simple HTML escaper -function escapeHtml(str: string) { - return str - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, ''') -} - -// Parse start line numbers from unified diff string -function getStartLine(diffString?: string): { old: number, new: number } { - if (!diffString) return { old: 1, new: 1 } - // Match the first hunk header: @@ -1,1 +1,2 @@ - const match = diffString.match(/^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/m) - if (match) { - return { old: parseInt(match[1], 10), new: parseInt(match[2], 10) } +/** 轻量统计 additions/deletions,不做高亮 */ +function computeDiffStats(before: string, after: string) { + const changes = diffLines(before, after) + let additions = 0 + let deletions = 0 + for (const change of changes) { + const count = change.count || 0 + if (change.added) additions += count + else if (change.removed) deletions += count } - return { old: 1, new: 1 } -} - -// Extract content (before/after) from unified diff string -function extractContentFromDiff(diff: string): { before: string, after: string } { - const lines = diff.split('\n'); - let before = ''; - let after = ''; - - for (const line of lines) { - if (line.startsWith('Index:')) continue; - if (line.startsWith('===')) continue; - if (line.startsWith('---')) continue; - if (line.startsWith('+++')) continue; - if (line.startsWith('@@')) continue; - if (line.startsWith('\\ No newline')) continue; - - if (line.startsWith(' ')) { - const content = line.slice(1) + '\n'; - before += content; - after += content; - } else if (line.startsWith('-')) { - const content = line.slice(1) + '\n'; - before += content; - } else if (line.startsWith('+')) { - const content = line.slice(1) + '\n'; - after += content; - } - } - - return { before: before.trimEnd(), after: after.trimEnd() }; -} - -// Hook to highlight code using tokens API (Robust) -function useHighlightedDiff(before: string, after: string, language: string) { - // Use generic hooks for highlighting - const { output: oldTokens } = useSyntaxHighlight(before, { lang: language, mode: 'tokens' }) - const { output: newTokens } = useSyntaxHighlight(after, { lang: language, mode: 'tokens' }) - - const [result, setResult] = useState<{ - lines: DiffLine[], - stats: { additions: number, deletions: number } - } | null>(null) - - useEffect(() => { - // If tokens are not ready or reset, reset result - if (!oldTokens || !newTokens) { - setResult(null) - return - } - - try { - // 1. Calculate diff on raw text - const changes = diffLines(before, after, { newlineIsToken: false }) - - // 2. Convert tokens to HTML strings per line - const tokensToHtmlLines = (tokenLines: any[][]) => { - return tokenLines.map(lineTokens => { - if (lineTokens.length === 0) return ' ' // Empty line - return lineTokens.map(t => - `${escapeHtml(t.content)}` - ).join('') - }) - } - - const oldLinesHtml = tokensToHtmlLines(oldTokens) - const newLinesHtml = tokensToHtmlLines(newTokens) - - // 3. Map diff changes to highlighted lines - const finalLines: DiffLine[] = [] - let oldIndex = 0 - let newIndex = 0 - let additions = 0 - let deletions = 0 - - for (const change of changes) { - const count = change.count || 0 - - if (change.removed) { - deletions += count - for (let i = 0; i < count; i++) { - finalLines.push({ - type: 'delete', - content: oldLinesHtml[oldIndex + i] || ' ', - oldLineNo: oldIndex + i + 1 - }) - } - oldIndex += count - } else if (change.added) { - additions += count - for (let i = 0; i < count; i++) { - finalLines.push({ - type: 'add', - content: newLinesHtml[newIndex + i] || ' ', - newLineNo: newIndex + i + 1 - }) - } - newIndex += count - } else { - // Context (unchanged) - for (let i = 0; i < count; i++) { - finalLines.push({ - type: 'context', - content: newLinesHtml[newIndex + i] || ' ', // Prefer new version for context - oldLineNo: oldIndex + i + 1, - newLineNo: newIndex + i + 1 - }) - } - oldIndex += count - newIndex += count - } - } - - setResult({ - lines: finalLines, - stats: { additions, deletions } - }) - - } catch (err) { - syntaxErrorHandler('diff highlighting', err) - } - }, [before, after, oldTokens, newTokens]) - - return result + return { additions, deletions } } export const DiffView = memo(function DiffView({ @@ -182,91 +46,123 @@ export const DiffView = memo(function DiffView({ filePath, defaultCollapsed = false, maxHeight = 300, - language: explicitLanguage + language: explicitLanguage, }: DiffViewProps) { + const { t } = useTranslation(['components', 'common']) const [collapsed, setCollapsed] = useState(defaultCollapsed) - const [modalOpen, setModalOpen] = useState(false) - + const [fullscreenViewMode, setFullscreenViewMode] = useState('split') + const generatedFullscreenId = useId() + // Determine content to diff const content = useMemo(() => { if (before !== undefined && after !== undefined) { return { before, after } } if (diff) { - return extractContentFromDiff(diff) + return extractContentFromUnifiedDiff(diff) } return null }, [before, after, diff]) const hasContent = content !== null - - // Hook calls must be unconditional + const language = useMemo(() => { return explicitLanguage || detectLanguage(filePath) }, [filePath, explicitLanguage]) - const diffResult = useHighlightedDiff(content?.before || '', content?.after || '', language) - - // Calculate start lines from unified diff header to show correct line numbers - const startLines = useMemo(() => getStartLine(diff), [diff]) + const diffViewerData = useDiffViewerData(content?.before ?? '', content?.after ?? '', language) - // Fallback for unified diff string only (should rarely happen now as we extract content) - if (!hasContent && diff) { - return ( -
- {diff} -
- ) - } + const stats = useMemo(() => { + if (!content) return { additions: 0, deletions: 0 } + return computeDiffStats(content.before, content.after) + }, [content]) + const fileName = filePath ? filePath.split(/[/\\]/).pop() : undefined + const fullscreenLayer = useMemo( + () => + content + ? { + id: `diff-view:${generatedFullscreenId}`, + title: fileName, + titleExtra: ( +
+ {stats.additions > 0 && +{stats.additions}} + {stats.deletions > 0 && -{stats.deletions}} +
+ ), + headerRight: , + deferContent: true, + content: ( + + ), + } + : null, + [ + content, + diffViewerData, + fileName, + fullscreenViewMode, + generatedFullscreenId, + language, + stats.additions, + stats.deletions, + ], + ) + const { isOpen: fullscreenOpen, open: openFullscreen } = useFullscreenLayer(fullscreenLayer) - if (!hasContent) return null - - // Loading state - if (hasContent && !diffResult) { + // 响应式 diff view mode(全屏弹窗用) + useEffect(() => { + if (!fullscreenOpen) return + const checkWidth = () => setFullscreenViewMode(window.innerWidth >= 1000 ? 'split' : 'unified') + checkWidth() + window.addEventListener('resize', checkWidth) + return () => window.removeEventListener('resize', checkWidth) + }, [fullscreenOpen]) + + // Fallback for unified diff string only + if (!hasContent && diff) { return ( -
- Generating diff... +
+ {diff}
) } - const { lines, stats } = diffResult! - const fileName = filePath ? filePath.split(/[/\\]/).pop() : undefined + if (!hasContent) return null return ( -
+
{/* Header */} -
setCollapsed(!collapsed)} > -
-
- +
+
+
- {fileName && ( - {fileName} - )} + {fileName && {fileName}}
-
- {stats.additions > 0 && ( - +{stats.additions} - )} - {stats.deletions > 0 && ( - -{stats.deletions} - )} +
+ {stats.additions > 0 && +{stats.additions}} + {stats.deletions > 0 && -{stats.deletions}} {stats.additions === 0 && stats.deletions === 0 && ( - No changes + {t('common:noChanges')} )} - + {/* 放大按钮 */} @@ -274,80 +170,23 @@ export const DiffView = memo(function DiffView({
{/* Content - 使用 grid 实现平滑展开动画 */} -
+
-
- - - - - - - - {lines.map((line, idx) => { - const rowBgClass = clsx( - line.type === 'add' && "bg-success-bg", - line.type === 'delete' && "bg-danger-bg" - ) - return ( - - {/* Old Line Number */} - - - {/* New Line Number */} - - - {/* Code Content */} - - - )})} - -
- {line.type !== 'add' && (line.oldLineNo! + startLines.old - 1)} - - {line.type !== 'delete' && (line.newLineNo! + startLines.new - 1)} - - {/* Indicator for add/delete */} - {(line.type === 'add' || line.type === 'delete') && ( - - {line.type === 'add' ? '+' : '-'} - - )} - - {/* The code itself */} -
-
-
+
- - {/* Diff Modal */} - {content && ( - setModalOpen(false)} - diff={content} - filePath={filePath} - language={language} - diffStats={stats} - /> - )}
) }) diff --git a/src/components/DiffViewer.test.tsx b/src/components/DiffViewer.test.tsx new file mode 100644 index 00000000..e61609d8 --- /dev/null +++ b/src/components/DiffViewer.test.tsx @@ -0,0 +1,97 @@ +import { fireEvent, render, screen, waitFor } from '@testing-library/react' +import { describe, expect, it, vi } from 'vitest' +import { DiffViewer } from './DiffViewer' + +vi.mock('../store/themeStore', async importOriginal => { + const actual = await importOriginal() + const snapshot = { diffStyle: 'lineNumbers' as const, codeWordWrap: false, codeFontScale: 0 } + return { + ...actual, + themeStore: { + subscribe: () => () => {}, + getSnapshot: () => snapshot, + }, + } +}) + +vi.mock('../hooks/useSyntaxHighlight', () => ({ + useSyntaxHighlight: (code: string, options?: { mode?: 'html' | 'tokens' }) => ({ + output: options?.mode === 'tokens' ? code.split('\n').map(line => [{ content: line, color: '#fff' }]) : null, + isLoading: false, + }), +})) + +describe('DiffViewer', () => { + it('uses wrapped rendering without proxy horizontal scrollbar when word wrap is enabled', () => { + const { container } = render( + , + ) + + expect(screen.getByText('const someRidiculouslyLongIdentifierName = oldValue')).toBeInTheDocument() + expect(screen.getByText('const someRidiculouslyLongIdentifierName = newValue')).toBeInTheDocument() + expect(container.querySelector('.sticky')).toBeNull() + }) + + it('keeps empty split content texture anchored while scrolling horizontally', async () => { + const { container } = render( + , + ) + + const scrollPanels = container.querySelectorAll('.scrollbar-none') + const leftContent = scrollPanels[0] as HTMLDivElement + + const emptyBuffer = container.querySelector('.diff-empty-content-buffer.min-w-full') as HTMLDivElement + const initialX = Number.parseFloat(emptyBuffer.style.backgroundPosition.split(' ')[0] ?? '0') + + Object.defineProperty(leftContent, 'scrollLeft', { value: 37, configurable: true }) + fireEvent.scroll(leftContent) + + await waitFor(() => { + const updatedBuffer = container.querySelector('.diff-empty-content-buffer.min-w-full') as HTMLDivElement + const updatedX = Number.parseFloat(updatedBuffer.style.backgroundPosition.split(' ')[0] ?? '0') + expect(updatedX - initialX).toBe(37) + }) + }) + + it('keeps split word-diff changed text themed when syntax highlighting is unavailable', () => { + const { container } = render( + , + ) + + const changedSegments = Array.from(container.querySelectorAll('span')).filter( + segment => segment.classList.contains('bg-danger-100/30') || segment.classList.contains('bg-success-100/30'), + ) + + expect(changedSegments.length).toBeGreaterThan(0) + for (const segment of changedSegments) { + expect(segment.closest('.text-text-100')).not.toBeNull() + } + }) + + it('does not duplicate trailing characters when split word diff is syntax highlighted', () => { + const { container } = render( + 0) return'} + language="ts" + viewMode="split" + wordWrap={false} + />, + ) + + expect(container.textContent).toContain('if (!enabled) return') + expect(container.textContent).not.toContain('returnn') + }) +}) diff --git a/src/components/DiffViewer.tsx b/src/components/DiffViewer.tsx index b871a790..e096a6d3 100644 --- a/src/components/DiffViewer.tsx +++ b/src/components/DiffViewer.tsx @@ -1,73 +1,33 @@ /** * DiffViewer - 核心 Diff 渲染组件 - * - * 参考 FileExplorer 的 CodePreview 实现: - * 1. 始终使用虚拟滚动 - * 2. 填满父容器(h-full) - * 3. 大文件跳过词级别diff和语法高亮 + * + * 两列架构(和 CodePreview 一致): + * - Gutter 列:change bar(3px 竖条,增绿删红)+ 行号,固定不水平滚动 + * - Content 列:代码内容,独立水平滚动 + * + * 默认使用虚拟滚动;启用自动换行后切到 wrapped 渲染 + * 不再按文件大小、行数、字符数降级高亮或 diff */ -import { memo, useMemo, useRef, useState, useEffect, useCallback } from 'react' -import { diffLines, diffWords } from 'diff' -import { useSyntaxHighlight } from '../hooks/useSyntaxHighlight' +import { memo, useMemo, useRef, useState, useEffect, useCallback, useSyncExternalStore, type CSSProperties } from 'react' +import { useTranslation } from 'react-i18next' +import { diffLines, diffWordsWithSpace } from 'diff' +import { useSyntaxHighlight, type HighlightTokens } from '../hooks/useSyntaxHighlight' +import { useDynamicVirtualScroll } from '../hooks/useDynamicVirtualScroll' +import { themeStore } from '../store/themeStore' +import type { DiffStyle } from '../store/themeStore' +import { getLineCount, getLineNumberColumnWidth } from '../utils/lineNumberUtils' +import { useUiState } from '../utils/uiDisclosureState' // ============================================ // 常量 // ============================================ -const LINE_HEIGHT = 20 // 和 CodePreview 保持一致 -const OVERSCAN = 5 - -// 大文件阈值 - 超过则跳过词级别diff -const LARGE_FILE_LINES = 2000 -const LARGE_FILE_CHARS = 300000 - -// 自适应高度阈值 - 行数少于此值时不用虚拟滚动 -const AUTO_HEIGHT_THRESHOLD = 100 - -// 滚动节流间隔(毫秒) -const SCROLL_THROTTLE_MS = 16 // ~60fps - -// ============================================ -// Throttle 工具函数 -// ============================================ - -function throttle void>( - fn: T, - delay: number -): T & { cancel: () => void } { - let lastCall = 0 - let timeoutId: ReturnType | null = null - - const throttled = ((...args: Parameters) => { - const now = Date.now() - const remaining = delay - (now - lastCall) - - if (remaining <= 0) { - if (timeoutId) { - clearTimeout(timeoutId) - timeoutId = null - } - lastCall = now - fn(...args) - } else if (!timeoutId) { - timeoutId = setTimeout(() => { - lastCall = Date.now() - timeoutId = null - fn(...args) - }, remaining) - } - }) as T & { cancel: () => void } - - throttled.cancel = () => { - if (timeoutId) { - clearTimeout(timeoutId) - timeoutId = null - } - } - - return throttled +/** codeFontScale 偏移 → 代码行高 (px)。基准 24px,每 1px 字号偏移对应 2px 行高增量 */ +function codeLineHeight(offset: number): number { + return 24 + offset * 2 } +const OVERSCAN = 5 // ============================================ // Types @@ -83,8 +43,18 @@ export interface DiffViewerProps { /** 不传则填满父容器 */ maxHeight?: number isResizing?: boolean - /** 自适应高度模式:内容少时自动撑开,多时限制高度 */ - autoHeight?: boolean + wordWrap?: boolean + data?: DiffViewerData + /** Stable key for preserving expanded collapsed-context regions across remounts. */ + stateKey?: string +} + +export interface DiffViewerData { + beforeTokens: HighlightTokens | null + afterTokens: HighlightTokens | null + pairedLines: PairedLine[] + unifiedLines: UnifiedLine[] + lineNumberWidth: number } export type LineType = 'add' | 'delete' | 'context' | 'empty' @@ -93,7 +63,15 @@ interface DiffLine { type: LineType content: string lineNo?: number - highlightedContent?: string + /** 词级别 diff 标记(结构化),与 syntax token 共存 */ + wordDiffSegments?: WordDiffSegment[] +} + +/** 词级别 diff 的单个片段 */ +interface WordDiffSegment { + text: string + /** 'add' | 'delete' 表示增删标记,undefined 表示无变化 */ + diffType?: 'add' | 'delete' } interface PairedLine { @@ -101,403 +79,1497 @@ interface PairedLine { right: DiffLine } +/** 折叠的 context 行占位 */ +interface CollapsedPairedLine { + collapsed: true + count: number + /** 在原始 lines 数组中的起始索引,用于展开 */ + id: number + isFirst: boolean + isLast: boolean + chunked: boolean +} + +type PairedLineOrCollapsed = PairedLine | CollapsedPairedLine + interface UnifiedLine extends DiffLine { oldLineNo?: number newLineNo?: number } +interface CollapsedUnifiedLine { + collapsed: true + count: number + id: number + isFirst: boolean + isLast: boolean + chunked: boolean +} + +type UnifiedLineOrCollapsed = UnifiedLine | CollapsedUnifiedLine + +function isCollapsed( + line: PairedLineOrCollapsed | UnifiedLineOrCollapsed, +): line is CollapsedPairedLine | CollapsedUnifiedLine { + return 'collapsed' in line && line.collapsed === true +} + +function expandRegion(prev: Map, id: number, direction: ExpandDirection): Map { + const next = new Map(prev) + const current = next.get(id) ?? { fromStart: 0, fromEnd: 0 } + next.set(id, { + fromStart: current.fromStart + (direction === 'up' || direction === 'both' ? EXPANSION_LINE_COUNT : 0), + fromEnd: current.fromEnd + (direction === 'down' || direction === 'both' ? EXPANSION_LINE_COUNT : 0), + }) + return next +} + +/** 上下文行保留数:变更前后各保留 CONTEXT_LINES 行 */ +const CONTEXT_LINES = 3 +const EXPANSION_LINE_COUNT = 100 +type ExpandDirection = 'up' | 'down' | 'both' +interface ExpansionRegion { + fromStart: number + fromEnd: number +} + +/** 将连续 context 行折叠,只保留变更前后各 CONTEXT_LINES 行 */ +function collapseContextPaired(lines: PairedLine[], expandedRegions?: ReadonlyMap): PairedLineOrCollapsed[] { + if (lines.length === 0) return [] + + const result: PairedLineOrCollapsed[] = [] + let contextStart = -1 + + for (let i = 0; i <= lines.length; i++) { + const isCtx = i < lines.length && lines[i].left.type === 'context' && lines[i].right.type === 'context' + + if (isCtx) { + if (contextStart === -1) contextStart = i + } else { + if (contextStart !== -1) { + const ctxLen = i - contextStart + const minToCollapse = CONTEXT_LINES * 2 + 2 + if (ctxLen > minToCollapse) { + const isFirst = contextStart === 0 + const isLast = i === lines.length + const keepBefore = isFirst ? 0 : CONTEXT_LINES + const keepAfter = isLast ? 0 : CONTEXT_LINES + const expanded = expandedRegions?.get(contextStart) ?? { fromStart: 0, fromEnd: 0 } + const prefixCount = Math.min(ctxLen, keepBefore + expanded.fromStart) + const suffixStart = Math.max(prefixCount, ctxLen - keepAfter - expanded.fromEnd) + + for (let j = contextStart; j < contextStart + prefixCount; j++) result.push(lines[j]) + if (suffixStart > prefixCount) { + const count = suffixStart - prefixCount + result.push({ + collapsed: true, + count, + id: contextStart, + isFirst, + isLast, + chunked: count > EXPANSION_LINE_COUNT, + }) + } + for (let j = contextStart + suffixStart; j < i; j++) result.push(lines[j]) + } else { + for (let j = contextStart; j < i; j++) result.push(lines[j]) + } + contextStart = -1 + } + if (i < lines.length) result.push(lines[i]) + } + } + + return result +} + +function collapseContextUnified(lines: UnifiedLine[], expandedRegions?: ReadonlyMap): UnifiedLineOrCollapsed[] { + if (lines.length === 0) return [] + + const result: UnifiedLineOrCollapsed[] = [] + let contextStart = -1 + + for (let i = 0; i <= lines.length; i++) { + const isCtx = i < lines.length && lines[i].type === 'context' + + if (isCtx) { + if (contextStart === -1) contextStart = i + } else { + if (contextStart !== -1) { + const ctxLen = i - contextStart + const minToCollapse = CONTEXT_LINES * 2 + 2 + if (ctxLen > minToCollapse) { + const isFirst = contextStart === 0 + const isLast = i === lines.length + const keepBefore = isFirst ? 0 : CONTEXT_LINES + const keepAfter = isLast ? 0 : CONTEXT_LINES + const expanded = expandedRegions?.get(contextStart) ?? { fromStart: 0, fromEnd: 0 } + const prefixCount = Math.min(ctxLen, keepBefore + expanded.fromStart) + const suffixStart = Math.max(prefixCount, ctxLen - keepAfter - expanded.fromEnd) + + for (let j = contextStart; j < contextStart + prefixCount; j++) result.push(lines[j]) + if (suffixStart > prefixCount) { + const count = suffixStart - prefixCount + result.push({ + collapsed: true, + count, + id: contextStart, + isFirst, + isLast, + chunked: count > EXPANSION_LINE_COUNT, + }) + } + for (let j = contextStart + suffixStart; j < i; j++) result.push(lines[j]) + } else { + for (let j = contextStart; j < i; j++) result.push(lines[j]) + } + contextStart = -1 + } + if (i < lines.length) result.push(lines[i]) + } + } + + return result +} + // ============================================ // Helpers // ============================================ function getLineBgClass(type: LineType): string { switch (type) { - case 'add': return 'bg-success-bg/40' - case 'delete': return 'bg-danger-bg/40' - case 'empty': return 'bg-bg-100/30' - default: return '' + case 'add': + return 'bg-success-bg/40' + case 'delete': + return 'bg-danger-bg/40' + case 'empty': + return 'bg-bg-100/30' + default: + return '' + } +} + +function getGutterBgClass(type: LineType): string { + switch (type) { + case 'add': + return 'bg-success-bg/40' + case 'delete': + return 'bg-danger-bg/40' + case 'empty': + return 'bg-bg-100/30' + default: + return '' + } +} + +function getContentBgClass(type: LineType): string { + if (type === 'empty') return 'diff-empty-content-buffer' + return getLineBgClass(type) +} + +function getEmptyBufferBackgroundStyle(yOffset: number, xOffset = 0): CSSProperties { + return { backgroundPosition: `${5 + xOffset}px ${-yOffset}px` } +} + +function getEmptyBufferRowStyle(height: number, yOffset = 0, xOffset = 0): CSSProperties { + return { height, ...getEmptyBufferBackgroundStyle(yOffset, xOffset) } +} + +function estimateWrappedVisualLineCount(content: string, availableWidth: number): number { + if (!content) return 1 + if (availableWidth <= 0) return 1 + + const charWidth = 8 + const charsPerLine = Math.max(1, Math.floor(availableWidth / charWidth)) + let visualLines = 0 + + for (const segment of content.split('\n')) { + visualLines += Math.max(1, Math.ceil(segment.length / charsPerLine)) + } + + return visualLines +} + +function getWrappedPairContent(pair: PairedLine): string { + return pair.left.content.length >= pair.right.content.length ? pair.left.content : pair.right.content +} + +function useDiffLineNumberWidth(before: string, after: string): number { + return useMemo( + () => getLineNumberColumnWidth(Math.max(getLineCount(before), getLineCount(after))), + [before, after], + ) +} + +function LineNumberCell({ lineNo, width, type }: { lineNo?: number; width: number; type?: LineType }) { + const toneClass = type === 'add' || type === 'delete' ? 'text-text-300' : 'text-text-400' + return ( +
+ {lineNo} +
+ ) +} + +function DiffMarkerCell({ type }: { type: LineType }) { + return ( +
+ {type === 'add' && +} + {type === 'delete' && } +
+ ) +} + +function EmptyContentBuffer({ height, yOffset = 0, xOffset = 0 }: { height: number; yOffset?: number; xOffset?: number }) { + return
+} + +/** Change bar 样式 — 行号左侧的 3px 竖条,add 实心 / delete 虚线 */ +function getChangeBarProps(type: LineType, yOffset = 0): { className: string; style?: React.CSSProperties } { + switch (type) { + case 'add': + return { className: 'w-1 shrink-0 bg-success-100' } + case 'delete': + return { + className: 'diff-change-bar-delete w-1 shrink-0', + style: { backgroundPositionY: `${-yOffset}px` }, + } + default: + return { className: 'w-1 shrink-0' } } } -function escapeHtml(str: string): string { - return str.replace(/&/g, '&').replace(//g, '>') +function alignDeleteChangeBars(container: HTMLElement, yOffset: number) { + for (const bar of container.querySelectorAll('.diff-change-bar-delete')) { + bar.style.backgroundPositionY = `${-yOffset}px` + } +} + +/** Pierre-like 折叠按钮,放在 gutter 区域 */ +function ExpandIcon({ type }: { type: ExpandDirection }) { + if (type === 'both') { + return ( + + ) + } + + return ( + + ) +} + +function getSeparatorDirections({ isFirst, isLast, chunked }: { isFirst?: boolean; isLast?: boolean; chunked?: boolean }): ExpandDirection[] { + if (!chunked) return [!isFirst && !isLast ? 'both' : isFirst ? 'down' : 'up'] + const directions: ExpandDirection[] = [] + if (!isFirst) directions.push('up') + if (!isLast) directions.push('down') + return directions +} + +function CollapsedExpandButton({ + directions, + onExpand, + width, +}: { + directions: ExpandDirection[] + onExpand?: (direction: ExpandDirection) => void + width?: number +}) { + const buttonWidth = width !== undefined && directions.length > 0 ? width / directions.length : undefined + + return ( +
+ {directions.map(direction => ( + + ))} +
+ ) +} + +/** 折叠文案区,放在 content 区域 */ +function CollapsedLabel({ + count, + t, + leadingDirections = [], + onExpand, + height = 24, +}: { + count: number + t: (key: string, opts?: Record) => string + leadingDirections?: ExpandDirection[] + onExpand?: (direction: ExpandDirection) => void + height?: number +}) { + return ( +
+ {leadingDirections.length > 0 && } +
+ +
+
+ ) +} + +function CollapsedLabelOverlay({ + count, + t, + onExpand, + height, + left, +}: { + count: number + t: (key: string, opts?: Record) => string + onExpand?: (direction: ExpandDirection) => void + height: number + left: number +}) { + return ( +
+ +
+ ) +} + +/** 右侧/续接区域,只显示同一条 separator 的延伸背景 */ +function CollapsedContinuation({ height = 24 }: { height?: number }) { + return
+} + +/** Wrapped 模式直接横跨整行 */ +function CollapsedBar({ + count, + t, + isFirst, + isLast, + chunked, + onExpand, + height = 24, + lineNumberAreaWidth, +}: { + count: number + t: (key: string, opts?: Record) => string + isFirst?: boolean + isLast?: boolean + chunked?: boolean + onExpand?: (direction: ExpandDirection) => void + height?: number + lineNumberAreaWidth?: number +}) { + const directions = getSeparatorDirections({ isFirst, isLast, chunked }) + return ( +
+ + +
+ ) } // ============================================ // Main Component // ============================================ +// eslint-disable-next-line react-refresh/only-export-components -- DiffViewer consumers share this data with fullscreen instances. +export function useDiffViewerData(before: string, after: string, language = 'text', isResizing = false, enabled = true): DiffViewerData { + const shouldHighlight = enabled && !isResizing && language !== 'text' + const { output: beforeTokens } = useSyntaxHighlight(before, { + lang: language, + mode: 'tokens', + enabled: shouldHighlight, + }) + const { output: afterTokens } = useSyntaxHighlight(after, { + lang: language, + mode: 'tokens', + enabled: shouldHighlight, + }) + const skipWordDiff = isResizing + const pairedLines = useMemo(() => (enabled ? computePairedLines(before, after, skipWordDiff) : []), [before, after, enabled, skipWordDiff]) + const unifiedLines = useMemo(() => (enabled ? computeUnifiedLines(before, after) : []), [before, after, enabled]) + const lineNumberWidth = useDiffLineNumberWidth(enabled ? before : '', enabled ? after : '') + + return useMemo( + () => ({ beforeTokens, afterTokens, pairedLines, unifiedLines, lineNumberWidth }), + [afterTokens, beforeTokens, lineNumberWidth, pairedLines, unifiedLines], + ) +} + export const DiffViewer = memo(function DiffViewer({ + data, + ...props +}: DiffViewerProps) { + if (data) return + return +}) + +function DiffViewerWithData({ before, after, language = 'text', isResizing = false, ...props }: DiffViewerProps) { + const data = useDiffViewerData(before, after, language, isResizing) + return +} + +const DiffViewerContent = memo(function DiffViewerContent({ before, after, - language = 'text', viewMode = 'split', maxHeight, isResizing = false, - autoHeight = false, -}: DiffViewerProps) { - // 检测大文件 - const totalLines = before.split('\n').length + after.split('\n').length - const isLargeFile = totalLines > LARGE_FILE_LINES || before.length + after.length > LARGE_FILE_CHARS - - // autoHeight 模式下,内容少时不用虚拟滚动 - const useVirtualScroll = !autoHeight || totalLines > AUTO_HEIGHT_THRESHOLD - - if (viewMode === 'split') { + wordWrap, + data, + stateKey, +}: DiffViewerProps & { data: DiffViewerData }) { + const { diffStyle, codeWordWrap, codeFontScale } = useSyncExternalStore(themeStore.subscribe, themeStore.getSnapshot) + const resolvedWordWrap = wordWrap ?? codeWordWrap + const lineHeight = codeLineHeight(codeFontScale) + const resolvedData = data + + // 纯增加或纯删除时,split 模式另一边是空的没意义,自动降级为 unified + const isAddOnly = !before.trim() + const isDeleteOnly = !after.trim() + const effectiveViewMode = isAddOnly || isDeleteOnly ? 'unified' : viewMode + + if (effectiveViewMode === 'split') { + if (resolvedWordWrap) { + return ( + + ) + } + return ( + ) + } + + if (resolvedWordWrap) { + return ( + ) } + return ( ) }) +const WrappedSplitDiffView = memo(function WrappedSplitDiffView({ + beforeTokens, + afterTokens, + pairedLines, + lineNumberWidth, + isResizing, + maxHeight, + diffStyle, + lineHeight, + stateKey, +}: { + beforeTokens: HighlightTokens | null + afterTokens: HighlightTokens | null + pairedLines: PairedLine[] + lineNumberWidth: number + isResizing: boolean + maxHeight?: number + diffStyle: DiffStyle + lineHeight: number + stateKey?: string +}) { + const { t } = useTranslation(['components', 'common']) + const [expandedRegions, setExpandedRegions] = useUiState>(stateKey, new Map()) + const displayLines = useMemo(() => collapseContextPaired(pairedLines, expandedRegions), [pairedLines, expandedRegions]) + const handleExpand = useCallback((id: number, direction: ExpandDirection) => { + setExpandedRegions(prev => expandRegion(prev, id, direction)) + }, [setExpandedRegions]) + + const useChangeBars = diffStyle === 'changeBars' + const gutterWidth = useChangeBars ? lineNumberWidth + 4 : lineNumberWidth + 20 + const estimateRowHeight = useCallback( + (index: number, containerWidth: number) => { + const item = displayLines[index] + if (!item || isCollapsed(item)) return lineHeight + + const panelWidth = Math.max(0, containerWidth / 2 - gutterWidth - 16) + return estimateWrappedVisualLineCount(getWrappedPairContent(item as PairedLine), panelWidth) * lineHeight + }, + [displayLines, gutterWidth, lineHeight], + ) + + const { containerRef, totalHeight, startIndex, endIndex, offsetY, handleScroll, measureRef } = + useDynamicVirtualScroll({ + lineCount: displayLines.length, + isResizing, + estimateLineHeight: lineHeight, + estimateHeight: estimateRowHeight, + }) + + const measureWrappedRowRef = useCallback( + (index: number, el: HTMLDivElement | null) => { + measureRef(index, el) + if (!el) return + + const rowTop = offsetY + el.offsetTop + for (const buffer of el.querySelectorAll('.diff-empty-content-buffer')) { + buffer.style.backgroundPosition = `5px ${-rowTop}px` + } + alignDeleteChangeBars(el, rowTop) + }, + [measureRef, offsetY], + ) + + if (pairedLines.length === 0) { + return ( +
+ {t('diffViewer.noChanges')} +
+ ) + } + + const visibleRows: React.ReactNode[] = [] + for (let i = startIndex; i < endIndex; i++) { + const item = displayLines[i] + + if (isCollapsed(item)) { + visibleRows.push( +
measureWrappedRowRef(i, el)}> + handleExpand(item.id, direction)} + lineNumberAreaWidth={lineNumberWidth} + height={lineHeight} + /> +
, + ) + continue + } + + const pair = item as PairedLine + const leftEmptyStyle = pair.left.type === 'empty' ? getEmptyBufferBackgroundStyle(0) : undefined + const rightEmptyStyle = pair.right.type === 'empty' ? getEmptyBufferBackgroundStyle(0) : undefined + visibleRows.push( +
measureWrappedRowRef(i, el)} className="flex items-stretch"> + {/* Left panel */} +
+
+ {useChangeBars ? ( +
+
+ +
+ ) : ( +
+ + +
+ )} +
+ +
+ {pair.left.type !== 'empty' && } +
+
+ + {/* Right panel */} +
+
+ {useChangeBars ? ( +
+
+ +
+ ) : ( +
+ + +
+ )} +
+ +
+ {pair.right.type !== 'empty' && } +
+
+
, + ) + } + + return ( +
+
+
+ {visibleRows} +
+
+
+ ) +}) + // ============================================ -// Split Diff View - 整体垂直滚动,左右各自水平滚动 +// Split Diff View - 两列架构 +// +// 结构: +// 外层容器 (overflow-y: auto) — 垂直滚动主控 +// flex 行 +// 左面板 (flex-1, flex row) +// 左 gutter (shrink-0, overflow: hidden) +// 左 content (flex-1, overflow-x: auto scrollbar-none) +// 分隔线 +// 右面板 (flex-1, flex row) +// 右 gutter (shrink-0, overflow: hidden) +// 右 content (flex-1, overflow-x: auto scrollbar-none) +// sticky proxy scrollbar 底部 // ============================================ -const SplitDiffView = memo(function SplitDiffView({ - before, - after, - language, +const SplitDiffView = memo(function SplitDiffView({ + beforeTokens, + afterTokens, + pairedLines, + lineNumberWidth, isResizing, - isLargeFile, maxHeight, - useVirtualScroll, -}: { - before: string - after: string - language: string + diffStyle, + lineHeight, + stateKey, +}: { + beforeTokens: HighlightTokens | null + afterTokens: HighlightTokens | null + pairedLines: PairedLine[] + lineNumberWidth: number isResizing: boolean - isLargeFile: boolean maxHeight?: number - useVirtualScroll: boolean + diffStyle: DiffStyle + lineHeight: number + stateKey?: string }) { + const { t } = useTranslation(['components', 'common']) const containerRef = useRef(null) - const leftPanelRef = useRef(null) - const rightPanelRef = useRef(null) + const leftContentRef = useRef(null) + const rightContentRef = useRef(null) const leftScrollbarRef = useRef(null) const rightScrollbarRef = useRef(null) + const leftScrollSourceRef = useRef<'content' | 'scrollbar' | null>(null) + const rightScrollSourceRef = useRef<'content' | 'scrollbar' | null>(null) + const maxLeftScrollWidthRef = useRef(0) + const maxRightScrollWidthRef = useRef(0) const [scrollTop, setScrollTop] = useState(0) const [containerHeight, setContainerHeight] = useState(300) const [leftContentWidth, setLeftContentWidth] = useState(0) const [rightContentWidth, setRightContentWidth] = useState(0) - - const cachedRef = useRef(null) - - const shouldHighlight = !isResizing && language !== 'text' - const { output: beforeTokens } = useSyntaxHighlight(before, { lang: language, mode: 'tokens', enabled: shouldHighlight }) - const { output: afterTokens } = useSyntaxHighlight(after, { lang: language, mode: 'tokens', enabled: shouldHighlight }) - - const skipWordDiff = isResizing || isLargeFile - const pairedLines = useMemo(() => { - if (isResizing && cachedRef.current) return cachedRef.current - const result = computePairedLines(before, after, skipWordDiff) - cachedRef.current = result - return result - }, [before, after, isResizing, skipWordDiff]) - - const totalHeight = pairedLines.length * LINE_HEIGHT - - // 可见范围 - 不用虚拟滚动时渲染全部 + const [leftClientWidth, setLeftClientWidth] = useState(0) + const [rightClientWidth, setRightClientWidth] = useState(0) + const [leftScrollLeft, setLeftScrollLeft] = useState(0) + const [rightScrollLeft, setRightScrollLeft] = useState(0) + + const [expandedRegions, setExpandedRegions] = useUiState>(stateKey, new Map()) + const displayLines = useMemo(() => collapseContextPaired(pairedLines, expandedRegions), [pairedLines, expandedRegions]) + const handleExpand = useCallback((id: number, direction: ExpandDirection) => { + setExpandedRegions(prev => expandRegion(prev, id, direction)) + }, [setExpandedRegions]) + + const totalHeight = displayLines.length * lineHeight + const { startIndex, endIndex, offsetY } = useMemo(() => { - if (!useVirtualScroll) { - return { startIndex: 0, endIndex: pairedLines.length, offsetY: 0 } - } - const start = Math.max(0, Math.floor(scrollTop / LINE_HEIGHT) - OVERSCAN) - const visibleCount = Math.ceil(containerHeight / LINE_HEIGHT) - const end = Math.min(pairedLines.length, start + visibleCount + OVERSCAN * 2) - return { startIndex: start, endIndex: end, offsetY: start * LINE_HEIGHT } - }, [scrollTop, containerHeight, pairedLines.length, useVirtualScroll]) - + const start = Math.max(0, Math.floor(scrollTop / lineHeight) - OVERSCAN) + const visibleCount = Math.ceil(containerHeight / lineHeight) + const end = Math.min(displayLines.length, start + visibleCount + OVERSCAN * 2) + return { startIndex: start, endIndex: end, offsetY: start * lineHeight } + }, [scrollTop, containerHeight, displayLines.length, lineHeight]) + // 监听容器大小 useEffect(() => { const container = containerRef.current if (!container || isResizing) return - + setContainerHeight(container.clientHeight) const resizeObserver = new ResizeObserver(() => setContainerHeight(container.clientHeight)) resizeObserver.observe(container) return () => resizeObserver.disconnect() }, [isResizing]) - - // 测量内容宽度 + + // 测量 content 宽度 — 追踪可见行 scrollWidth 历史最大值 useEffect(() => { - const leftPanel = leftPanelRef.current - const rightPanel = rightPanelRef.current - if (!leftPanel || !rightPanel) return - - const updateWidths = () => { - const leftContent = leftPanel.firstElementChild as HTMLElement - const rightContent = rightPanel.firstElementChild as HTMLElement - if (leftContent) setLeftContentWidth(leftContent.scrollWidth) - if (rightContent) setRightContentWidth(rightContent.scrollWidth) + const leftContent = leftContentRef.current + const rightContent = rightContentRef.current + if (!leftContent || !rightContent) return + const leftInner = leftContent.firstElementChild as HTMLElement + const rightInner = rightContent.firstElementChild as HTMLElement + + const measure = () => { + if (leftInner) { + const sw = leftInner.scrollWidth + if (sw > maxLeftScrollWidthRef.current) { + maxLeftScrollWidthRef.current = sw + leftInner.style.minWidth = `${sw}px` + } + setLeftContentWidth(maxLeftScrollWidthRef.current) + } + if (rightInner) { + const sw = rightInner.scrollWidth + if (sw > maxRightScrollWidthRef.current) { + maxRightScrollWidthRef.current = sw + rightInner.style.minWidth = `${sw}px` + } + setRightContentWidth(maxRightScrollWidthRef.current) + } + setLeftClientWidth(leftContent.clientWidth) + setRightClientWidth(rightContent.clientWidth) } - - updateWidths() - const observer = new MutationObserver(updateWidths) - observer.observe(leftPanel, { childList: true, subtree: true }) - observer.observe(rightPanel, { childList: true, subtree: true }) - return () => observer.disconnect() - }, [pairedLines, startIndex, endIndex]) - - // 使用 throttle 优化滚动性能 - const throttledSetScrollTop = useMemo( - () => throttle((value: number) => setScrollTop(value), SCROLL_THROTTLE_MS), - [] - ) - - // 清理 throttle - useEffect(() => { - return () => throttledSetScrollTop.cancel() - }, [throttledSetScrollTop]) - + + measure() + const ro = new ResizeObserver(() => { + maxLeftScrollWidthRef.current = 0 + maxRightScrollWidthRef.current = 0 + if (leftInner) leftInner.style.minWidth = '' + if (rightInner) rightInner.style.minWidth = '' + measure() + }) + ro.observe(leftContent) + ro.observe(rightContent) + const mo = new MutationObserver(measure) + mo.observe(leftContent, { childList: true, subtree: true }) + mo.observe(rightContent, { childList: true, subtree: true }) + return () => { + ro.disconnect() + mo.disconnect() + } + }, [displayLines, startIndex, endIndex]) + const handleScroll = useCallback((e: React.UIEvent) => { - throttledSetScrollTop(e.currentTarget.scrollTop) - }, [throttledSetScrollTop]) - - // 同步 proxy 滚动条 <-> 面板 + setScrollTop(e.currentTarget.scrollTop) + }, []) + + // 同步 proxy 滚动条 <-> content 面板(带 guard 防循环) const handleLeftScrollbar = useCallback((e: React.UIEvent) => { - if (leftPanelRef.current) leftPanelRef.current.scrollLeft = e.currentTarget.scrollLeft + if (leftScrollSourceRef.current === 'content') return + leftScrollSourceRef.current = 'scrollbar' + setLeftScrollLeft(e.currentTarget.scrollLeft) + if (leftContentRef.current) leftContentRef.current.scrollLeft = e.currentTarget.scrollLeft + requestAnimationFrame(() => { + leftScrollSourceRef.current = null + }) }, []) const handleRightScrollbar = useCallback((e: React.UIEvent) => { - if (rightPanelRef.current) rightPanelRef.current.scrollLeft = e.currentTarget.scrollLeft + if (rightScrollSourceRef.current === 'content') return + rightScrollSourceRef.current = 'scrollbar' + setRightScrollLeft(e.currentTarget.scrollLeft) + if (rightContentRef.current) rightContentRef.current.scrollLeft = e.currentTarget.scrollLeft + requestAnimationFrame(() => { + rightScrollSourceRef.current = null + }) }, []) - const handleLeftPanelScroll = useCallback((e: React.UIEvent) => { + const handleLeftContentScroll = useCallback((e: React.UIEvent) => { + if (leftScrollSourceRef.current === 'scrollbar') return + leftScrollSourceRef.current = 'content' + setLeftScrollLeft(e.currentTarget.scrollLeft) if (leftScrollbarRef.current) leftScrollbarRef.current.scrollLeft = e.currentTarget.scrollLeft + requestAnimationFrame(() => { + leftScrollSourceRef.current = null + }) }, []) - const handleRightPanelScroll = useCallback((e: React.UIEvent) => { + const handleRightContentScroll = useCallback((e: React.UIEvent) => { + if (rightScrollSourceRef.current === 'scrollbar') return + rightScrollSourceRef.current = 'content' + setRightScrollLeft(e.currentTarget.scrollLeft) if (rightScrollbarRef.current) rightScrollbarRef.current.scrollLeft = e.currentTarget.scrollLeft + requestAnimationFrame(() => { + rightScrollSourceRef.current = null + }) }, []) if (pairedLines.length === 0) { - return
No changes
+ return ( +
+ {t('diffViewer.noChanges')} +
+ ) } - - const leftRows: React.ReactNode[] = [] - const rightRows: React.ReactNode[] = [] - + + // 渲染可见行 — 分别生成 gutter 和 content + const useChangeBars = diffStyle === 'changeBars' + const gutterWidth = useChangeBars ? lineNumberWidth + 4 : lineNumberWidth + 20 + + const leftGutterRows: React.ReactNode[] = [] + const leftContentRows: React.ReactNode[] = [] + const rightGutterRows: React.ReactNode[] = [] + const rightContentRows: React.ReactNode[] = [] + for (let i = startIndex; i < endIndex; i++) { - const pair = pairedLines[i] - - leftRows.push( -
-
- {pair.left.lineNo} + const item = displayLines[i] + + if (isCollapsed(item)) { + const directions = getSeparatorDirections(item) + leftGutterRows.push( +
+ handleExpand(item.id, direction)} + width={lineNumberWidth} + /> + handleExpand(item.id, direction)} + height={lineHeight} + left={lineNumberWidth} + /> +
, + ) + leftContentRows.push( +
+ +
, + ) + rightGutterRows.push( +
, + ) + rightContentRows.push( +
+ +
, + ) + continue + } + + const pair = item as PairedLine + const leftGutterClass = pair.left.type === 'empty' ? 'diff-empty-content-buffer' : getGutterBgClass(pair.left.type) + const rightGutterClass = pair.right.type === 'empty' ? 'diff-empty-content-buffer' : getGutterBgClass(pair.right.type) + const rowTop = i * lineHeight + const leftGutterStyle = getEmptyBufferRowStyle(lineHeight, rowTop) + const rightGutterStyle = getEmptyBufferRowStyle(lineHeight, rowTop) + + // Left gutter + leftGutterRows.push( + useChangeBars ? ( +
+
+
-
- {pair.left.type === 'delete' && } - {pair.left.type !== 'empty' && } + ) : ( +
+ +
-
+ ), + ) + + // Left content: 代码 + leftContentRows.push( +
+ {pair.left.type === 'empty' ? : } +
, ) - - rightRows.push( -
-
- {pair.right.lineNo} + + // Right gutter + rightGutterRows.push( + useChangeBars ? ( +
+
+
-
- {pair.right.type === 'add' && +} - {pair.right.type !== 'empty' && } + ) : ( +
+ +
-
+ ), ) - } + // Right content + rightContentRows.push( +
+ {pair.right.type === 'empty' ? : } +
, + ) + } return ( -
- {/* 虚拟滚动时用占位 + 绝对定位,否则直接渲染 */} -
-
- {/* Left — 隐藏自身滚动条,由 proxy 控制 */} -
-
- {leftRows} + {/* 虚拟滚动高度占位 */} +
+
+ {/* 左面板 */} +
+ {/* 左 gutter */} +
+ {leftGutterRows} +
+ {/* 左 content — 隐藏自身滚动条,由 proxy 控制 */} +
+
{leftContentRows}
- {/* Right */} -
-
- {rightRows} + + {/* 右面板 */} +
+ {/* 右 gutter */} +
+ {rightGutterRows} +
+ {/* 右 content */} +
+
{rightContentRows}
- {/* Sticky proxy 横向滚动条 — 固定在可视区底部,和面板天然对齐 */} -
-
-
-
-
-
+ {/* Sticky proxy 横向滚动条 — 只在内容实际溢出时显示 */} + {(leftContentWidth > leftClientWidth || rightContentWidth > rightClientWidth) && ( +
+ {/* 左面板: gutter 占位 + scrollbar */} +
+
+
+
+
+
+ {/* 右面板: gutter 占位 + scrollbar */} +
+
+
+
+
+
-
+ )}
) }) // ============================================ -// Unified Diff View - 始终虚拟滚动 +// Unified Diff View — 和 SplitDiffView / CodePreview 一致的架构 +// +// 结构: +// 外层容器 (overflow-y: auto, overflow-x: hidden) — 垂直滚动唯一来源 +// 高度占位 (height: totalHeight, relative) — 虚拟滚动 +// absolute div (translateY: offsetY) — 可见行 +// flex row +// gutter (shrink-0, overflow: hidden): +// markers 模式: oldLineNo | newLineNo | +/- +// changeBars 模式: changeBar | oldLineNo | newLineNo +// content (flex-1, overflow-x: auto, scrollbar-none): 代码 +// inline-block min-w-full — 被最宽行撑开 +// sticky proxy scrollbar (bottom: 0) — 可见的横向滚动条 // ============================================ -const UnifiedDiffView = memo(function UnifiedDiffView({ - before, - after, - language, +const UnifiedDiffView = memo(function UnifiedDiffView({ + beforeTokens, + afterTokens, + lines, + lineNumberWidth, isResizing, maxHeight, - useVirtualScroll, -}: { - before: string - after: string - language: string + diffStyle, + lineHeight, + stateKey, +}: { + beforeTokens: HighlightTokens | null + afterTokens: HighlightTokens | null + lines: UnifiedLine[] + lineNumberWidth: number isResizing: boolean maxHeight?: number - useVirtualScroll: boolean + diffStyle: DiffStyle + lineHeight: number + stateKey?: string }) { + const { t } = useTranslation(['components', 'common']) const containerRef = useRef(null) + const contentRef = useRef(null) + const scrollbarRef = useRef(null) + const scrollSourceRef = useRef<'content' | 'scrollbar' | null>(null) + const maxScrollWidthRef = useRef(0) const [scrollTop, setScrollTop] = useState(0) const [containerHeight, setContainerHeight] = useState(300) - - const cachedRef = useRef(null) - - // resize时禁用高亮(大文件仍然高亮,因为useSyntaxHighlight是异步的不会阻塞) - const shouldHighlight = !isResizing && language !== 'text' - const { output: beforeTokens } = useSyntaxHighlight(before, { lang: language, mode: 'tokens', enabled: shouldHighlight }) - const { output: afterTokens } = useSyntaxHighlight(after, { lang: language, mode: 'tokens', enabled: shouldHighlight }) - - const lines = useMemo(() => { - if (isResizing && cachedRef.current) return cachedRef.current - const result = computeUnifiedLines(before, after) - cachedRef.current = result - return result - }, [before, after, isResizing]) - - const totalHeight = lines.length * LINE_HEIGHT - + const [contentWidth, setContentWidth] = useState(0) + const [contentClientWidth, setContentClientWidth] = useState(0) + + const [expandedRegions, setExpandedRegions] = useUiState>(stateKey, new Map()) + const displayLines = useMemo(() => collapseContextUnified(lines, expandedRegions), [lines, expandedRegions]) + const handleExpand = useCallback((id: number, direction: ExpandDirection) => { + setExpandedRegions(prev => expandRegion(prev, id, direction)) + }, [setExpandedRegions]) + + const totalHeight = displayLines.length * lineHeight + const { startIndex, endIndex, offsetY } = useMemo(() => { - if (!useVirtualScroll) { - return { startIndex: 0, endIndex: lines.length, offsetY: 0 } - } - const start = Math.max(0, Math.floor(scrollTop / LINE_HEIGHT) - OVERSCAN) - const visibleCount = Math.ceil(containerHeight / LINE_HEIGHT) - const end = Math.min(lines.length, start + visibleCount + OVERSCAN * 2) - return { startIndex: start, endIndex: end, offsetY: start * LINE_HEIGHT } - }, [scrollTop, containerHeight, lines.length, useVirtualScroll]) - + const start = Math.max(0, Math.floor(scrollTop / lineHeight) - OVERSCAN) + const visibleCount = Math.ceil(containerHeight / lineHeight) + const end = Math.min(displayLines.length, start + visibleCount + OVERSCAN * 2) + return { startIndex: start, endIndex: end, offsetY: start * lineHeight } + }, [scrollTop, containerHeight, displayLines.length, lineHeight]) + useEffect(() => { const container = containerRef.current if (!container || isResizing) return - + setContainerHeight(container.clientHeight) const resizeObserver = new ResizeObserver(() => setContainerHeight(container.clientHeight)) resizeObserver.observe(container) return () => resizeObserver.disconnect() }, [isResizing]) - - // 使用 throttle 优化滚动性能 - const throttledSetScrollTop = useMemo( - () => throttle((value: number) => setScrollTop(value), SCROLL_THROTTLE_MS), - [] - ) - - // 清理 throttle + + // 测量 content 宽度 — 追踪可见行 scrollWidth 历史最大值 useEffect(() => { - return () => throttledSetScrollTop.cancel() - }, [throttledSetScrollTop]) - + const content = contentRef.current + if (!content) return + const inner = content.firstElementChild as HTMLElement + + const measure = () => { + if (inner) { + const sw = inner.scrollWidth + if (sw > maxScrollWidthRef.current) { + maxScrollWidthRef.current = sw + inner.style.minWidth = `${sw}px` + } + setContentWidth(maxScrollWidthRef.current) + } + setContentClientWidth(content.clientWidth) + } + + measure() + const ro = new ResizeObserver(() => { + maxScrollWidthRef.current = 0 + if (inner) inner.style.minWidth = '' + measure() + }) + ro.observe(content) + const mo = new MutationObserver(measure) + mo.observe(content, { childList: true, subtree: true }) + return () => { + ro.disconnect() + mo.disconnect() + } + }, [startIndex, endIndex]) + const handleScroll = useCallback((e: React.UIEvent) => { - throttledSetScrollTop(e.currentTarget.scrollTop) - }, [throttledSetScrollTop]) + setScrollTop(e.currentTarget.scrollTop) + }, []) + + // proxy scrollbar ↔ content 面板水平同步(带 guard 防循环) + const handleScrollbar = useCallback((e: React.UIEvent) => { + if (scrollSourceRef.current === 'content') return + scrollSourceRef.current = 'scrollbar' + if (contentRef.current) contentRef.current.scrollLeft = e.currentTarget.scrollLeft + requestAnimationFrame(() => { + scrollSourceRef.current = null + }) + }, []) + const handleContentScroll = useCallback((e: React.UIEvent) => { + if (scrollSourceRef.current === 'scrollbar') return + scrollSourceRef.current = 'content' + if (scrollbarRef.current) scrollbarRef.current.scrollLeft = e.currentTarget.scrollLeft + requestAnimationFrame(() => { + scrollSourceRef.current = null + }) + }, []) + + if (lines.length === 0) { + return ( +
+ {t('diffViewer.noChanges')} +
+ ) + } + + const useChangeBars = diffStyle === 'changeBars' + const GUTTER_WIDTH = useChangeBars ? lineNumberWidth * 2 + 4 : lineNumberWidth * 2 + 20 + + const gutterRows: React.ReactNode[] = [] + const contentRows: React.ReactNode[] = [] + + for (let i = startIndex; i < endIndex; i++) { + const item = displayLines[i] + + if (isCollapsed(item)) { + const directions = getSeparatorDirections(item) + gutterRows.push( +
+ handleExpand(item.id, direction)} + width={lineNumberWidth * 2} + /> + handleExpand(item.id, direction)} + height={lineHeight} + left={lineNumberWidth * 2} + /> +
, + ) + contentRows.push( +
+ +
, + ) + continue + } + + const line = item as UnifiedLine + let tokens: HighlightTokens | null = null + let lineNo: number | undefined + if (line.type === 'delete' && line.oldLineNo) { + tokens = beforeTokens + lineNo = line.oldLineNo + } else if ((line.type === 'add' || line.type === 'context') && line.newLineNo) { + tokens = afterTokens + lineNo = line.newLineNo + } + + // Gutter 行 + gutterRows.push( + useChangeBars ? ( +
+
+ + +
+ ) : ( +
+ + + +
+ ), + ) + + // Content 行 + contentRows.push( +
+ +
, + ) + } + + return ( +
+ {/* 虚拟滚动高度占位 */} +
+
+ {/* Gutter: 固定宽度,不水平滚动 */} +
+ {gutterRows} +
+ + {/* Content: 独立水平滚动,隐藏自身滚动条 */} +
+
{contentRows}
+
+
+
+ + {/* Sticky proxy 横向滚动条 — 只在内容实际溢出时显示 */} + {contentWidth > contentClientWidth && ( +
+
+
+
+
+
+ )} +
+ ) +}) + +const WrappedUnifiedDiffView = memo(function WrappedUnifiedDiffView({ + beforeTokens, + afterTokens, + lines, + lineNumberWidth, + isResizing, + maxHeight, + diffStyle, + lineHeight, + stateKey, +}: { + beforeTokens: HighlightTokens | null + afterTokens: HighlightTokens | null + lines: UnifiedLine[] + lineNumberWidth: number + isResizing: boolean + maxHeight?: number + diffStyle: DiffStyle + lineHeight: number + stateKey?: string +}) { + const { t } = useTranslation(['components', 'common']) + const [expandedRegions, setExpandedRegions] = useUiState>(stateKey, new Map()) + const displayLines = useMemo(() => collapseContextUnified(lines, expandedRegions), [lines, expandedRegions]) + const handleExpand = useCallback((id: number, direction: ExpandDirection) => { + setExpandedRegions(prev => expandRegion(prev, id, direction)) + }, [setExpandedRegions]) + + const useChangeBars = diffStyle === 'changeBars' + const gutterWidth = useChangeBars ? lineNumberWidth * 2 + 4 : lineNumberWidth * 2 + 20 + const estimateRowHeight = useCallback( + (index: number, containerWidth: number) => { + const item = displayLines[index] + if (!item || isCollapsed(item)) return lineHeight + + const availableWidth = Math.max(0, containerWidth - gutterWidth - 16) + return estimateWrappedVisualLineCount((item as UnifiedLine).content, availableWidth) * lineHeight + }, + [displayLines, gutterWidth, lineHeight], + ) + + const { containerRef, totalHeight, startIndex, endIndex, offsetY, handleScroll, measureRef } = + useDynamicVirtualScroll({ + lineCount: displayLines.length, + isResizing, + estimateLineHeight: lineHeight, + estimateHeight: estimateRowHeight, + }) + + const measureWrappedUnifiedRowRef = useCallback( + (index: number, el: HTMLDivElement | null) => { + measureRef(index, el) + if (!el) return + + alignDeleteChangeBars(el, offsetY + el.offsetTop) + }, + [measureRef, offsetY], + ) if (lines.length === 0) { - return
No changes
+ return ( +
+ {t('diffViewer.noChanges')} +
+ ) } - + const visibleRows: React.ReactNode[] = [] for (let i = startIndex; i < endIndex; i++) { - const line = lines[i] - let tokens: any[][] | null = null + const item = displayLines[i] + + if (isCollapsed(item)) { + visibleRows.push( +
measureWrappedUnifiedRowRef(i, el)}> + handleExpand(item.id, direction)} + lineNumberAreaWidth={lineNumberWidth * 2} + height={lineHeight} + /> +
, + ) + continue + } + + const line = item as UnifiedLine + let tokens: HighlightTokens | null = null let lineNo: number | undefined + if (line.type === 'delete' && line.oldLineNo) { - tokens = beforeTokens as any[][] | null + tokens = beforeTokens lineNo = line.oldLineNo } else if ((line.type === 'add' || line.type === 'context') && line.newLineNo) { - tokens = afterTokens as any[][] | null + tokens = afterTokens lineNo = line.newLineNo } - + visibleRows.push( -
-
- {line.oldLineNo} -
-
- {line.newLineNo} +
measureWrappedUnifiedRowRef(i, el)} className={`flex items-stretch ${getLineBgClass(line.type)}`}> +
+ {useChangeBars ? ( +
+
+ + +
+ ) : ( +
+ + + +
+ )}
-
- {line.type === 'add' && +} - {line.type === 'delete' && } + +
-
+
, ) } return ( -
-
-
+
+
{visibleRows}
@@ -509,28 +1581,110 @@ const UnifiedDiffView = memo(function UnifiedDiffView({ // Line Content Renderer // ============================================ -const LineContent = memo(function LineContent({ - line, - tokens -}: { - line: DiffLine - tokens: any[][] | null -}) { - // 词级别diff高亮 - if (line.highlightedContent) { - return +type HighlightToken = HighlightTokens[number][number] +type WordDiffChange = ReturnType[number] + +const LineContent = memo(function LineContent({ line, tokens }: { line: DiffLine; tokens: HighlightTokens | null }) { + const lineTokens = tokens && line.lineNo ? tokens[line.lineNo - 1] : null + + // 有 word diff 标记时:在语法着色基础上叠加增删背景 + if (line.wordDiffSegments) { + // 有语法 token → 合并渲染(token 提供颜色,word diff segment 提供背景) + if (lineTokens) { + return + } + // 无语法 token → 纯 word diff(文字用默认色 + 增删背景) + return ( + <> + {line.wordDiffSegments.map((seg, i) => + seg.diffType ? ( + + {seg.text} + + ) : ( + + {seg.text} + + ), + )} + + ) } - - // 语法高亮 - if (tokens && line.lineNo && tokens[line.lineNo - 1]) { - const lineTokens = tokens[line.lineNo - 1] - return <>{lineTokens.map((token: any, i: number) => {token.content})} + + // 无 word diff,有语法高亮 + if (lineTokens) { + return ( + <> + {lineTokens.map((token: HighlightToken, i: number) => ( + + {token.content} + + ))} + + ) } - + // 纯文本 return {line.content} }) +/** + * 合并渲染:syntax token 提供文字颜色,word diff segment 提供背景色。 + * + * 两者的切分边界不同,需要对齐遍历—— + * 遍历 word diff segment,在每个 segment 内部按 syntax token 切分渲染。 + */ +function MergedWordDiffLine({ segments, lineTokens }: { segments: WordDiffSegment[]; lineTokens: HighlightToken[] }) { + const result: React.ReactNode[] = [] + let tokenIdx = 0 + let tokenOffset = 0 // 当前 token 内已消费的字符数 + + for (let si = 0; si < segments.length; si++) { + const seg = segments[si] + let remaining = seg.text.length + const bgClass = seg.diffType === 'delete' ? 'bg-danger-100/30' : seg.diffType === 'add' ? 'bg-success-100/30' : '' + const children: React.ReactNode[] = [] + + while (remaining > 0 && tokenIdx < lineTokens.length) { + const token = lineTokens[tokenIdx] + const available = token.content.length - tokenOffset + const take = Math.min(remaining, available) + const slice = token.content.substring(tokenOffset, tokenOffset + take) + + children.push( + + {slice} + , + ) + + remaining -= take + tokenOffset += take + if (tokenOffset >= token.content.length) { + tokenIdx++ + tokenOffset = 0 + } + } + + // 如果 token 用完了但 segment 还有剩余(不应该发生,但防御性处理) + if (remaining > 0) { + const extraStart = seg.text.length - remaining + children.push({seg.text.substring(extraStart)}) + } + + if (bgClass) { + result.push( + + {children} + , + ) + } else { + result.push(...children) + } + } + + return <>{result} +} + // ============================================ // Diff Computation // ============================================ @@ -540,50 +1694,54 @@ function computePairedLines(before: string, after: string, skipWordDiff: boolean const result: PairedLine[] = [] const beforeLines = before.split('\n') const afterLines = after.split('\n') - - let oldIdx = 0, newIdx = 0, i = 0 - + + let oldIdx = 0, + newIdx = 0, + i = 0 + while (i < changes.length) { const change = changes[i] const count = change.count || 0 - + if (change.removed) { const next = changes[i + 1] if (next?.added) { const addCount = next.count || 0 const maxCount = Math.max(count, addCount) - + for (let j = 0; j < maxCount; j++) { const oldLine = j < count ? beforeLines[oldIdx + j] : undefined const newLine = j < addCount ? afterLines[newIdx + j] : undefined - - let leftHighlight: string | undefined - let rightHighlight: string | undefined - + + let leftSegments: WordDiffSegment[] | undefined + let rightSegments: WordDiffSegment[] | undefined + if (!skipWordDiff && oldLine !== undefined && newLine !== undefined) { const wordDiff = computeWordDiff(oldLine, newLine) if (!isTooFragmented(wordDiff.changes)) { - leftHighlight = wordDiff.left - rightHighlight = wordDiff.right + leftSegments = wordDiff.left + rightSegments = wordDiff.right } } - + result.push({ - left: oldLine !== undefined - ? { type: 'delete', content: oldLine, lineNo: oldIdx + j + 1, highlightedContent: leftHighlight } - : { type: 'empty', content: '' }, - right: newLine !== undefined - ? { type: 'add', content: newLine, lineNo: newIdx + j + 1, highlightedContent: rightHighlight } - : { type: 'empty', content: '' }, + left: + oldLine !== undefined + ? { type: 'delete', content: oldLine, lineNo: oldIdx + j + 1, wordDiffSegments: leftSegments } + : { type: 'empty', content: '' }, + right: + newLine !== undefined + ? { type: 'add', content: newLine, lineNo: newIdx + j + 1, wordDiffSegments: rightSegments } + : { type: 'empty', content: '' }, }) } - + oldIdx += count newIdx += addCount i += 2 continue } - + for (let j = 0; j < count; j++) { result.push({ left: { type: 'delete', content: beforeLines[oldIdx + j] || '', lineNo: oldIdx + j + 1 }, @@ -611,7 +1769,7 @@ function computePairedLines(before: string, after: string, skipWordDiff: boolean } i++ } - + return result } @@ -620,12 +1778,13 @@ function computeUnifiedLines(before: string, after: string): UnifiedLine[] { const result: UnifiedLine[] = [] const beforeLines = before.split('\n') const afterLines = after.split('\n') - - let oldIdx = 0, newIdx = 0 - + + let oldIdx = 0, + newIdx = 0 + for (const change of changes) { const count = change.count || 0 - + if (change.removed) { for (let j = 0; j < count; j++) { result.push({ type: 'delete', content: beforeLines[oldIdx + j] || '', oldLineNo: oldIdx + j + 1 }) @@ -638,18 +1797,24 @@ function computeUnifiedLines(before: string, after: string): UnifiedLine[] { newIdx += count } else { for (let j = 0; j < count; j++) { - result.push({ type: 'context', content: afterLines[newIdx + j] || '', oldLineNo: oldIdx + j + 1, newLineNo: newIdx + j + 1 }) + result.push({ + type: 'context', + content: afterLines[newIdx + j] || '', + oldLineNo: oldIdx + j + 1, + newLineNo: newIdx + j + 1, + }) } oldIdx += count newIdx += count } } - + return result } -function isTooFragmented(changes: any[]): boolean { - let commonLength = 0, totalLength = 0 +function isTooFragmented(changes: WordDiffChange[]): boolean { + let commonLength = 0, + totalLength = 0 for (const change of changes) { totalLength += change.value.length if (!change.added && !change.removed) commonLength += change.value.length @@ -657,14 +1822,17 @@ function isTooFragmented(changes: any[]): boolean { return totalLength > 10 && commonLength / totalLength < 0.4 } -function computeWordDiff(oldLine: string, newLine: string): { left: string; right: string; changes: any[] } { - const changes = diffWords(oldLine, newLine) - - const mergedChanges: any[] = [] +function computeWordDiff( + oldLine: string, + newLine: string, +): { left: WordDiffSegment[]; right: WordDiffSegment[]; changes: WordDiffChange[] } { + const changes = diffWordsWithSpace(oldLine, newLine) + + const mergedChanges: WordDiffChange[] = [] for (let i = 0; i < changes.length; i++) { const current = changes[i] const prev = mergedChanges[mergedChanges.length - 1] - + if (prev && !current.added && !current.removed && /^\s*$/.test(current.value)) { const next = changes[i + 1] if ((prev.removed && next?.removed) || (prev.added && next?.added)) { @@ -672,7 +1840,7 @@ function computeWordDiff(oldLine: string, newLine: string): { left: string; righ continue } } - + if (prev && ((prev.added && current.added) || (prev.removed && current.removed))) { prev.value += current.value } else { @@ -680,29 +1848,16 @@ function computeWordDiff(oldLine: string, newLine: string): { left: string; righ } } - let left = '', right = '' + const left: WordDiffSegment[] = [] + const right: WordDiffSegment[] = [] for (const change of mergedChanges) { - const escaped = escapeHtml(change.value) - if (change.removed) left += `${escaped}` - else if (change.added) right += `${escaped}` - else { left += escaped; right += escaped } + if (change.removed) left.push({ text: change.value, diffType: 'delete' }) + else if (change.added) right.push({ text: change.value, diffType: 'add' }) + else { + left.push({ text: change.value }) + right.push({ text: change.value }) + } } - - return { left, right, changes: mergedChanges } -} - -// ============================================ -// Export helper -// ============================================ -export function extractContentFromUnifiedDiff(diff: string): { before: string, after: string } { - let before = '', after = '' - for (const line of diff.split('\n')) { - if (line.startsWith('---') || line.startsWith('+++') || line.startsWith('Index:') || - line.startsWith('===') || line.startsWith('@@') || line.startsWith('\\ No newline')) continue - if (line.startsWith('-')) before += line.slice(1) + '\n' - else if (line.startsWith('+')) after += line.slice(1) + '\n' - else if (line.startsWith(' ')) { before += line.slice(1) + '\n'; after += line.slice(1) + '\n' } - } - return { before: before.trimEnd(), after: after.trimEnd() } + return { left, right, changes: mergedChanges } } diff --git a/src/components/ErrorBoundary.tsx b/src/components/ErrorBoundary.tsx new file mode 100644 index 00000000..f015a53c --- /dev/null +++ b/src/components/ErrorBoundary.tsx @@ -0,0 +1,65 @@ +import { Component, type ErrorInfo, type ReactNode } from 'react' +import { globalErrorHandler } from '../utils/errorHandling' + +interface ErrorBoundaryProps { + children: ReactNode + onOpenSettings?: () => void +} + +interface ErrorBoundaryState { + error: Error | null +} + +export class ErrorBoundary extends Component { + state: ErrorBoundaryState = { error: null } + + static getDerivedStateFromError(error: Error): ErrorBoundaryState { + return { error } + } + + componentDidCatch(error: Error, info: ErrorInfo) { + globalErrorHandler('render error', error, false, info.componentStack) + } + + render() { + if (!this.state.error) return this.props.children + + return ( +
+
+
+
OpenCode UI ran into a problem
+
+ {this.state.error.message || 'The chat view could not render this response.'} +
+
+
+ {this.props.onOpenSettings && ( + + )} + + +
+
+
+ ) + } +} diff --git a/src/components/FileExplorer.tsx b/src/components/FileExplorer.tsx index 7a75c0a5..8de18d29 100644 --- a/src/components/FileExplorer.tsx +++ b/src/components/FileExplorer.tsx @@ -4,63 +4,89 @@ // 性能优化:使用 CSS 变量 + requestAnimationFrame 处理 resize // ============================================ -import { memo, useCallback, useMemo, useEffect, useRef, useState, useLayoutEffect } from 'react' +import { memo, useCallback, useMemo, useEffect, useRef, useState, type PointerEvent, type ReactNode } from 'react' +import { useTranslation } from 'react-i18next' import { useFileExplorer, type FileTreeNode } from '../hooks' +import { useVerticalSplitResize } from '../hooks/useVerticalSplitResize' import { layoutStore, type PreviewFile } from '../store/layoutStore' -import { - FileIcon, - FolderIcon, - FolderOpenIcon, - ChevronRightIcon, - ChevronDownIcon, - RetryIcon, - CloseIcon, - AlertCircleIcon, -} from './Icons' +import { ChevronRightIcon, ChevronDownIcon, RetryIcon, AlertCircleIcon, DownloadIcon, MaximizeIcon } from './Icons' +import { CodePreview } from './CodePreview' +import { PreviewTabsBar, type PreviewTabsBarItem } from './PreviewTabsBar' +import { MarkdownRenderer } from './MarkdownRenderer' +import { useFullscreenLayer } from '../contexts' +import { getMaterialIconUrl } from '../utils/materialIcons' import { detectLanguage } from '../utils/languageUtils' -import { useSyntaxHighlight } from '../hooks/useSyntaxHighlight' +import { + getPreviewCategory, + isBinaryContent, + isTextualMedia, + buildDataUrl, + buildTextDataUrl, + decodeBase64Text, + formatMimeType, + type PreviewCategory, +} from '../utils/mimeUtils' +import { downloadFileContent } from '../utils/downloadUtils' import type { FileContent } from '../api/types' +import { startInternalDrag } from '../lib/internalDragCore' // 常量 const MIN_TREE_HEIGHT = 100 const MIN_PREVIEW_HEIGHT = 150 -const LINE_HEIGHT = 20 // 每行高度(用于虚拟滚动) -const OVERSCAN = 5 // 额外渲染的行数 -const MAX_LINE_LENGTH = 5000 // 超过此长度截断显示,避免 DOM 过大 + +const MARKDOWN_MIME_TYPES = new Set(['text/markdown', 'text/x-markdown', 'text/md', 'application/markdown']) + +function isMarkdownPreview(language: string, mimeType?: string): boolean { + if (language === 'markdown' || language === 'mdx') return true + if (!mimeType) return false + return MARKDOWN_MIME_TYPES.has(mimeType.split(';', 1)[0].toLowerCase()) +} interface FileExplorerProps { + panelTabId: string directory?: string previewFile: PreviewFile | null + previewFiles: PreviewFile[] position?: 'bottom' | 'right' isPanelResizing?: boolean sessionId?: string | null } -export const FileExplorer = memo(function FileExplorer({ - directory, +export const FileExplorer = memo(function FileExplorer({ + panelTabId, + directory, previewFile, + previewFiles, position = 'right', isPanelResizing = false, sessionId, }: FileExplorerProps) { + const { t } = useTranslation(['components', 'common']) const containerRef = useRef(null) const treeRef = useRef(null) - const [treeHeight, setTreeHeight] = useState(null) // null 表示自动 - const [isResizing, setIsResizing] = useState(false) - const rafRef = useRef(0) - const currentHeightRef = useRef(null) - + const { + splitHeight: treeHeight, + isResizing, + resetSplitHeight, + handleResizeStart, + handleTouchResizeStart, + } = useVerticalSplitResize({ + containerRef, + primaryRef: treeRef, + cssVariableName: '--tree-height', + minPrimaryHeight: MIN_TREE_HEIGHT, + minSecondaryHeight: MIN_PREVIEW_HEIGHT, + }) + // 综合 resize 状态 - 外部面板 resize 或内部 resize const isAnyResizing = isPanelResizing || isResizing - + const { tree, isLoading, error, expandedPaths, toggleExpand, - selectedPath, - selectFile, previewContent, previewLoading, previewError, @@ -70,149 +96,79 @@ export const FileExplorer = memo(function FileExplorer({ refresh, } = useFileExplorer({ directory, autoLoad: true, sessionId: sessionId || undefined }) - // 同步高度到 CSS 变量 - useLayoutEffect(() => { - if (!isResizing && treeRef.current && treeHeight !== null) { - treeRef.current.style.setProperty('--tree-height', `${treeHeight}px`) - currentHeightRef.current = treeHeight - } - }, [treeHeight, isResizing]) - // 当 previewFile 改变时加载预览 useEffect(() => { if (previewFile) { - selectFile(previewFile.path) loadPreview(previewFile.path) - } - }, [previewFile, selectFile, loadPreview]) - - // 处理文件点击 - const handleFileClick = useCallback((node: FileTreeNode) => { - if (node.type === 'directory') { - toggleExpand(node.path) } else { - selectFile(node.path) - loadPreview(node.path) - layoutStore.openFilePreview({ path: node.path, name: node.name }, position) + clearPreview() } - }, [toggleExpand, selectFile, loadPreview, position]) + }, [previewFile, loadPreview, clearPreview]) - // 关闭预览 - const handleClosePreview = useCallback(() => { - clearPreview() - layoutStore.closeFilePreview() - setTreeHeight(null) - currentHeightRef.current = null - }, [clearPreview]) - - // 拖拽调整高度 - 使用 CSS 变量 + requestAnimationFrame 优化 - const handleResizeStart = useCallback((e: React.MouseEvent) => { - e.preventDefault() - - const container = containerRef.current - const treeEl = treeRef.current - if (!container || !treeEl) return - - setIsResizing(true) - - const containerRect = container.getBoundingClientRect() - const startY = e.clientY - const startHeight = currentHeightRef.current ?? containerRect.height * 0.4 - - const handleMouseMove = (moveEvent: MouseEvent) => { - if (rafRef.current) { - cancelAnimationFrame(rafRef.current) - } - - rafRef.current = requestAnimationFrame(() => { - const deltaY = moveEvent.clientY - startY - const newHeight = startHeight + deltaY - const maxHeight = containerRect.height - MIN_PREVIEW_HEIGHT - const clampedHeight = Math.min(Math.max(newHeight, MIN_TREE_HEIGHT), maxHeight) - // 直接修改 CSS 变量 - treeEl.style.setProperty('--tree-height', `${clampedHeight}px`) - currentHeightRef.current = clampedHeight - }) - } - - const handleMouseUp = () => { - if (rafRef.current) { - cancelAnimationFrame(rafRef.current) - } - - setIsResizing(false) - document.body.style.cursor = '' - document.body.style.userSelect = '' - document.removeEventListener('mousemove', handleMouseMove) - document.removeEventListener('mouseup', handleMouseUp) - - // 更新 state 以持久化 - if (currentHeightRef.current !== null) { - setTreeHeight(currentHeightRef.current) - } + const handleRefresh = useCallback(async () => { + await refresh() + if (previewFile) { + await loadPreview(previewFile.path) } - - document.body.style.cursor = 'row-resize' - document.body.style.userSelect = 'none' - document.addEventListener('mousemove', handleMouseMove) - document.addEventListener('mouseup', handleMouseUp) - }, []) + }, [loadPreview, previewFile, refresh]) - // 触摸拖拽调整高度 - const handleTouchResizeStart = useCallback((e: React.TouchEvent) => { - const container = containerRef.current - const treeEl = treeRef.current - if (!container || !treeEl) return - - setIsResizing(true) - - const containerRect = container.getBoundingClientRect() - const startY = e.touches[0].clientY - const startHeight = currentHeightRef.current ?? containerRect.height * 0.4 - - const handleTouchMove = (moveEvent: TouchEvent) => { - moveEvent.preventDefault() - if (rafRef.current) { - cancelAnimationFrame(rafRef.current) + // 处理文件点击 + const handleFileClick = useCallback( + (node: FileTreeNode) => { + if (node.type === 'directory') { + toggleExpand(node.path) + } else { + layoutStore.openFilePreview({ path: node.path, name: node.name }, position) } + }, + [toggleExpand, position], + ) - rafRef.current = requestAnimationFrame(() => { - const deltaY = moveEvent.touches[0].clientY - startY - const newHeight = startHeight + deltaY - const maxHeight = containerRect.height - MIN_PREVIEW_HEIGHT - const clampedHeight = Math.min(Math.max(newHeight, MIN_TREE_HEIGHT), maxHeight) - treeEl.style.setProperty('--tree-height', `${clampedHeight}px`) - currentHeightRef.current = clampedHeight - }) - } - - const handleTouchEnd = () => { - if (rafRef.current) { - cancelAnimationFrame(rafRef.current) - } + // 关闭预览 + const handleClosePreview = useCallback(() => { + layoutStore.closeAllFilePreviews(panelTabId) + resetSplitHeight() + }, [panelTabId, resetSplitHeight]) - setIsResizing(false) - document.removeEventListener('touchmove', handleTouchMove) - document.removeEventListener('touchend', handleTouchEnd) + const handleActivatePreview = useCallback( + (path: string) => { + layoutStore.activateFilePreview(panelTabId, path) + }, + [panelTabId], + ) - if (currentHeightRef.current !== null) { - setTreeHeight(currentHeightRef.current) - } - } + const handleClosePreviewTab = useCallback( + (path: string) => { + layoutStore.closeFilePreview(panelTabId, path) + }, + [panelTabId], + ) - document.addEventListener('touchmove', handleTouchMove, { passive: false }) - document.addEventListener('touchend', handleTouchEnd) - }, []) + const handleReorderPreviewTabs = useCallback( + (draggedPath: string, targetPath: string) => { + layoutStore.reorderFilePreviews(panelTabId, draggedPath, targetPath) + }, + [panelTabId], + ) // 是否显示预览 - const showPreview = previewContent || previewLoading || previewError + const showPreview = Boolean(previewFile) || previewLoading || Boolean(previewError) // 没有选择目录 if (!directory) { return ( -
- - Select a project to browse files +
+ { + e.currentTarget.style.visibility = 'hidden' + }} + /> + {t('fileExplorer.selectProject')}
) } @@ -220,44 +176,49 @@ export const FileExplorer = memo(function FileExplorer({ return (
{/* File Tree - 使用 CSS 变量控制高度 */} -
{/* Tree Header */} -
- - Explorer +
+ + {t('fileExplorer.explorer')} +
{/* Tree Content */}
{isLoading && tree.length === 0 ? ( -
- Loading... +
+ {t('common:loading')}
) : error ? ( -
+
{error}
) : tree.length === 0 ? ( -
- No files found +
+ {t('fileExplorer.noFilesFound')}
) : (
@@ -267,7 +228,6 @@ export const FileExplorer = memo(function FileExplorer({ node={node} depth={0} expandedPaths={expandedPaths} - selectedPath={selectedPath} fileStatus={fileStatus} onClick={handleFileClick} /> @@ -277,14 +237,13 @@ export const FileExplorer = memo(function FileExplorer({
- {/* Resize Handle - 扩大拖拽区域,支持触摸 */} + {/* Resize Handle - 与标签栏同色 */} {showPreview && (
@@ -316,7 +279,6 @@ interface FileTreeItemProps { node: FileTreeNode depth: number expandedPaths: Set - selectedPath: string | null fileStatus: Map onClick: (node: FileTreeNode) => void } @@ -325,34 +287,52 @@ const FileTreeItem = memo(function FileTreeItem({ node, depth, expandedPaths, - selectedPath, fileStatus, onClick, }: FileTreeItemProps) { const isExpanded = expandedPaths.has(node.path) - const isSelected = selectedPath === node.path const isDirectory = node.type === 'directory' - const status = fileStatus.get(node.path) + // node.path 可能用反斜杠(Windows),statusMap key 统一用正斜杠 + const status = fileStatus.get(node.path) || fileStatus.get(node.path.replace(/\\/g, '/')) // 状态颜色 const statusColor = useMemo(() => { if (!status) return null switch (status.status) { - case 'added': return 'text-success-100' - case 'modified': return 'text-warning-100' - case 'deleted': return 'text-danger-100' - default: return null + case 'added': + return 'text-success-100' + case 'modified': + return 'text-warning-100' + case 'deleted': + return 'text-danger-100' + default: + return null } }, [status]) + // 拖拽到输入框实现 @mention。使用 pointer 拖拽,避免 Tauri 原生文件 drop 接管 HTML5 DnD。 + const handlePointerDragStart = useCallback( + (e: PointerEvent) => { + const fileData = { + type: (isDirectory ? 'folder' : 'file') as 'file' | 'folder', + path: node.path, // 相对路径 + absolute: node.absolute, // 绝对路径 + name: node.name, + } + startInternalDrag(e, { kind: 'file-mention', file: fileData }) + }, + [node.path, node.absolute, node.name, isDirectory], + ) + return (
-
+ ) : null, + [content, fileName, handleDownload, t], + ) + + const fullscreenLayer = useMemo( + () => + fullscreenContent + ? { + id: `file-preview:${path || fileName}`, + title: fileName, + headerRight: fullscreenHeaderRight, + content: fullscreenContent, + } + : null, + [fileName, fullscreenContent, fullscreenHeaderRight, path], + ) + const { open: openFullscreen } = useFullscreenLayer(fullscreenLayer) + + return ( +
+ + + + + ) : null + } + /> {/* Preview Content */}
{isLoading ? ( -
- Loading... +
+ {t('common:loading')}
) : error ? ( -
+
{error}
- ) : displayContent?.type === 'diff' ? ( - - ) : displayContent?.type === 'text' ? ( + ) : displayContent?.type === 'media' ? ( + + ) : displayContent?.type === 'binary' ? ( + + ) : displayContent?.type === 'textMedia' ? ( + + ) : displayContent?.type === 'markdown' ? ( + + ) : // diff 渲染已移至 Changes 面板 + // ) : displayContent?.type === 'diff' ? ( + // + displayContent?.type === 'text' ? ( ) : ( -
- No content +
+ {t('common:noContent')}
)}
@@ -491,6 +648,366 @@ function FilePreview({ path, content, isLoading, error, onClose, isResizing = fa ) } +// ============================================ +// Media Preview - 路由到具体渲染器 +// ============================================ + +interface MediaPreviewProps { + category: PreviewCategory + dataUrl: string + mimeType: string + fileName: string +} + +function MediaPreview({ category, dataUrl, mimeType, fileName }: MediaPreviewProps) { + switch (category) { + case 'image': + return + case 'audio': + return ( +
+
{formatMimeType(mimeType)}
+
+ ) + case 'video': + return ( +
+
+ ) + case 'pdf': + return