diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 33ce2007a..195203f1b 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -15,44 +15,56 @@ jobs: environment: release outputs: GOFLAGS: ${{ steps.goflags.outputs.GOFLAGS }} + VERSION: ${{ steps.goflags.outputs.VERSION }} steps: - uses: actions/checkout@v4 - name: Set environment id: goflags run: | echo GOFLAGS="'-ldflags=-w -s \"-X=github.com/ollama/ollama/version.Version=${GITHUB_REF_NAME#v}\" \"-X=github.com/ollama/ollama/server.mode=release\"'" >>$GITHUB_OUTPUT + echo VERSION="${GITHUB_REF_NAME#v}" >>$GITHUB_OUTPUT darwin-build: - runs-on: macos-13-xlarge + runs-on: macos-14-xlarge environment: release needs: setup-environment - strategy: - matrix: - os: [darwin] - arch: [amd64, arm64] env: GOFLAGS: ${{ needs.setup-environment.outputs.GOFLAGS }} + VERSION: ${{ needs.setup-environment.outputs.VERSION }} + APPLE_IDENTITY: ${{ secrets.APPLE_IDENTITY }} + APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} + APPLE_TEAM_ID: ${{ vars.APPLE_TEAM_ID }} + APPLE_ID: ${{ vars.APPLE_ID }} + MACOS_SIGNING_KEY: ${{ secrets.MACOS_SIGNING_KEY }} + MACOS_SIGNING_KEY_PASSWORD: ${{ secrets.MACOS_SIGNING_KEY_PASSWORD }} + CGO_CFLAGS: '-mmacosx-version-min=14.0 -O3' + CGO_CXXFLAGS: '-mmacosx-version-min=14.0 -O3' + CGO_LDFLAGS: '-mmacosx-version-min=14.0 -O3' steps: - uses: actions/checkout@v4 + - run: | + echo $MACOS_SIGNING_KEY | base64 --decode > certificate.p12 + security create-keychain -p password build.keychain + security default-keychain -s build.keychain + security unlock-keychain -p password build.keychain + security import certificate.p12 -k build.keychain -P $MACOS_SIGNING_KEY_PASSWORD -T /usr/bin/codesign + security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k password build.keychain + security set-keychain-settings -lut 3600 build.keychain - uses: actions/setup-go@v5 with: go-version-file: go.mod - run: | - go build -o dist/ . - env: - GOOS: ${{ matrix.os }} - GOARCH: ${{ matrix.arch }} - CGO_ENABLED: 1 - CGO_CPPFLAGS: '-mmacosx-version-min=11.3' - - if: matrix.arch == 'amd64' + ./scripts/build_darwin.sh + - name: Log build results run: | - cmake --preset CPU -DCMAKE_OSX_DEPLOYMENT_TARGET=11.3 -DCMAKE_SYSTEM_PROCESSOR=x86_64 -DCMAKE_OSX_ARCHITECTURES=x86_64 - cmake --build --parallel --preset CPU - cmake --install build --component CPU --strip --parallel 8 + ls -l dist/ - uses: actions/upload-artifact@v4 with: - name: build-${{ matrix.os }}-${{ matrix.arch }} - path: dist/* + name: bundles-darwin + path: | + dist/*.tgz + dist/*.zip + dist/*.dmg windows-depends: strategy: @@ -72,7 +84,6 @@ jobs: - '"cublas_dev"' cuda-version: '12.8' flags: '' - runner_dir: 'cuda_v12' - os: windows arch: amd64 preset: 'CUDA 13' @@ -87,14 +98,12 @@ jobs: - '"nvptxcompiler"' cuda-version: '13.0' flags: '' - runner_dir: 'cuda_v13' - os: windows arch: amd64 preset: 'ROCm 6' install: https://download.amd.com/developer/eula/rocm-hub/AMD-Software-PRO-Edition-24.Q4-WinSvr2022-For-HIP.exe rocm-version: '6.2' flags: '-DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_C_FLAGS="-parallel-jobs=4 -Wno-ignored-attributes -Wno-deprecated-pragma" -DCMAKE_CXX_FLAGS="-parallel-jobs=4 -Wno-ignored-attributes -Wno-deprecated-pragma"' - runner_dir: 'rocm' runs-on: ${{ matrix.arch == 'arm64' && format('{0}-{1}', matrix.os, matrix.arch) || matrix.os }} environment: release env: @@ -160,12 +169,15 @@ jobs: run: | Import-Module 'C:\Program Files\Microsoft Visual Studio\2022\Enterprise\Common7\Tools\Microsoft.VisualStudio.DevShell.dll' Enter-VsDevShell -VsInstallPath 'C:\Program Files\Microsoft Visual Studio\2022\Enterprise' -SkipAutomaticLocation -DevCmdArguments '-arch=x64 -no_logo' - cmake --preset "${{ matrix.preset }}" ${{ matrix.flags }} -DOLLAMA_RUNNER_DIR="${{ matrix.runner_dir }}" - cmake --build --parallel --preset "${{ matrix.preset }}" - cmake --install build --component "${{ startsWith(matrix.preset, 'CUDA ') && 'CUDA' || startsWith(matrix.preset, 'ROCm ') && 'HIP' || 'CPU' }}" --strip --parallel 8 + cmake --preset "${{ matrix.preset }}" ${{ matrix.flags }} --install-prefix "$((pwd).Path)\dist\${{ matrix.os }}-${{ matrix.arch }}" + cmake --build --parallel ([Environment]::ProcessorCount) --preset "${{ matrix.preset }}" + cmake --install build --component "${{ startsWith(matrix.preset, 'CUDA ') && 'CUDA' || startsWith(matrix.preset, 'ROCm ') && 'HIP' || 'CPU' }}" --strip Remove-Item -Path dist\lib\ollama\rocm\rocblas\library\*gfx906* -ErrorAction SilentlyContinue env: CMAKE_GENERATOR: Ninja + - name: Log build results + run: | + gci -path .\dist -Recurse -File | ForEach-Object { get-filehash -path $_.FullName -Algorithm SHA256 } | format-list - uses: actions/upload-artifact@v4 with: name: depends-${{ matrix.os }}-${{ matrix.arch }}-${{ matrix.preset }} @@ -188,6 +200,7 @@ jobs: needs: [setup-environment] env: GOFLAGS: ${{ needs.setup-environment.outputs.GOFLAGS }} + VERSION: ${{ needs.setup-environment.outputs.VERSION }} steps: - name: Install ARM64 system dependencies if: matrix.arch == 'arm64' @@ -198,6 +211,9 @@ jobs: iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1')) echo "C:\ProgramData\chocolatey\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + Invoke-WebRequest -Uri https://aka.ms/vs/17/release/vc_redist.arm64.exe -OutFile "${{ runner.temp }}\vc_redist.arm64.exe" + Start-Process -FilePath "${{ runner.temp }}\vc_redist.arm64.exe" -ArgumentList @("/install", "/quiet", "/norestart") -NoNewWindow -Wait + choco install -y --no-progress git gzip echo "C:\Program Files\Git\cmd" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append - name: Install clang and gcc-compat @@ -223,13 +239,72 @@ jobs: exit 1 } $ErrorActionPreference='Stop' + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" - run: | - go build -o dist/${{ matrix.os }}-${{ matrix.arch }}/ . + ./scripts/build_windows ollama app + - name: Log build results + run: | + gci -path .\dist -Recurse -File | ForEach-Object { get-filehash -path $_.FullName -Algorithm SHA256 } | format-list - uses: actions/upload-artifact@v4 with: name: build-${{ matrix.os }}-${{ matrix.arch }} path: | - dist\${{ matrix.os }}-${{ matrix.arch }}\*.exe + dist\* + + windows-app: + runs-on: windows + environment: release + needs: [windows-build, windows-depends] + env: + GOFLAGS: ${{ needs.setup-environment.outputs.GOFLAGS }} + VERSION: ${{ needs.setup-environment.outputs.VERSION }} + KEY_CONTAINER: ${{ vars.KEY_CONTAINER }} + steps: + - uses: actions/checkout@v4 + # - uses: google-github-actions/auth@v2 + # with: + # project_id: ollama + # credentials_json: ${{ secrets.GOOGLE_SIGNING_CREDENTIALS }} + # - run: | + # $ErrorActionPreference = "Stop" + # Invoke-WebRequest -Uri "https://go.microsoft.com/fwlink/p/?LinkId=323507" -OutFile "${{ runner.temp }}\sdksetup.exe" + # Start-Process "${{ runner.temp }}\sdksetup.exe" -ArgumentList @("/q") -NoNewWindow -Wait + + # Invoke-WebRequest -Uri "https://github.com/GoogleCloudPlatform/kms-integrations/releases/download/cng-v1.0/kmscng-1.0-windows-amd64.zip" -OutFile "${{ runner.temp }}\plugin.zip" + # Expand-Archive -Path "${{ runner.temp }}\plugin.zip" -DestinationPath "${{ runner.temp }}\plugin\" + # & "${{ runner.temp }}\plugin\*\kmscng.msi" /quiet + + # echo "${{ vars.OLLAMA_CERT }}" >ollama_inc.crt + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + - uses: actions/download-artifact@v4 + with: + pattern: depends-windows* + path: dist + merge-multiple: true + - uses: actions/download-artifact@v4 + with: + pattern: build-windows* + path: dist + merge-multiple: true + - name: Log dist contents after download + run: | + gci -path .\dist -recurse + - run: | + ./scripts/build_windows.ps1 deps sign installer zip + - name: Log contents after build + run: | + gci -path .\dist -Recurse -File | ForEach-Object { get-filehash -path $_.FullName -Algorithm SHA256 } | format-list + - uses: actions/upload-artifact@v4 + with: + name: bundles-windows + path: | + dist/*.zip + dist/OllamaSetup.exe linux-build: strategy: @@ -288,7 +363,7 @@ jobs: done - uses: actions/upload-artifact@v4 with: - name: dist-${{ matrix.os }}-${{ matrix.arch }}-${{ matrix.target }} + name: bundles-${{ matrix.os }}-${{ matrix.arch }}-${{ matrix.target }} path: | *.tgz @@ -344,7 +419,7 @@ jobs: with: context: . platforms: ${{ matrix.os }}/${{ matrix.arch }} - target: ${{ matrix.target }} + target: ${{ matrix.preset }} build-args: ${{ matrix.build-args }} outputs: type=image,name=${{ vars.DOCKER_REPO }},push-by-digest=true,name-canonical=true,push=true cache-from: type=registry,ref=${{ vars.DOCKER_REPO }}:latest @@ -393,17 +468,28 @@ jobs: docker buildx imagetools inspect ${{ vars.DOCKER_REPO }}:${{ steps.metadata.outputs.version }} working-directory: ${{ runner.temp }} - # Trigger downstream release process - trigger: + # Final release process + release: runs-on: ubuntu-latest environment: release - needs: [darwin-build, windows-build, windows-depends, linux-build] + needs: [darwin-build, windows-app, linux-build] permissions: contents: write env: GH_TOKEN: ${{ github.token }} steps: - uses: actions/checkout@v4 + - uses: actions/download-artifact@v4 + with: + pattern: bundles-* + path: dist + merge-multiple: true + - name: Log dist contents + run: | + ls -l dist/ + - name: Generate checksum file + run: find . -type f -not -name 'sha256sum.txt' | xargs sha256sum | tee sha256sum.txt + working-directory: dist - name: Create or update Release for tag run: | RELEASE_VERSION="$(echo ${GITHUB_REF_NAME} | cut -f1 -d-)" @@ -420,12 +506,17 @@ jobs: --generate-notes \ --prerelease fi - - name: Trigger downstream release process + - name: Upload release artifacts run: | - curl -L \ - -X POST \ - -H "Accept: application/vnd.github+json" \ - -H "Authorization: Bearer ${{ secrets.RELEASE_TOKEN }}" \ - -H "X-GitHub-Api-Version: 2022-11-28" \ - https://api.github.com/repos/ollama/${{ vars.RELEASE_REPO }}/dispatches \ - -d "{\"event_type\": \"trigger-workflow\", \"client_payload\": {\"run_id\": \"${GITHUB_RUN_ID}\", \"version\": \"${GITHUB_REF_NAME#v}\", \"origin\": \"${GITHUB_REPOSITORY}\", \"publish\": \"1\"}}" + pids=() + for payload in dist/*.txt dist/*.zip dist/*.tgz dist/*.exe dist/*.dmg ; do + echo "Uploading $payload" + gh release upload ${GITHUB_REF_NAME} $payload --clobber & + pids[$!]=$! + sleep 1 + done + echo "Waiting for uploads to complete" + for pid in "${pids[*]}"; do + wait $pid + done + echo "done" diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 12ee71351..d74da923c 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -200,51 +200,26 @@ jobs: runs-on: ${{ matrix.os }} env: CGO_ENABLED: '1' - GOEXPERIMENT: 'synctest' steps: - - name: checkout - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # 4.2.2 - - - name: cache restore - uses: actions/cache/restore@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 with: - # Note: unlike the other setups, this is only grabbing the mod download - # cache, rather than the whole mod directory, as the download cache - # contains zips that can be unpacked in parallel faster than they can be - # fetched and extracted by tar - path: | - ~/.cache/go-build - ~/go/pkg/mod/cache - ~\AppData\Local\go-build - # NOTE: The -3- here should be incremented when the scheme of data to be - # cached changes (e.g. path above changes). - key: ${{ github.job }}-${{ runner.os }}-${{ matrix.goarch }}-${{ matrix.buildflags }}-go-3-${{ hashFiles('**/go.sum') }}-${{ github.run_id }} - restore-keys: | - ${{ github.job }}-${{ runner.os }}-${{ matrix.goarch }}-${{ matrix.buildflags }}-go-3-${{ hashFiles('**/go.sum') }} - ${{ github.job }}-${{ runner.os }}-${{ matrix.goarch }}-${{ matrix.buildflags }}-go-3- - - - name: Setup Go - uses: actions/setup-go@v5 + go-version-file: 'go.mod' + - uses: actions/setup-node@v4 with: - # The caching strategy of setup-go is less than ideal, and wastes - # time by not saving artifacts due to small failures like the linter - # complaining, etc. This means subsequent have to rebuild their world - # again until all checks pass. For instance, if you mispell a word, - # you're punished until you fix it. This is more hostile than - # helpful. - cache: false - - go-version-file: go.mod - - # It is tempting to run this in a platform independent way, but the past - # shows this codebase will see introductions of platform specific code - # generation, and so we need to check this per platform to ensure we - # don't abuse go generate on specific platforms. - - name: check that 'go generate' is clean - if: always() + node-version: '20' + - name: Install UI dependencies + working-directory: ./app/ui/app + run: npm ci + - name: Install tscriptify run: | - go generate ./... - git diff --name-only --exit-code || (echo "Please run 'go generate ./...'." && exit 1) + go install github.com/tkrajina/typescriptify-golang-structs/tscriptify@latest + - name: Run UI tests + if: ${{ startsWith(matrix.os, 'ubuntu') }} + working-directory: ./app/ui/app + run: npm test + - name: Run go generate + run: go generate ./... - name: go test if: always() @@ -257,26 +232,6 @@ jobs: with: args: --timeout 10m0s -v - - name: cache save - # Always save the cache, even if the job fails. The artifacts produced - # during the building of test binaries are not all for naught. They can - # be used to speed up subsequent runs. - if: always() - - uses: actions/cache/save@1bd1e32a3bdc45362d1e726936510720a7c30a57 # v4.2.0 - with: - # Note: unlike the other setups, this is only grabbing the mod download - # cache, rather than the whole mod directory, as the download cache - # contains zips that can be unpacked in parallel faster than they can be - # fetched and extracted by tar - path: | - ~/.cache/go-build - ~/go/pkg/mod/cache - ~\AppData\Local\go-build - # NOTE: The -3- here should be incremented when the scheme of data to be - # cached changes (e.g. path above changes). - key: ${{ github.job }}-${{ runner.os }}-${{ matrix.goarch }}-${{ matrix.buildflags }}-go-3-${{ hashFiles('**/go.sum') }}-${{ github.run_id }} - patches: runs-on: ubuntu-latest steps: diff --git a/CMakePresets.json b/CMakePresets.json index 72417ade9..6fcdf4d25 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -23,7 +23,8 @@ "inherits": [ "CUDA" ], "cacheVariables": { "CMAKE_CUDA_ARCHITECTURES": "50-virtual;60-virtual;61-virtual;70-virtual;75-virtual;80-virtual;86-virtual;87-virtual;89-virtual;90-virtual", - "CMAKE_CUDA_FLAGS": "-Wno-deprecated-gpu-targets -t 2" + "CMAKE_CUDA_FLAGS": "-Wno-deprecated-gpu-targets -t 2", + "OLLAMA_RUNNER_DIR": "cuda_v11" } }, { @@ -31,7 +32,8 @@ "inherits": [ "CUDA" ], "cacheVariables": { "CMAKE_CUDA_ARCHITECTURES": "50;52;60;61;70;75;80;86;89;90;90a;120", - "CMAKE_CUDA_FLAGS": "-Wno-deprecated-gpu-targets -t 2" + "CMAKE_CUDA_FLAGS": "-Wno-deprecated-gpu-targets -t 2", + "OLLAMA_RUNNER_DIR": "cuda_v12" } }, { @@ -39,21 +41,24 @@ "inherits": [ "CUDA" ], "cacheVariables": { "CMAKE_CUDA_ARCHITECTURES": "75-virtual;80-virtual;86-virtual;87-virtual;89-virtual;90-virtual;90a-virtual;100-virtual;103-virtual;110-virtual;120-virtual;121-virtual", - "CMAKE_CUDA_FLAGS": "-t 2" + "CMAKE_CUDA_FLAGS": "-t 2", + "OLLAMA_RUNNER_DIR": "cuda_v13" } }, { "name": "JetPack 5", "inherits": [ "CUDA" ], "cacheVariables": { - "CMAKE_CUDA_ARCHITECTURES": "72;87" + "CMAKE_CUDA_ARCHITECTURES": "72;87", + "OLLAMA_RUNNER_DIR": "cuda_jetpack5" } }, { "name": "JetPack 6", "inherits": [ "CUDA" ], "cacheVariables": { - "CMAKE_CUDA_ARCHITECTURES": "87" + "CMAKE_CUDA_ARCHITECTURES": "87", + "OLLAMA_RUNNER_DIR": "cuda_jetpack6" } }, { @@ -68,12 +73,16 @@ "inherits": [ "ROCm" ], "cacheVariables": { "CMAKE_HIP_FLAGS": "-parallel-jobs=4", - "AMDGPU_TARGETS": "gfx940;gfx941;gfx942;gfx1010;gfx1012;gfx1030;gfx1100;gfx1101;gfx1102;gfx1151;gfx1200;gfx1201;gfx908:xnack-;gfx90a:xnack+;gfx90a:xnack-" + "AMDGPU_TARGETS": "gfx940;gfx941;gfx942;gfx1010;gfx1012;gfx1030;gfx1100;gfx1101;gfx1102;gfx1151;gfx1200;gfx1201;gfx908:xnack-;gfx90a:xnack+;gfx90a:xnack-", + "OLLAMA_RUNNER_DIR": "rocm" } }, { "name": "Vulkan", - "inherits": [ "Default" ] + "inherits": [ "Default" ], + "cacheVariables": { + "OLLAMA_RUNNER_DIR": "vulkan" + } } ], "buildPresets": [ diff --git a/Dockerfile b/Dockerfile index dbc9207e3..c56c229aa 100644 --- a/Dockerfile +++ b/Dockerfile @@ -58,7 +58,7 @@ RUN dnf install -y cuda-toolkit-${CUDA11VERSION//./-} ENV PATH=/usr/local/cuda-11/bin:$PATH ARG PARALLEL RUN --mount=type=cache,target=/root/.ccache \ - cmake --preset 'CUDA 11' -DOLLAMA_RUNNER_DIR="cuda_v11" \ + cmake --preset 'CUDA 11' \ && cmake --build --parallel ${PARALLEL} --preset 'CUDA 11' \ && cmake --install build --component CUDA --strip --parallel ${PARALLEL} @@ -68,7 +68,7 @@ RUN dnf install -y cuda-toolkit-${CUDA12VERSION//./-} ENV PATH=/usr/local/cuda-12/bin:$PATH ARG PARALLEL RUN --mount=type=cache,target=/root/.ccache \ - cmake --preset 'CUDA 12' -DOLLAMA_RUNNER_DIR="cuda_v12"\ + cmake --preset 'CUDA 12' \ && cmake --build --parallel ${PARALLEL} --preset 'CUDA 12' \ && cmake --install build --component CUDA --strip --parallel ${PARALLEL} @@ -79,7 +79,7 @@ RUN dnf install -y cuda-toolkit-${CUDA13VERSION//./-} ENV PATH=/usr/local/cuda-13/bin:$PATH ARG PARALLEL RUN --mount=type=cache,target=/root/.ccache \ - cmake --preset 'CUDA 13' -DOLLAMA_RUNNER_DIR="cuda_v13" \ + cmake --preset 'CUDA 13' \ && cmake --build --parallel ${PARALLEL} --preset 'CUDA 13' \ && cmake --install build --component CUDA --strip --parallel ${PARALLEL} @@ -88,7 +88,7 @@ FROM base AS rocm-6 ENV PATH=/opt/rocm/hcc/bin:/opt/rocm/hip/bin:/opt/rocm/bin:/opt/rocm/hcc/bin:$PATH ARG PARALLEL RUN --mount=type=cache,target=/root/.ccache \ - cmake --preset 'ROCm 6' -DOLLAMA_RUNNER_DIR="rocm" \ + cmake --preset 'ROCm 6' \ && cmake --build --parallel ${PARALLEL} --preset 'ROCm 6' \ && cmake --install build --component HIP --strip --parallel ${PARALLEL} RUN rm -f dist/lib/ollama/rocm/rocblas/library/*gfx90[06]* @@ -101,7 +101,7 @@ COPY CMakeLists.txt CMakePresets.json . COPY ml/backend/ggml/ggml ml/backend/ggml/ggml ARG PARALLEL RUN --mount=type=cache,target=/root/.ccache \ - cmake --preset 'JetPack 5' -DOLLAMA_RUNNER_DIR="cuda_jetpack5" \ + cmake --preset 'JetPack 5' \ && cmake --build --parallel ${PARALLEL} --preset 'JetPack 5' \ && cmake --install build --component CUDA --strip --parallel ${PARALLEL} @@ -113,13 +113,13 @@ COPY CMakeLists.txt CMakePresets.json . COPY ml/backend/ggml/ggml ml/backend/ggml/ggml ARG PARALLEL RUN --mount=type=cache,target=/root/.ccache \ - cmake --preset 'JetPack 6' -DOLLAMA_RUNNER_DIR="cuda_jetpack6" \ + cmake --preset 'JetPack 6' \ && cmake --build --parallel ${PARALLEL} --preset 'JetPack 6' \ && cmake --install build --component CUDA --strip --parallel ${PARALLEL} FROM base AS vulkan RUN --mount=type=cache,target=/root/.ccache \ - cmake --preset 'Vulkan' -DOLLAMA_RUNNER_DIR="vulkan" \ + cmake --preset 'Vulkan' \ && cmake --build --parallel --preset 'Vulkan' \ && cmake --install build --component Vulkan --strip --parallel 8 diff --git a/app/.gitignore b/app/.gitignore index 0aa247948..a83eb70ad 100644 --- a/app/.gitignore +++ b/app/.gitignore @@ -1 +1,11 @@ ollama.syso +*.crt +*.exe +/app/app +/app/squirrel +ollama +*cover* +.vscode +.env +.DS_Store +.claude diff --git a/app/README.md b/app/README.md index 433ee44e8..fcacc07a3 100644 --- a/app/README.md +++ b/app/README.md @@ -1,22 +1,107 @@ -# Ollama App +# Ollama for macOS and Windows -## Linux +## Download -TODO +- [macOS](https://github.com/ollama/app/releases/download/latest/Ollama.dmg) +- [Windows](https://github.com/ollama/app/releases/download/latest/OllamaSetup.exe) -## MacOS +## Development -TODO +### Desktop App -## Windows +```bash +go generate ./... && +go run ./cmd/app +``` + +### UI Development + +#### Setup + +Install required tools: + +```bash +go install github.com/tkrajina/typescriptify-golang-structs/tscriptify@latest +``` + +#### Develop UI (Development Mode) + +1. Start the React development server (with hot-reload): + +```bash +cd ui/app +npm install +npm run dev +``` + +2. In a separate terminal, run the Ollama app with the `-dev` flag: + +```bash +go generate ./... && +OLLAMA_DEBUG=1 go run ./cmd/app -dev +``` + +The `-dev` flag enables: + +- Loading the UI from the Vite dev server at http://localhost:5173 +- Fixed UI server port at http://127.0.0.1:3001 for API requests +- CORS headers for cross-origin requests +- Hot-reload support for UI development + +#### Run Storybook + +Inside the `ui/app` directory, run: + +```bash +npm run storybook +``` + +For now we're writing stories as siblings of the component they're testing. So for example, `src/components/Message.stories.tsx` is the story for `src/components/Message.tsx`. + +## Build + + +### Windows -If you want to build the installer, youll need to install - https://jrsoftware.org/isinfo.php -In the top directory of this repo, run the following powershell script -to build the ollama CLI, ollama app, and ollama installer. - +**Dependencies** - either build a local copy of ollama, or use a github release ```powershell -powershell -ExecutionPolicy Bypass -File .\scripts\build_windows.ps1 +# Local dependencies +.\scripts\deps_local.ps1 + +# Release dependencies +.\scripts\deps_release.ps1 0.6.8 +``` + +**Build** +```powershell +.\scripts\build_windows.ps1 +``` + +### macOS + +CI builds with Xcode 14.1 for OS compatibility prior to v13. If you want to manually build v11+ support, you can download the older Xcode [here](https://developer.apple.com/services-account/download?path=/Developer_Tools/Xcode_14.1/Xcode_14.1.xip), extract, then `mv ./Xcode.app /Applications/Xcode_14.1.0.app` then activate with: + +``` +export CGO_CFLAGS=-mmacosx-version-min=12.0 +export CGO_CXXFLAGS=-mmacosx-version-min=12.0 +export CGO_LDFLAGS=-mmacosx-version-min=12.0 +export SDKROOT=/Applications/Xcode_14.1.0.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk +export DEVELOPER_DIR=/Applications/Xcode_14.1.0.app/Contents/Developer +``` + +**Dependencies** - either build a local copy of Ollama, or use a GitHub release: +```sh +# Local dependencies +./scripts/deps_local.sh + +# Release dependencies +./scripts/deps_release.sh 0.6.8 +``` + +**Build** +```sh +./scripts/build_darwin.sh ``` diff --git a/app/assets/assets.go b/app/assets/assets.go index 6fed2d0ee..1146e41c5 100644 --- a/app/assets/assets.go +++ b/app/assets/assets.go @@ -1,3 +1,5 @@ +//go:build windows || darwin + package assets import ( diff --git a/app/assets/background.png b/app/assets/background.png new file mode 100644 index 000000000..bbfc5879f Binary files /dev/null and b/app/assets/background.png differ diff --git a/app/assets/tray.ico b/app/assets/tray.ico index e63616c57..578669824 100644 Binary files a/app/assets/tray.ico and b/app/assets/tray.ico differ diff --git a/app/assets/tray_upgrade.ico b/app/assets/tray_upgrade.ico index d20830518..4cb604c9d 100644 Binary files a/app/assets/tray_upgrade.ico and b/app/assets/tray_upgrade.ico differ diff --git a/app/auth/connect.go b/app/auth/connect.go new file mode 100644 index 000000000..2d3297e18 --- /dev/null +++ b/app/auth/connect.go @@ -0,0 +1,26 @@ +//go:build windows || darwin + +package auth + +import ( + "encoding/base64" + "fmt" + "net/url" + "os" + + "github.com/ollama/ollama/auth" +) + +// BuildConnectURL generates the connect URL with the public key and device name +func BuildConnectURL(baseURL string) (string, error) { + pubKey, err := auth.GetPublicKey() + if err != nil { + return "", fmt.Errorf("failed to get public key: %w", err) + } + + encodedKey := base64.RawURLEncoding.EncodeToString([]byte(pubKey)) + hostname, _ := os.Hostname() + encodedDevice := url.QueryEscape(hostname) + + return fmt.Sprintf("%s/connect?name=%s&key=%s&launch=true", baseURL, encodedDevice, encodedKey), nil +} diff --git a/app/cmd/app/AppDelegate.h b/app/cmd/app/AppDelegate.h new file mode 100644 index 000000000..b75825236 --- /dev/null +++ b/app/cmd/app/AppDelegate.h @@ -0,0 +1,7 @@ +#import + +@interface AppDelegate : NSObject + +- (void)applicationDidFinishLaunching:(NSNotification *)aNotification; + +@end \ No newline at end of file diff --git a/app/cmd/app/app.go b/app/cmd/app/app.go new file mode 100644 index 000000000..1a4cd568f --- /dev/null +++ b/app/cmd/app/app.go @@ -0,0 +1,478 @@ +//go:build windows || darwin + +package main + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "log/slog" + "net" + "net/http" + "net/url" + "os" + "os/exec" + "os/signal" + "path/filepath" + "runtime" + "strings" + "syscall" + "time" + + "github.com/google/uuid" + "github.com/ollama/ollama/app/auth" + "github.com/ollama/ollama/app/logrotate" + "github.com/ollama/ollama/app/server" + "github.com/ollama/ollama/app/store" + "github.com/ollama/ollama/app/tools" + "github.com/ollama/ollama/app/ui" + "github.com/ollama/ollama/app/updater" + "github.com/ollama/ollama/app/version" +) + +var ( + wv = &Webview{} + uiServerPort int +) + +var debug = strings.EqualFold(os.Getenv("OLLAMA_DEBUG"), "true") || os.Getenv("OLLAMA_DEBUG") == "1" + +var ( + fastStartup = false + devMode = false +) + +type appMove int + +const ( + CannotMove appMove = iota + UserDeclinedMove + MoveCompleted + AlreadyMoved + LoginSession + PermissionDenied + MoveError +) + +func main() { + startHidden := false + var urlSchemeRequest string + if len(os.Args) > 1 { + for _, arg := range os.Args { + // Handle URL scheme requests (Windows) + if strings.HasPrefix(arg, "ollama://") { + urlSchemeRequest = arg + slog.Info("received URL scheme request", "url", arg) + continue + } + switch arg { + case "serve": + fmt.Fprintln(os.Stderr, "serve command not supported, use ollama") + os.Exit(1) + case "version", "-v", "--version": + fmt.Println(version.Version) + os.Exit(0) + case "background": + // When running the process in this "background" mode, we spawn a + // child process for the main app. This is necessary so the + // "Allow in the Background" setting in MacOS can be unchecked + // without breaking the main app. Two copies of the app are + // present in the bundle, one for the main app and one for the + // background initiator. + fmt.Fprintln(os.Stdout, "starting in background") + runInBackground() + os.Exit(0) + case "hidden", "-j", "--hide": + // startHidden suppresses the UI on startup, and can be triggered multiple ways + // On windows, path based via login startup detection + // On MacOS via [NSApp isHidden] from `open -j -a /Applications/Ollama.app` or equivalent + // On both via the "hidden" command line argument + startHidden = true + case "--fast-startup": + // Skip optional steps like pending updates to start quickly for immediate use + fastStartup = true + case "-dev", "--dev": + // Development mode: use local dev server and enable CORS + devMode = true + } + } + } + + level := slog.LevelInfo + if debug { + level = slog.LevelDebug + } + + logrotate.Rotate(appLogPath) + if _, err := os.Stat(filepath.Dir(appLogPath)); errors.Is(err, os.ErrNotExist) { + if err := os.MkdirAll(filepath.Dir(appLogPath), 0o755); err != nil { + slog.Error(fmt.Sprintf("failed to create server log dir %v", err)) + return + } + } + + var logFile io.Writer + var err error + logFile, err = os.OpenFile(appLogPath, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0o755) + if err != nil { + slog.Error(fmt.Sprintf("failed to create server log %v", err)) + return + } + // Detect if we're a GUI app on windows, and if not, send logs to console as well + if os.Stderr.Fd() != 0 { + // Console app detected + logFile = io.MultiWriter(os.Stderr, logFile) + } + + handler := slog.NewTextHandler(logFile, &slog.HandlerOptions{ + Level: level, + AddSource: true, + ReplaceAttr: func(_ []string, attr slog.Attr) slog.Attr { + if attr.Key == slog.SourceKey { + source := attr.Value.Any().(*slog.Source) + source.File = filepath.Base(source.File) + } + return attr + }, + }) + + slog.SetDefault(slog.New(handler)) + logStartup() + + // On Windows, check if another instance is running and send URL to it + // Do this after logging is set up so we can debug issues + if runtime.GOOS == "windows" && urlSchemeRequest != "" { + slog.Debug("checking for existing instance", "url", urlSchemeRequest) + if checkAndHandleExistingInstance(urlSchemeRequest) { + // The function will exit if it successfully sends to another instance + // If we reach here, we're the first/only instance + } else { + // No existing instance found, handle the URL scheme in this instance + go func() { + handleURLSchemeInCurrentInstance(urlSchemeRequest) + }() + } + } + + if u := os.Getenv("OLLAMA_UPDATE_URL"); u != "" { + updater.UpdateCheckURLBase = u + } + + // Detect if this is a first start after an upgrade, in + // which case we need to do some cleanup + var skipMove bool + if _, err := os.Stat(updater.UpgradeMarkerFile); err == nil { + slog.Debug("first start after upgrade") + err = updater.DoPostUpgradeCleanup() + if err != nil { + slog.Error("failed to cleanup prior version", "error", err) + } + // We never prompt to move the app after an upgrade + skipMove = true + // Start hidden after updates to prevent UI from opening automatically + startHidden = true + } + + if !skipMove && !fastStartup { + if maybeMoveAndRestart() == MoveCompleted { + return + } + } + + // Check if another instance is already running + // On Windows, focus the existing instance; on other platforms, kill it + handleExistingInstance(startHidden) + + // on macOS, offer the user to create a symlink + // from /usr/local/bin/ollama to the app bundle + installSymlink() + + var ln net.Listener + if devMode { + // Use a fixed port in dev mode for predictable API access + ln, err = net.Listen("tcp", "127.0.0.1:3001") + } else { + ln, err = net.Listen("tcp", "127.0.0.1:0") + } + if err != nil { + slog.Error("failed to find available port", "error", err) + return + } + + port := ln.Addr().(*net.TCPAddr).Port + token := uuid.NewString() + wv.port = port + wv.token = token + uiServerPort = port + + st := &store.Store{} + + // Enable CORS in development mode + if devMode { + os.Setenv("OLLAMA_CORS", "1") + + // Check if Vite dev server is running on port 5173 + var conn net.Conn + var err error + for _, addr := range []string{"127.0.0.1:5173", "localhost:5173"} { + conn, err = net.DialTimeout("tcp", addr, 2*time.Second) + if err == nil { + conn.Close() + break + } + } + + if err != nil { + slog.Error("Vite dev server not running on port 5173") + fmt.Fprintln(os.Stderr, "Error: Vite dev server is not running on port 5173") + fmt.Fprintln(os.Stderr, "Please run 'npm run dev' in the ui/app directory to start the UI in development mode") + os.Exit(1) + } + } + + // Initialize tools registry + toolRegistry := tools.NewRegistry() + slog.Info("initialized tools registry", "tool_count", len(toolRegistry.List())) + + // ctx is the app-level context that will be used to stop the app + ctx, cancel := context.WithCancel(context.Background()) + + // octx is the ollama server context that will be used to stop the ollama server + octx, ocancel := context.WithCancel(ctx) + + // TODO (jmorganca): instead we should instantiate the + // webview with the store instead of assigning it here, however + // making the webview a global variable is easier for now + wv.Store = st + done := make(chan error, 1) + osrv := server.New(st, devMode) + go func() { + slog.Info("starting ollama server") + done <- osrv.Run(octx) + }() + + uiServer := ui.Server{ + Token: token, + Restart: func() { + ocancel() + <-done + octx, ocancel = context.WithCancel(ctx) + go func() { + done <- osrv.Run(octx) + }() + }, + Store: st, + ToolRegistry: toolRegistry, + Dev: devMode, + Logger: slog.Default(), + } + + srv := &http.Server{ + Handler: uiServer.Handler(), + } + + if _, err := uiServer.UserData(ctx); err != nil { + slog.Warn("failed to load user data", "error", err) + } + + // Start the UI server + slog.Info("starting ui server", "port", port) + go func() { + slog.Debug("starting ui server on port", "port", port) + err = srv.Serve(ln) + if err != nil && !errors.Is(err, http.ErrServerClosed) { + slog.Warn("desktop server", "error", err) + } + slog.Debug("background desktop server done") + }() + + updater := &updater.Updater{Store: st} + updater.StartBackgroundUpdaterChecker(ctx, UpdateAvailable) + + hasCompletedFirstRun, err := st.HasCompletedFirstRun() + if err != nil { + slog.Error("failed to load has completed first run", "error", err) + } + + if !hasCompletedFirstRun { + err = st.SetHasCompletedFirstRun(true) + if err != nil { + slog.Error("failed to set has completed first run", "error", err) + } + } + + // capture SIGINT and SIGTERM signals and gracefully shutdown the app + signals := make(chan os.Signal, 1) + signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM) + go func() { + <-signals + slog.Info("received SIGINT or SIGTERM signal, shutting down") + quit() + }() + + if urlSchemeRequest != "" { + go func() { + handleURLSchemeInCurrentInstance(urlSchemeRequest) + }() + } else { + slog.Debug("no URL scheme request to handle") + } + + osRun(cancel, hasCompletedFirstRun, startHidden) + + slog.Info("shutting down desktop server") + if err := srv.Close(); err != nil { + slog.Warn("error shutting down desktop server", "error", err) + } + + slog.Info("shutting down ollama server") + cancel() + <-done +} + +func startHiddenTasks() { + // If an upgrade is ready and we're in hidden mode, perform it at startup. + // If we're not in hidden mode, we want to start as fast as possible and not + // slow the user down with an upgrade. + if updater.IsUpdatePending() { + if fastStartup { + // CLI triggered app startup use-case + slog.Info("deferring pending update for fast startup") + } else { + if err := updater.DoUpgradeAtStartup(); err != nil { + slog.Info("unable to perform upgrade at startup", "error", err) + // Make sure the restart to upgrade menu shows so we can attempt an interactive upgrade to get authorization + UpdateAvailable("") + } else { + slog.Debug("launching new version...") + // TODO - consider a timer that aborts if this takes too long and we haven't been killed yet... + LaunchNewApp() + os.Exit(0) + } + } + } +} + +func checkUserLoggedIn(uiServerPort int) bool { + if uiServerPort == 0 { + slog.Debug("UI server not ready yet, skipping auth check") + return false + } + + resp, err := http.Get(fmt.Sprintf("http://127.0.0.1:%d/api/v1/me", uiServerPort)) + if err != nil { + slog.Debug("failed to call local auth endpoint", "error", err) + return false + } + defer resp.Body.Close() + + // Check if the response is successful + if resp.StatusCode != http.StatusOK { + slog.Debug("auth endpoint returned non-OK status", "status", resp.StatusCode) + return false + } + + var user struct { + ID string `json:"id"` + Name string `json:"name"` + } + + if err := json.NewDecoder(resp.Body).Decode(&user); err != nil { + slog.Debug("failed to parse user response", "error", err) + return false + } + + // Verify we have a valid user with an ID and name + if user.ID == "" || user.Name == "" { + slog.Debug("user response missing required fields", "id", user.ID, "name", user.Name) + return false + } + + slog.Debug("user is logged in", "user_id", user.ID, "user_name", user.Name) + return true +} + +// handleConnectURLScheme fetches the connect URL and opens it in the browser +func handleConnectURLScheme() { + if checkUserLoggedIn(uiServerPort) { + slog.Info("user is already logged in, opening settings instead") + sendUIRequestMessage("/") + return + } + + connectURL, err := auth.BuildConnectURL("https://ollama.com") + if err != nil { + slog.Error("failed to build connect URL", "error", err) + openInBrowser("https://ollama.com/connect") + return + } + + openInBrowser(connectURL) +} + +// openInBrowser opens the specified URL in the default browser +func openInBrowser(url string) { + var cmd string + var args []string + + switch runtime.GOOS { + case "windows": + cmd = "rundll32" + args = []string{"url.dll,FileProtocolHandler", url} + case "darwin": + cmd = "open" + args = []string{url} + default: // "linux", "freebsd", "openbsd", "netbsd"... should not reach here + slog.Warn("unsupported OS for openInBrowser", "os", runtime.GOOS) + } + + slog.Info("executing browser command", "cmd", cmd, "args", args) + if err := exec.Command(cmd, args...).Start(); err != nil { + slog.Error("failed to open URL in browser", "url", url, "cmd", cmd, "args", args, "error", err) + } +} + +// parseURLScheme parses an ollama:// URL and returns whether it's a connect URL and the UI path +func parseURLScheme(urlSchemeRequest string) (isConnect bool, uiPath string, err error) { + parsedURL, err := url.Parse(urlSchemeRequest) + if err != nil { + return false, "", err + } + + // Check if this is a connect URL + if parsedURL.Host == "connect" || strings.TrimPrefix(parsedURL.Path, "/") == "connect" { + return true, "", nil + } + + // Extract the UI path + path := "/" + if parsedURL.Path != "" && parsedURL.Path != "/" { + // For URLs like ollama:///settings, use the path directly + path = parsedURL.Path + } else if parsedURL.Host != "" { + // For URLs like ollama://settings (without triple slash), + // the "settings" part is parsed as the host, not the path. + // We need to convert it to a path by prepending "/" + // This also handles ollama://settings/ where Windows adds a trailing slash + path = "/" + parsedURL.Host + } + + return false, path, nil +} + +// handleURLSchemeInCurrentInstance processes URL scheme requests in the current instance +func handleURLSchemeInCurrentInstance(urlSchemeRequest string) { + isConnect, uiPath, err := parseURLScheme(urlSchemeRequest) + if err != nil { + slog.Error("failed to parse URL scheme request", "url", urlSchemeRequest, "error", err) + return + } + + if isConnect { + handleConnectURLScheme() + } else { + sendUIRequestMessage(uiPath) + } +} diff --git a/app/cmd/app/app_darwin.go b/app/cmd/app/app_darwin.go new file mode 100644 index 000000000..2018ce8e4 --- /dev/null +++ b/app/cmd/app/app_darwin.go @@ -0,0 +1,269 @@ +//go:build windows || darwin + +package main + +// #cgo CFLAGS: -x objective-c +// #cgo LDFLAGS: -framework Webkit -framework Cocoa -framework LocalAuthentication -framework ServiceManagement +// #include "app_darwin.h" +// #include "../../updater/updater_darwin.h" +// typedef const char cchar_t; +import "C" + +import ( + "log/slog" + "os" + "os/exec" + "path/filepath" + "strings" + "time" + "unsafe" + + "github.com/ollama/ollama/app/updater" + "github.com/ollama/ollama/app/version" +) + +var ollamaPath = func() string { + if updater.BundlePath != "" { + return filepath.Join(updater.BundlePath, "Contents", "Resources", "ollama") + } + + pwd, err := os.Getwd() + if err != nil { + slog.Warn("failed to get pwd", "error", err) + return "" + } + return filepath.Join(pwd, "ollama") +}() + +var ( + isApp = updater.BundlePath != "" + appLogPath = filepath.Join(os.Getenv("HOME"), ".ollama", "logs", "app.log") + launchAgentPath = filepath.Join(os.Getenv("HOME"), "Library", "LaunchAgents", "com.ollama.ollama.plist") +) + +// TODO(jmorganca): pre-create the window and pass +// it to the webview instead of using the internal one +// +//export StartUI +func StartUI(path *C.cchar_t) { + p := C.GoString(path) + wv.Run(p) + styleWindow(wv.webview.Window()) + C.setWindowDelegate(wv.webview.Window()) +} + +//export ShowUI +func ShowUI() { + // If webview is already running, just show the window + if wv.IsRunning() && wv.webview != nil { + showWindow(wv.webview.Window()) + } else { + root := C.CString("/") + defer C.free(unsafe.Pointer(root)) + StartUI(root) + } +} + +//export StopUI +func StopUI() { + wv.Terminate() +} + +//export StartUpdate +func StartUpdate() { + if err := updater.DoUpgrade(true); err != nil { + slog.Error("upgrade failed", "error", err) + return + } + slog.Debug("launching new version...") + // TODO - consider a timer that aborts if this takes too long and we haven't been killed yet... + LaunchNewApp() + // not reached if upgrade works, the new app will kill this process +} + +//export darwinStartHiddenTasks +func darwinStartHiddenTasks() { + startHiddenTasks() +} + +func init() { + // Temporary code to mimic Squirrel ShipIt behavior + if len(os.Args) > 2 { + if os.Args[1] == "___launch___" { + path := strings.TrimPrefix(os.Args[2], "file://") + slog.Info("Ollama binary called as ShipIt - launching", "app", path) + appName := C.CString(path) + defer C.free(unsafe.Pointer(appName)) + C.launchApp(appName) + slog.Info("other instance has been launched") + time.Sleep(5 * time.Second) + slog.Info("exiting with zero status") + os.Exit(0) + } + } +} + +// maybeMoveAndRestart checks if we should relocate +// and returns true if we did and should immediately exit +func maybeMoveAndRestart() appMove { + if updater.BundlePath == "" { + // Typically developer mode with 'go run ./cmd/app' + return CannotMove + } + // Respect users intent if they chose "keep" vs. "replace" when dragging to Applications + if strings.HasPrefix(updater.BundlePath, strings.TrimSuffix(updater.SystemWidePath, filepath.Ext(updater.SystemWidePath))) { + return AlreadyMoved + } + + // Ask to move to applications directory + status := (appMove)(C.askToMoveToApplications()) + if status == MoveCompleted { + // Double check + if _, err := os.Stat(updater.SystemWidePath); err != nil { + slog.Warn("stat failure after move", "path", updater.SystemWidePath, "error", err) + return MoveError + } + } + return status +} + +// handleExistingInstance handles existing instances on macOS +func handleExistingInstance(_ bool) { + C.killOtherInstances() +} + +func installSymlink() { + if !isApp { + return + } + cliPath := C.CString(ollamaPath) + defer C.free(unsafe.Pointer(cliPath)) + + // Check the users path first + cmd, _ := exec.LookPath("ollama") + if cmd != "" { + resolved, err := os.Readlink(cmd) + if err == nil { + tmp, err := filepath.Abs(resolved) + if err == nil { + resolved = tmp + } + } else { + resolved = cmd + } + if resolved == ollamaPath { + slog.Info("ollama already in users PATH", "cli", cmd) + return + } + } + + code := C.installSymlink(cliPath) + if code != 0 { + slog.Error("Failed to install symlink") + } +} + +func UpdateAvailable(ver string) error { + slog.Debug("update detected, adjusting menu") + // TODO (jmorganca): find a better check for development mode than checking the bundle path + if updater.BundlePath != "" { + C.updateAvailable() + } + return nil +} + +func osRun(_ func(), hasCompletedFirstRun, startHidden bool) { + registerLaunchAgent(hasCompletedFirstRun) + + // Run the native macOS app + // Note: this will block until the app is closed + slog.Debug("starting native darwin event loop") + C.run(C._Bool(hasCompletedFirstRun), C._Bool(startHidden)) +} + +func quit() { + C.quit() +} + +func LaunchNewApp() { + appName := C.CString(updater.BundlePath) + defer C.free(unsafe.Pointer(appName)) + C.launchApp(appName) +} + +// Send a request to the main app thread to load a UI page +func sendUIRequestMessage(path string) { + p := C.CString(path) + defer C.free(unsafe.Pointer(p)) + C.uiRequest(p) +} + +func registerLaunchAgent(hasCompletedFirstRun bool) { + // Remove any stale Login Item registrations + C.unregisterSelfFromLoginItem() + + C.registerSelfAsLoginItem(C._Bool(hasCompletedFirstRun)) +} + +func logStartup() { + appPath := updater.BundlePath + if appPath == updater.SystemWidePath { + // Detect sandboxed scenario + exe, err := os.Executable() + if err == nil { + p := filepath.Dir(exe) + if filepath.Base(p) == "MacOS" { + p = filepath.Dir(filepath.Dir(p)) + if p != appPath { + slog.Info("starting sandboxed Ollama", "app", appPath, "sandbox", p) + return + } + } + } + } + slog.Info("starting Ollama", "app", appPath, "version", version.Version, "OS", updater.UserAgentOS) +} + +func hideWindow(ptr unsafe.Pointer) { + C.hideWindow(C.uintptr_t(uintptr(ptr))) +} + +func showWindow(ptr unsafe.Pointer) { + C.showWindow(C.uintptr_t(uintptr(ptr))) +} + +func styleWindow(ptr unsafe.Pointer) { + C.styleWindow(C.uintptr_t(uintptr(ptr))) +} + +func runInBackground() { + cmd := exec.Command(filepath.Join(updater.BundlePath, "Contents", "MacOS", "Ollama"), "hidden") + if cmd != nil { + err := cmd.Run() + if err != nil { + slog.Error("failed to run Ollama", "bundlePath", updater.BundlePath, "error", err) + os.Exit(1) + } + } else { + slog.Error("failed to start Ollama in background", "bundlePath", updater.BundlePath) + os.Exit(1) + } +} + +func drag(ptr unsafe.Pointer) { + C.drag(C.uintptr_t(uintptr(ptr))) +} + +func doubleClick(ptr unsafe.Pointer) { + C.doubleClick(C.uintptr_t(uintptr(ptr))) +} + +//export handleConnectURL +func handleConnectURL() { + handleConnectURLScheme() +} + +// checkAndHandleExistingInstance is not needed on non-Windows platforms +func checkAndHandleExistingInstance(_ string) bool { + return false +} diff --git a/app/cmd/app/app_darwin.h b/app/cmd/app/app_darwin.h new file mode 100644 index 000000000..4a5ba055f --- /dev/null +++ b/app/cmd/app/app_darwin.h @@ -0,0 +1,43 @@ +#import +#import + +@interface AppDelegate : NSObject +- (void)applicationDidFinishLaunching:(NSNotification *)aNotification; +@end + +enum AppMove +{ + CannotMove, + UserDeclinedMove, + MoveCompleted, + AlreadyMoved, + LoginSession, + PermissionDenied, + MoveError, +}; + +void run(bool firstTimeRun, bool startHidden); +void killOtherInstances(); +enum AppMove askToMoveToApplications(); +int createSymlinkWithAuthorization(); +int installSymlink(const char *cliPath); +extern void Restart(); +// extern void Quit(); +void StartUI(const char *path); +void ShowUI(); +void StopUI(); +void StartUpdate(); +void darwinStartHiddenTasks(); +void launchApp(const char *appPath); +void updateAvailable(); +void quit(); +void uiRequest(char *path); +void registerSelfAsLoginItem(bool firstTimeRun); +void unregisterSelfFromLoginItem(); +void setWindowDelegate(void *window); +void showWindow(uintptr_t wndPtr); +void hideWindow(uintptr_t wndPtr); +void styleWindow(uintptr_t wndPtr); +void drag(uintptr_t wndPtr); +void doubleClick(uintptr_t wndPtr); +void handleConnectURL(); diff --git a/app/cmd/app/app_darwin.m b/app/cmd/app/app_darwin.m new file mode 100644 index 000000000..4e1d52f76 --- /dev/null +++ b/app/cmd/app/app_darwin.m @@ -0,0 +1,1125 @@ +#import "app_darwin.h" +#import "menu.h" +#import "../../updater/updater_darwin.h" +#import +#import +#import +#import +#import +#import +#import + +extern NSString *SystemWidePath; + +@interface AppDelegate () +@property(strong, nonatomic) NSStatusItem *statusItem; +@property(assign, nonatomic) BOOL updateAvailable; +@end + +@implementation AppDelegate + +bool firstTimeRun,startHidden; // Set in run before initialization + +- (void)application:(NSApplication *)application openURLs:(NSArray *)urls { + for (NSURL *url in urls) { + if ([url.scheme isEqualToString:@"ollama"]) { + NSString *path = url.path; + if (!path || [path isEqualToString:@""]) { + // For URLs like ollama://settings (without triple slash), + // the "settings" part is parsed as the host, not the path. + // We need to convert it to a path by prepending "/" + if (url.host && ![url.host isEqualToString:@""]) { + path = [@"/" stringByAppendingString:url.host]; + } else { + path = @"/"; + } + } + + if ([path isEqualToString:@"/connect"] || [url.host isEqualToString:@"connect"]) { + // Special case: handle connect by opening browser instead of app + handleConnectURL(); + } else { + // Set app to be active and visible + [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular]; + [NSApp activateIgnoringOtherApps:YES]; + + // Open the path with the UI + [self uiRequest:path]; + } + + break; + } + } +} + +- (void)applicationDidFinishLaunching:(NSNotification *)aNotification { + // if we're in development mode, set the app icon + NSString *bundlePath = [[NSBundle mainBundle] bundlePath]; + if (![bundlePath hasSuffix:@".app"]) { + NSString *cwdPath = + [[NSFileManager defaultManager] currentDirectoryPath]; + NSString *iconPath = [cwdPath + stringByAppendingPathComponent: + [NSString + stringWithFormat: + @"darwin/Ollama.app/Contents/Resources/icon.icns"]]; + NSImage *customIcon = [[NSImage alloc] initWithContentsOfFile:iconPath]; + [NSApp setApplicationIconImage:customIcon]; + } + + // Create status item and menu + NSMenu *menu = [[NSMenu alloc] init]; + NSMenuItem *openMenuItem = + [[NSMenuItem alloc] initWithTitle:@"Open Ollama" + action:@selector(openUI) + keyEquivalent:@""]; + [openMenuItem setTarget:self]; + [menu addItem:openMenuItem]; + + [menu addItemWithTitle:@"Settings..." + action:@selector(settingsUI) + keyEquivalent:@","]; + [menu addItem:[NSMenuItem separatorItem]]; + + NSMenuItem *updateAvailable = + [[NSMenuItem alloc] initWithTitle:@"An update is available" + action:nil + keyEquivalent:@""]; + [updateAvailable setEnabled:NO]; + [updateAvailable setHidden:YES]; + [menu addItem:updateAvailable]; + + NSMenuItem *restartMenuItem = + [[NSMenuItem alloc] initWithTitle:@"Restart to update" + action:@selector(startUpdate) + keyEquivalent:@""]; + [restartMenuItem setTarget:self]; + [restartMenuItem setHidden:YES]; + [menu addItem:restartMenuItem]; + + [menu addItem:[NSMenuItem separatorItem]]; + + [menu addItemWithTitle:@"Quit Ollama" + action:@selector(quit) + keyEquivalent:@"q"]; + + self.statusItem = [[NSStatusBar systemStatusBar] + statusItemWithLength:NSVariableStatusItemLength]; + [self.statusItem addObserver:self + forKeyPath:@"button.effectiveAppearance" + options:NSKeyValueObservingOptionNew | + NSKeyValueObservingOptionInitial + context:nil]; + + self.statusItem.menu = menu; + [self showIcon]; + + // Application menu + NSString *appName = @"Ollama"; + + NSMenu *mainMenu = [[NSMenu alloc] init]; + NSMenuItem *appMenuItem = [[NSMenuItem alloc] initWithTitle:appName + action:nil + keyEquivalent:@""]; + NSMenu *appMenu = [[NSMenu alloc] initWithTitle:appName]; + [appMenuItem setSubmenu:appMenu]; + [mainMenu addItem:appMenuItem]; + + [appMenu addItemWithTitle:[NSString stringWithFormat:@"About %@", appName] + action:@selector(aboutOllama) + keyEquivalent:@""]; + [appMenu addItem:[NSMenuItem separatorItem]]; + [appMenu addItemWithTitle:@"Settings..." + action:@selector(settingsUI) + keyEquivalent:@","]; + [appMenu addItem:[NSMenuItem separatorItem]]; + [appMenu addItemWithTitle:[NSString stringWithFormat:@"Hide %@", appName] + action:@selector(hide:) + keyEquivalent:@"h"]; + + NSMenuItem *hideOthers = [[NSMenuItem alloc] initWithTitle:@"Hide Others" action:@selector(hideOtherApplications:) keyEquivalent:@"h"]; + hideOthers.keyEquivalentModifierMask = NSEventModifierFlagOption | NSEventModifierFlagCommand; + [appMenu addItem:hideOthers]; + [appMenu addItemWithTitle:@"Show All" + action:@selector(unhideAllApplications:) + keyEquivalent:@""]; + [appMenu addItem:[NSMenuItem separatorItem]]; + [appMenu addItemWithTitle:[NSString stringWithFormat:@"Quit %@", appName] + action:@selector(hide) + keyEquivalent:@"q"]; + + NSMenuItem *fileMenuItem = [[NSMenuItem alloc] init]; + NSMenu *fileMenu = [[NSMenu alloc] initWithTitle:@"File"]; + + NSMenuItem *newChatItem = [[NSMenuItem alloc] initWithTitle:@"New Chat" + action:@selector(newChat) + keyEquivalent:@"n"]; + [newChatItem setTarget:self]; + [fileMenu addItem:newChatItem]; + [fileMenu addItem:[NSMenuItem separatorItem]]; + + NSMenuItem *closeItem = [[NSMenuItem alloc] initWithTitle:@"Close Window" action:@selector(hide:) keyEquivalent:@"w"]; + [fileMenu addItem:closeItem]; + [fileMenuItem setSubmenu:fileMenu]; + [mainMenu addItem:fileMenuItem]; + + NSMenuItem *editMenuItem = [[NSMenuItem alloc] init]; + NSMenu *editMenu = [[NSMenu alloc] initWithTitle:@"Edit"]; + + [editMenu addItemWithTitle:@"Undo" + action:@selector(undo:) + keyEquivalent:@"z"]; + [editMenu addItemWithTitle:@"Redo" + action:@selector(redo:) + keyEquivalent:@"Z"]; + [editMenu addItem:[NSMenuItem separatorItem]]; + [editMenu addItemWithTitle:@"Cut" + action:@selector(cut:) + keyEquivalent:@"x"]; + [editMenu addItemWithTitle:@"Copy" + action:@selector(copy:) + keyEquivalent:@"c"]; + [editMenu addItemWithTitle:@"Paste" + action:@selector(paste:) + keyEquivalent:@"v"]; + [editMenu addItemWithTitle:@"Select All" + action:@selector(selectAll:) + keyEquivalent:@"a"]; + + [editMenuItem setSubmenu:editMenu]; + [mainMenu addItem:editMenuItem]; + + NSMenuItem *windowMenuItem = [[NSMenuItem alloc] init]; + NSMenu *windowMenu = [[NSMenu alloc] initWithTitle:@"Window"]; + [windowMenu addItemWithTitle:@"Minimize" + action:@selector(performMiniaturize:) + keyEquivalent:@"m"]; + [windowMenu addItemWithTitle:@"Zoom" + action:@selector(performZoom:) + keyEquivalent:@""]; + [windowMenu addItem:[NSMenuItem separatorItem]]; + [windowMenu addItemWithTitle:@"Bring All to Front" + action:@selector(arrangeInFront:) + keyEquivalent:@""]; + [windowMenuItem setSubmenu:windowMenu]; + [mainMenu addItem:windowMenuItem]; + [NSApp setWindowsMenu:windowMenu]; + + NSMenuItem *helpMenuItem = [[NSMenuItem alloc] init]; + NSMenu *helpMenu = [[NSMenu alloc] initWithTitle:@"Help"]; + [helpMenu addItemWithTitle:[NSString stringWithFormat:@"%@ Help", appName] + action:@selector(openHelp:) + keyEquivalent:@"?"]; + [helpMenuItem setSubmenu:helpMenu]; + [mainMenu addItem:helpMenuItem]; + [NSApp setHelpMenu:helpMenu]; + [NSApp setMainMenu:mainMenu]; + + BOOL hidden = [NSApp isHidden]; + dispatch_async(dispatch_get_main_queue(), ^{ + if (hidden || startHidden) { + darwinStartHiddenTasks(); + } else { + if (!startHidden) { + StartUI("/"); + } + } + }); +} + +- (void)applicationDidBecomeActive:(NSNotification *)notification { + NSRunningApplication *currentApp = [NSRunningApplication currentApplication]; + if (currentApp.activationPolicy == NSApplicationActivationPolicyAccessory) { + for (NSWindow *window in [NSApp windows]) { + if ([window isVisible]) { + // Switch to regular activation policy since we have a visible window + [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular]; + return; + } + } + [NSApp hide:nil]; + return; + } +} + +- (BOOL)applicationShouldHandleReopen:(NSApplication *)sender hasVisibleWindows:(BOOL)hasVisibleWindows { + [self openUI]; + return YES; +} + +- (void)showUpdateAvailable { + self.updateAvailable = YES; + [self.statusItem.menu.itemArray[3] setHidden:NO]; + [self.statusItem.menu.itemArray[4] setHidden:NO]; + [self showIcon]; +} + +- (void)aboutOllama { + [[NSApplication sharedApplication] orderFrontStandardAboutPanel:nil]; + [NSApp activateIgnoringOtherApps:YES]; +} + +- (void)openHelp:(id)sender { + NSURL *url = [NSURL URLWithString:@"https://github.com/ollama/ollama/tree/main/docs"]; + [[NSWorkspace sharedWorkspace] openURL:url]; +} + +- (void)settingsUI { + [self uiRequest:@"/settings"]; +} + +- (void)openUI { + ShowUI(); +} + +- (void)newChat { + [self uiRequest:@"/c/new"]; +} + +- (void)uiRequest:(NSString *)path { + if (path == nil) { + appLogInfo(@"app UI request for URL is missing"); + } + + appLogInfo([NSString + stringWithFormat:@"XXX got app UI request for URL: %@", path]); + StartUI([path UTF8String]); +} + +- (void)startUpdate { + StartUpdate(); + [NSApp activateIgnoringOtherApps:YES]; +} + +- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *)sender { + [NSApp hide:nil]; + [NSApp setActivationPolicy:NSApplicationActivationPolicyAccessory]; + return NSTerminateCancel; +} + +- (IBAction)terminate:(id)sender { + [NSApp hide:nil]; + [NSApp setActivationPolicy:NSApplicationActivationPolicyAccessory]; +} + +- (BOOL)windowShouldClose:(id)sender { + [NSApp hide:nil]; + return NO; +} + +- (void)showIcon { + NSAppearance *appearance = self.statusItem.button.effectiveAppearance; + NSString *appearanceName = (NSString *)(appearance.name); + NSString *iconName = @"ollama"; + if (self.updateAvailable) { + iconName = [iconName stringByAppendingString:@"Update"]; + } + if ([appearanceName containsString:@"Dark"]) { + iconName = [iconName stringByAppendingString:@"Dark"]; + } + + NSImage *statusImage; + NSBundle *bundle = [NSBundle mainBundle]; + if (![bundle.bundlePath hasSuffix:@".app"]) { + NSString *cwdPath = + [[NSFileManager defaultManager] currentDirectoryPath]; + NSString *bundlePath = + [cwdPath stringByAppendingPathComponent: + [NSString stringWithFormat:@"darwin/Ollama.app"]]; + bundle = [NSBundle bundleWithPath:bundlePath]; + } + + statusImage = [bundle imageForResource:iconName]; + [statusImage setTemplate:YES]; + self.statusItem.button.image = statusImage; +} + +- (void)observeValueForKeyPath:(NSString *)keyPath + ofObject:(id)object + change:(NSDictionary *)change + context:(void *)context { + [self showIcon]; +} + +- (void)hide { + [NSApp hide:nil]; + [NSApp setActivationPolicy:NSApplicationActivationPolicyAccessory]; +} + +- (void)quit { + [NSApp stop:self]; + [NSApp postEvent:[NSEvent otherEventWithType:NSEventTypeApplicationDefined + location:NSZeroPoint + modifierFlags:0 + timestamp:0 + windowNumber:0 + context:nil + subtype:0 + data1:0 + data2:0] + atStart:YES]; +} + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" +- (void)registerSelfAsLoginItem:(BOOL)firstTimeRun { + appLogInfo(@"using v13+ SMAppService for login registration"); + // Maps to the file Ollama.app/Contents/Library/LaunchAgents/com.ollama.ollama.plist + SMAppService* service = [SMAppService agentServiceWithPlistName:@"com.ollama.ollama.plist"]; + if (!service) { + appLogInfo(@"SMAppService failed to find service for com.ollama.ollama.plist"); + return; + } + SMAppServiceStatus status = [service status]; + switch (status) { + case SMAppServiceStatusNotRegistered: + appLogInfo(@"service not registered, registering now"); + break; + case SMAppServiceStatusEnabled: + appLogInfo(@"service is already enabled, no need to register again"); + return; + case SMAppServiceStatusRequiresApproval: + // User has disabled our login behavior explicitly so leave it as is + appLogInfo(@"service is currently disabled and will not start at login"); + return; + case SMAppServiceStatusNotFound: + appLogInfo(@"service not found, registering now"); + break; + default: + appLogInfo([NSString stringWithFormat:@"unexpected status: %ld", (long)status]); + break; + } + NSError *error = nil; + if (![service registerAndReturnError:&error]) { + appLogInfo([NSString stringWithFormat:@"Failed to register %@ as a login item: %@", NSBundle.mainBundle.bundleURL, error]); + return; + } + return; +} + +/// Remove ollama from the deprecated Login Items list as we now use LaunchAgents +- (void)unregisterSelfFromLoginItem { + NSURL *bundleURL = NSBundle.mainBundle.bundleURL; + NSString *bundlePrefix = [SystemWidePath stringByDeletingPathExtension]; + + LSSharedFileListRef loginItems = + LSSharedFileListCreate(NULL, kLSSharedFileListSessionLoginItems, NULL); + if (!loginItems) { + return; + } + + UInt32 seed; + CFArrayRef currentItems = LSSharedFileListCopySnapshot(loginItems, &seed); + + for (id item in (__bridge NSArray *)currentItems) { + CFURLRef itemURL = NULL; + if (LSSharedFileListItemResolve((LSSharedFileListItemRef)item, 0, + &itemURL, NULL) == noErr) { + CFStringRef loginPath = CFURLCopyFileSystemPath(itemURL, kCFURLPOSIXPathStyle); + // Compare the prefix to match against "keep existing" flow, e.g. // "/Applications/Ollama.app" vs "/Applications/Ollama 2.app" + if (loginPath && [(NSString *)loginPath hasPrefix:bundlePrefix]) { + appLogInfo([NSString stringWithFormat:@"removing login item %@", loginPath]); + LSSharedFileListItemRemove(loginItems, + (LSSharedFileListItemRef)item); + } + if (itemURL) { + CFRelease(itemURL); + } + } else if (!itemURL) { + // If the user has removed the App that has a current login item, we can't use + // LSSharedFileListItemResolve to get the file path, since it doesn't "resolve" + CFStringRef displayName = LSSharedFileListItemCopyDisplayName((LSSharedFileListItemRef)item); + if (displayName) { + NSString *name = (__bridge NSString *)displayName; + if ([name hasPrefix:@"Ollama"]) { + LSSharedFileListItemRemove(loginItems, (LSSharedFileListItemRef)item); + appLogInfo([NSString stringWithFormat:@"removing dangling login item %@", displayName]); + } + CFRelease(displayName); + } + } + } + if (currentItems) { + CFRelease(currentItems); + } + CFRelease(loginItems); +} +#pragma clang diagnostic pop + +- (void)windowWillEnterFullScreen:(NSNotification *)notification { + NSWindow *w = notification.object; + if (w.toolbar != nil) { + [w.toolbar setVisible:NO]; // hide the (empty) toolbar + } +} + +- (void)windowDidExitFullScreen:(NSNotification *)notification { + NSWindow *w = notification.object; + if (w.toolbar != nil) { + [w.toolbar setVisible:YES]; // show it again + } +} + +- (void) webView:(WKWebView *)webView +decidePolicyForNavigationAction:(WKNavigationAction *)action + decisionHandler:(void (^)(WKNavigationActionPolicy))handler +{ + NSURL *url = action.request.URL; + if (action.navigationType == WKNavigationTypeLinkActivated) { + NSString *host = [url.host lowercaseString]; + if ([host isEqualToString:@"localhost"] || + [host isEqualToString:@"127.0.0.1"]) { + handler(WKNavigationActionPolicyCancel); + NSString *path = url.path; + if (path.length == 0) { + path = @"/"; + } + [self uiRequest:path]; + return; + } + + [[NSWorkspace sharedWorkspace] openURL:url]; + handler(WKNavigationActionPolicyCancel); + return; + } + handler(WKNavigationActionPolicyAllow); +} + +- (nullable WKWebView *)webView:(WKWebView *)webView + createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration + forNavigationAction:(WKNavigationAction *)action + windowFeatures:(WKWindowFeatures *)features +{ + // "Open Link in New Window" (or target="_blank") ends up here. + NSURL *url = action.request.URL; + if (url) { + NSString *host = [url.host lowercaseString]; + if ([host isEqualToString:@"localhost"] || + [host isEqualToString:@"127.0.0.1"]) { + return nil; + } + [[NSWorkspace sharedWorkspace] openURL:url]; + } + return nil; +} + +// TODO (jmorganca): the confirm button is always "Confirm" +// it should be customizable in the future +- (void)webView:(WKWebView *)webView + runJavaScriptConfirmPanelWithMessage:(NSString *)message + initiatedByFrame:(WKFrameInfo *)frame + completionHandler:(void (^)(BOOL))completionHandler { + + NSAlert *alert = [[NSAlert alloc] init]; + [alert setMessageText:message]; + [alert addButtonWithTitle:@"Confirm"]; + [alert addButtonWithTitle:@"Cancel"]; + + completionHandler([alert runModal] == NSAlertFirstButtonReturn); +} + +// HACK (jmorganca): remove the "Copy Link with Highlight" item from the context menu by +// swizzling the WKWebView's willOpenMenu:withEvent: method. In the future we should probably +// subclass the WKWebView and override the context menu items, but this is a quick fix for now. ++ (void)load { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + [self swizzleWKWebViewContextMenu]; + }); +} + ++ (void)swizzleWKWebViewContextMenu { + Class class = [WKWebView class]; + + SEL originalSelector = @selector(willOpenMenu:withEvent:); + SEL swizzledSelector = @selector(ollama_willOpenMenu:withEvent:); + + Method originalMethod = class_getInstanceMethod(class, originalSelector); + Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector); + BOOL didAddMethod = class_addMethod(class, originalSelector, + method_getImplementation(swizzledMethod), + method_getTypeEncoding(swizzledMethod)); + + if (didAddMethod) { + class_replaceMethod(class, + swizzledSelector, + method_getImplementation(originalMethod), + method_getTypeEncoding(originalMethod)); + } else { + method_exchangeImplementations(originalMethod, swizzledMethod); + } +} + +@end + +@implementation WKWebView (OllamaContextMenu) +- (void)ollama_willOpenMenu:(NSMenu *)menu withEvent:(NSEvent *)event { + [self ollama_willOpenMenu:menu withEvent:event]; + NSMutableArray *itemsToRemove = [NSMutableArray array]; + for (NSMenuItem *item in menu.itemArray) { + if ([item.title containsString:@"Copy Link with Highlight"] || + [item.title containsString:@"Open Link in New Window"] || + [item.title containsString:@"Services"] || + [item.title containsString:@"Download Linked File"] || + [item.title containsString:@"Back"] || + [item.title containsString:@"Reload"] || + [item.title containsString:@"Refresh"] || + [item.title containsString:@"Open Link"] || + [item.title containsString:@"Copy Link"] || + [item.title containsString:@"Share"]) { + [itemsToRemove addObject:item]; + continue; + } + } + + for (NSMenuItem *item in itemsToRemove) { + [menu removeItem:item]; + } + + int customItemCount = menu_get_item_count(); + if (customItemCount > 0) { + menuItem* customItems = (menuItem*)menu_get_items(); + if (customItems) { + NSInteger insertIndex = 0; + + for (int i = 0; i < customItemCount; i++) { + if (customItems[i].separator) { + [menu insertItem:[NSMenuItem separatorItem] atIndex:insertIndex++]; + } else if (customItems[i].label) { + NSString *label = [NSString stringWithUTF8String:customItems[i].label]; + NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:label + action:@selector(handleCustomMenuItem:) + keyEquivalent:@""]; + [item setTarget:self]; + [item setRepresentedObject:label]; + [item setEnabled:customItems[i].enabled]; + [menu insertItem:item atIndex:insertIndex++]; + } + } + + // Add separator after custom items if there are remaining items + if (insertIndex > 0 && menu.itemArray.count > insertIndex) { + [menu insertItem:[NSMenuItem separatorItem] atIndex:insertIndex]; + } + } + } +} + +- (void)handleCustomMenuItem:(NSMenuItem *)sender { + NSString *label = [sender representedObject]; + if (label) { + menu_handle_selection((char*)[label UTF8String]); + } +} + +@end + +AppDelegate *appDelegate; +void run(bool ftr, bool sh) { + [NSApplication sharedApplication]; + [NSApp setActivationPolicy:NSApplicationActivationPolicyAccessory]; + appDelegate = [[AppDelegate alloc] init]; + [NSApp setDelegate:appDelegate]; + firstTimeRun = ftr; + startHidden = sh; + [NSApp run]; + StopUI(); +} + +// killOtherInstances kills all other instances of the app currently +// running. This way we can ensure that only the most recently started +// instance of Ollama is running +void killOtherInstances() { + pid_t myPid = getpid(); + NSArray *apps = [[NSWorkspace sharedWorkspace] runningApplications]; + + for (NSRunningApplication *app in apps) { + NSString *bundleId = app.bundleIdentifier; + + // Skip apps without bundle identifiers + if (!bundleId || [bundleId length] == 0) { + continue; + } + + if ([bundleId isEqualToString:[[NSBundle mainBundle] bundleIdentifier]] || + [bundleId isEqualToString:@"ai.ollama.ollama"] || + [bundleId isEqualToString:@"com.electron.ollama"]) { + + pid_t pid = app.processIdentifier; + if (pid != myPid && pid > 0) { + appLogInfo([NSString stringWithFormat:@"terminating other ollama instance %d", pid]); + kill(pid, SIGTERM); + } else if (pid == -1) { + appLogInfo([NSString stringWithFormat:@"skipping app with invalid pid: %@", bundleId]); + } + } + } +} + +// Move the source bundle to the system-wide applications location +// without prompting for additional authorization +bool moveToApplications(const char *src) { + NSString *bundlePath = @(src); + appLogInfo([NSString + stringWithFormat: + @"trying move to /Applications without extra authorization"]); + NSFileManager *fileManager = [NSFileManager defaultManager]; + + // Check if the newPath already exists + if ([fileManager fileExistsAtPath:SystemWidePath]) { + appLogInfo([NSString stringWithFormat:@"existing install exists"]); + NSError *removeError = nil; + [fileManager removeItemAtPath:SystemWidePath error:&removeError]; + if (removeError) { + appLogInfo([NSString + stringWithFormat:@"Error removing without authorization %@: %@", + SystemWidePath, removeError]); + return false; + } + } + + // Move can be problematic, so use copy + NSError *err = nil; + [fileManager copyItemAtPath:bundlePath toPath:SystemWidePath error:&err]; + if (err) { + appLogInfo( + [NSString stringWithFormat: + @"unable to copy without authorization %@ to %@: %@", + bundlePath, SystemWidePath, err]); + return false; + } + + // Best effort attempt to remove old content + if ([fileManager isDeletableFileAtPath:bundlePath]) { + err = nil; + [fileManager trashItemAtURL:[NSURL fileURLWithPath:bundlePath] + resultingItemURL:nil + error:&err]; + if (err) { + appLogInfo( + [NSString stringWithFormat:@"unable to clean up now stale " + @"bundle via file manager %@: %@", + bundlePath, err]); + } + } else { + appLogInfo([NSString stringWithFormat:@"unable to clean up now stale " + @"bundle via file manager %@", + bundlePath]); + } + + appLogInfo([NSString stringWithFormat:@"app relocated %@ to %@", bundlePath, + SystemWidePath]); + return true; +} + +AuthorizationRef getSymlinkAuthorization() { + return getAuthorization(@"Ollama is trying to install its command line " + @"interface (CLI) tool.", + @"symlink"); +} + +// Prompt the user for authorization and move to the system wide +// location +// +// Note: this flow must not be executed from the old app instance +// otherwise the malware scanner will trigger on subsequent +// AuthorizationExecuteWithPrivileges calls as it can not +// verify the calling app's signature on the filesystem +// once the files are removed +bool moveToApplicationsWithAuthorization(const char *src) { + int pid, status; + AuthorizationRef authRef = getAppInstallAuthorization(); + if (authRef == NULL) { + return NO; + } + + // Remove existing /Applications/Ollama.app (if any) + // - We do this via /bin/rm with elevated privileges + // + const char *rmTool = "/bin/rm"; + const char *rmArgs[] = {"-rf", [SystemWidePath UTF8String], NULL}; + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + OSStatus err = AuthorizationExecuteWithPrivileges( + authRef, rmTool, kAuthorizationFlagDefaults, (char *const *)rmArgs, + NULL); +#pragma clang diagnostic pop + + if (err != errAuthorizationSuccess) { + appLogInfo([NSString + stringWithFormat:@"Failed to remove existing %@. err = %d", + SystemWidePath, err]); + AuthorizationFree(authRef, kAuthorizationFlagDestroyRights); + return NO; + } + + // wait for the command to finish + pid = wait(&status); + if (pid == -1 || !WIFEXITED(status)) { + appLogInfo([NSString stringWithFormat:@"rm of %@ failed pid=%d exit=%d", + SystemWidePath, pid, + WEXITSTATUS(status)]); + } + appLogDebug([NSString + stringWithFormat:@"finished cleaning up prior %@", SystemWidePath]); + + // Copy bundle to /Applications + // We can't use mv as we may be denied if we're sandboxed + const char *cpTool = "/bin/cp"; + const char *cpArgs[] = {"-pR", src, [SystemWidePath UTF8String], NULL}; + appLogDebug([NSString stringWithFormat:@"running authorized cp -pR %s %@", + src, SystemWidePath]); + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + err = AuthorizationExecuteWithPrivileges(authRef, cpTool, + kAuthorizationFlagDefaults, + (char *const *)cpArgs, NULL); +#pragma clang diagnostic pop + + if (err != errAuthorizationSuccess) { + appLogInfo( + [NSString stringWithFormat:@"Failed to copy %s -> %@. err = %d", + src, SystemWidePath, err]); + AuthorizationFree(authRef, kAuthorizationFlagDestroyRights); + return NO; + } + + // Wait for the command to finish + pid = wait(&status); + appLogInfo([NSString stringWithFormat:@"cp -pR %s %@ - pid=%d exit=%d", src, + SystemWidePath, pid, + WEXITSTATUS(status)]); + + if (pid == -1 || !WIFEXITED(status) || WEXITSTATUS(status)) { + AuthorizationFree(authRef, kAuthorizationFlagDestroyRights); + return NO; + } + + // Copy worked, now best effort try to clean up the source bundle + // Try file manager, then authorized rm -rf + NSFileManager *fileManager = [NSFileManager defaultManager]; + NSString *bundlePath = @(src); + NSError *removeError = nil; + err = [fileManager trashItemAtURL:[NSURL fileURLWithPath:bundlePath] + resultingItemURL:nil + error:&removeError]; + if (removeError) { + appLogInfo( + [NSString stringWithFormat:@"unable to clean up now stale " + @"bundle via NSFileManager %@: %@", + bundlePath, removeError]); + const char *rm2Args[] = {"-rf", src, NULL}; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + err = AuthorizationExecuteWithPrivileges(authRef, rmTool, + kAuthorizationFlagDefaults, + (char *const *)rm2Args, NULL); +#pragma clang diagnostic pop + if (err != errAuthorizationSuccess) { + appLogInfo([NSString + stringWithFormat:@"Failed to remove existing %s. err = %d", src, + err]); + } else { + // wait for the command to finish + pid = wait(&status); + appLogInfo([NSString stringWithFormat:@"rm of %s pid=%d exit=%d", + src, pid, + WEXITSTATUS(status)]); + if (pid == -1 || !WIFEXITED(status) || WEXITSTATUS(status)) { + appLogInfo([NSString + stringWithFormat:@"rm of %s failed pid=%d exit=%d", src, + pid, WEXITSTATUS(status)]); + } else { + appLogDebug([NSString + stringWithFormat:@"finished cleaning up %s", src]); + } + } + } + AuthorizationFree(authRef, kAuthorizationFlagDestroyRights); + return YES; +} + +enum AppMove askToMoveToApplications() { + NSAppleEventDescriptor *evt = + [[NSAppleEventManager sharedAppleEventManager] currentAppleEvent]; + if (!evt || [evt eventID] != kAEOpenApplication) { + // This scenario triggers if we were launched from a double click, + // or the CLI spawns the app via open -a Ollama.app + appLogDebug([NSString + stringWithFormat:@"launched from double click or open -a"]); + } + NSAppleEventDescriptor *prop = + [evt paramDescriptorForKeyword:keyAEPropData]; + if (prop && [prop enumCodeValue] == keyAELaunchedAsLogInItem) { + // For a login session launch, we don't want to prompt for moving if + // the user opted out + appLogDebug([NSString stringWithFormat:@"launched from login"]); + return LoginSession; + } + pid_t pid = getpid(); + NSString *bundlePath = [[NSBundle mainBundle] bundlePath]; + appLogInfo(@"asking to move to system wide location"); + + NSAlert *alert = [[NSAlert alloc] init]; + [alert setMessageText:@"Move to Applications?"]; + [alert setInformativeText: + @"Ollama works best when run from the Applications directory."]; + [alert addButtonWithTitle:@"Move to Applications"]; + [alert addButtonWithTitle:@"Don't move"]; + + [NSApp activateIgnoringOtherApps:YES]; + + if ([alert runModal] != NSAlertFirstButtonReturn) { + appLogInfo([NSString + stringWithFormat:@"user rejected moving to /Applications"]); + return UserDeclinedMove; + } + + // move to applications + if (!moveToApplications([bundlePath UTF8String])) { + if (!moveToApplicationsWithAuthorization([bundlePath UTF8String])) { + appLogInfo([NSString + stringWithFormat:@"unable to move with authorization"]); + return PermissionDenied; + } + } + + appLogInfo([NSString + stringWithFormat:@"Launching %@ from PID=%d", SystemWidePath, pid]); + NSError *error = nil; + NSWorkspace *workspace = [NSWorkspace sharedWorkspace]; +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + [workspace launchApplicationAtURL:[NSURL fileURLWithPath:SystemWidePath] + options:NSWorkspaceLaunchNewInstance | + NSWorkspaceLaunchDefault + configuration:@{} + error:&error]; + return MoveCompleted; +} + +void launchApp(const char *appPath) { + pid_t pid = getpid(); + appLogInfo([NSString + stringWithFormat:@"Launching %@ from PID=%d", @(appPath), pid]); + NSError *error = nil; + NSWorkspace *workspace = [NSWorkspace sharedWorkspace]; +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + [workspace launchApplicationAtURL:[NSURL fileURLWithPath:@(appPath)] + options:NSWorkspaceLaunchNewInstance | + NSWorkspaceLaunchDefault + configuration:@{} + error:&error]; +} + +int installSymlink(const char *cliPath) { + NSString *linkPath = @"/usr/local/bin/ollama"; + NSString *dirPath = @"/usr/local/bin"; + NSError *error = nil; + + NSFileManager *fileManager = [NSFileManager defaultManager]; + NSString *symlinkPath = + [fileManager destinationOfSymbolicLinkAtPath:linkPath error:&error]; + NSString *resPath = [NSString stringWithUTF8String:cliPath]; + + // if the symlink already exists and points to the right place, don't + // prompt + if ([symlinkPath isEqualToString:resPath]) { + appLogDebug( + @"symbolic link already exists and points to the right place"); + return 0; + } + + // Get authorization once for both operations + AuthorizationRef authRef = getSymlinkAuthorization(); + if (authRef == NULL) { + return NO; + } + + // Check if /usr/local/bin directory exists, create it if it doesn't + BOOL isDirectory; + if (![fileManager fileExistsAtPath:dirPath isDirectory:&isDirectory] || !isDirectory) { + appLogInfo(@"/usr/local/bin directory does not exist, creating it"); + + const char *mkdirTool = "/bin/mkdir"; + const char *mkdirArgs[] = {"-p", [dirPath UTF8String], NULL}; + +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + OSStatus err = AuthorizationExecuteWithPrivileges( + authRef, mkdirTool, kAuthorizationFlagDefaults, (char *const *)mkdirArgs, + NULL); + if (err != errAuthorizationSuccess) { + appLogInfo(@"Failed to create /usr/local/bin directory"); + AuthorizationFree(authRef, kAuthorizationFlagDestroyRights); + return -1; + } + + // Wait for mkdir to complete + int status; + wait(&status); + } + + // Create the symlink using the same authorization + const char *toolPath = "/bin/ln"; + const char *args[] = {"-s", "-F", [resPath UTF8String], + "/usr/local/bin/ollama", NULL}; + FILE *pipe = NULL; + +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + OSStatus err = AuthorizationExecuteWithPrivileges( + authRef, toolPath, kAuthorizationFlagDefaults, (char *const *)args, + &pipe); + if (err != errAuthorizationSuccess) { + appLogInfo(@"Failed to create symlink"); + AuthorizationFree(authRef, kAuthorizationFlagDestroyRights); + return -1; + } + + AuthorizationFree(authRef, kAuthorizationFlagDestroyRights); + return 0; +} + +void updateAvailable() { + dispatch_async(dispatch_get_main_queue(), ^{ + [appDelegate showUpdateAvailable]; + }); +} + +void quit() { + dispatch_async(dispatch_get_main_queue(), ^{ + [appDelegate quit]; + }); +} + +void uiRequest(char *path) { + NSString *p = [NSString stringWithFormat:@"%s", path]; + appLogInfo([NSString stringWithFormat:@"XXX UI request for URL: %@", p]); + dispatch_async(dispatch_get_main_queue(), ^{ + [appDelegate uiRequest:p]; + }); +} + +void registerSelfAsLoginItem(bool firstTimeRun) { + dispatch_async(dispatch_get_main_queue(), ^{ + [appDelegate registerSelfAsLoginItem:firstTimeRun]; + }); +} + +void unregisterSelfFromLoginItem() { + dispatch_async(dispatch_get_main_queue(), ^{ + [appDelegate unregisterSelfFromLoginItem]; + }); +} + +static WKWebView *FindWKWebView(NSView *root) { + if ([root isKindOfClass:[WKWebView class]]) { + return (WKWebView *)root; + } + for (NSView *child in root.subviews) { + WKWebView *found = FindWKWebView(child); + if (found) { + return found; + } + } + return nil; +} + +void setWindowDelegate(void* window) { + NSWindow *w = (__bridge NSWindow *)window; + [w setDelegate:appDelegate]; + WKWebView *webView = FindWKWebView(w.contentView); + if (webView) { + webView.navigationDelegate = appDelegate; + webView.UIDelegate = appDelegate; + } +} + +void hideWindow(uintptr_t wndPtr) { + NSWindow *w = (__bridge NSWindow *)wndPtr; + [NSApp setActivationPolicy:NSApplicationActivationPolicyAccessory]; + [w orderOut:nil]; +} + +void showWindow(uintptr_t wndPtr) { + NSWindow *w = (__bridge NSWindow *)wndPtr; + + [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular]; + + dispatch_async(dispatch_get_main_queue(), ^{ + [NSApp unhide:nil]; + [NSApp activateIgnoringOtherApps:YES]; + [w makeKeyAndOrderFront:nil]; + }); +} + +void styleWindow(uintptr_t wndPtr) { + NSWindow *w = (__bridge NSWindow *)wndPtr; + if (!w) return; + + // Define the desired style mask + NSWindowStyleMask desiredStyleMask = NSWindowStyleMaskTitled | + NSWindowStyleMaskClosable | + NSWindowStyleMaskMiniaturizable | + NSWindowStyleMaskResizable | + NSWindowStyleMaskFullSizeContentView | + NSWindowStyleMaskUnifiedTitleAndToolbar; + + if (!(w.styleMask & NSWindowStyleMaskFullScreen)) { + w.styleMask = desiredStyleMask; + } + + if (w.toolbar == nil) { + NSToolbar *tb = [[NSToolbar alloc] initWithIdentifier:@"OllamaToolbar"]; + tb.displayMode = NSToolbarDisplayModeIconOnly; + tb.showsBaselineSeparator = NO; + w.toolbar = tb; + } + + w.titleVisibility = NSWindowTitleHidden; + w.titlebarAppearsTransparent = YES; + w.toolbarStyle = NSWindowToolbarStyleUnified; + w.movableByWindowBackground = NO; + w.hasShadow = YES; + + NSView *cv = w.contentView; + cv.wantsLayer = YES; + CALayer *L = cv.layer; + L.cornerRadius = 0.0; + L.masksToBounds = NO; + L.borderColor = nil; + L.borderWidth = 0.0; +} + +void drag(uintptr_t wndPtr) { + NSWindow *w = (__bridge NSWindow *)wndPtr; + if (!w) return; + NSPoint mouseLoc = [NSEvent mouseLocation]; + NSPoint locInWindow = [w convertPointFromScreen:mouseLoc]; + + NSEvent *e = [NSEvent mouseEventWithType:NSEventTypeLeftMouseDown + location:locInWindow + modifierFlags:0 + timestamp:NSTimeIntervalSince1970 + windowNumber:[w windowNumber] + context:nil + eventNumber:0 + clickCount:1 + pressure:1.0]; + [w performWindowDragWithEvent:e]; +} + +void doubleClick(uintptr_t wndPtr) { + NSWindow *w = (__bridge NSWindow *)wndPtr; + if (!w) return; + + // Respect the user's Dock preference + NSString *action = + [[NSUserDefaults standardUserDefaults] stringForKey:@"AppleActionOnDoubleClick"]; + + if ([action isEqualToString:@"Minimize"]) { + [w performMiniaturize:nil]; + } else { + [w performZoom:nil]; + } +} diff --git a/app/cmd/app/app_windows.go b/app/cmd/app/app_windows.go new file mode 100644 index 000000000..51cf1f923 --- /dev/null +++ b/app/cmd/app/app_windows.go @@ -0,0 +1,439 @@ +//go:build windows || darwin + +package main + +import ( + "errors" + "fmt" + "io" + "log" + "log/slog" + "os" + "os/exec" + "os/signal" + "path/filepath" + "runtime" + "strings" + "syscall" + "unsafe" + + "github.com/ollama/ollama/app/updater" + "github.com/ollama/ollama/app/version" + "github.com/ollama/ollama/app/wintray" + "golang.org/x/sys/windows" +) + +var ( + u32 = windows.NewLazySystemDLL("User32.dll") + pBringWindowToTop = u32.NewProc("BringWindowToTop") + pShowWindow = u32.NewProc("ShowWindow") + pSendMessage = u32.NewProc("SendMessageA") + pGetSystemMetrics = u32.NewProc("GetSystemMetrics") + pGetWindowRect = u32.NewProc("GetWindowRect") + pSetWindowPos = u32.NewProc("SetWindowPos") + pSetForegroundWindow = u32.NewProc("SetForegroundWindow") + pSetActiveWindow = u32.NewProc("SetActiveWindow") + pIsIconic = u32.NewProc("IsIconic") + + appPath = filepath.Join(os.Getenv("LOCALAPPDATA"), "Programs", "Ollama") + appLogPath = filepath.Join(os.Getenv("LOCALAPPDATA"), "Ollama", "app.log") + startupShortcut = filepath.Join(os.Getenv("APPDATA"), "Microsoft", "Windows", "Start Menu", "Programs", "Startup", "Ollama.lnk") + ollamaPath string + DesktopAppName = "ollama app.exe" +) + +func init() { + // With alternate install location use executable location + exe, err := os.Executable() + if err != nil { + slog.Warn("error discovering executable directory", "error", err) + } else { + appPath = filepath.Dir(exe) + } + ollamaPath = filepath.Join(appPath, "ollama.exe") + + // Handle developer mode (go run ./cmd/app) + if _, err := os.Stat(ollamaPath); err != nil { + pwd, err := os.Getwd() + if err != nil { + slog.Warn("missing ollama.exe and failed to get pwd", "error", err) + return + } + distAppPath := filepath.Join(pwd, "dist", "windows-"+runtime.GOARCH) + distOllamaPath := filepath.Join(distAppPath, "ollama.exe") + if _, err := os.Stat(distOllamaPath); err == nil { + slog.Info("detected developer mode") + appPath = distAppPath + ollamaPath = distOllamaPath + } + } +} + +func maybeMoveAndRestart() appMove { + return 0 +} + +// handleExistingInstance checks for existing instances and optionally focuses them +func handleExistingInstance(startHidden bool) { + if wintray.CheckAndFocusExistingInstance(!startHidden) { + slog.Info("existing instance found, exiting") + os.Exit(0) + } +} + +func installSymlink() {} + +type appCallbacks struct { + t wintray.TrayCallbacks + shutdown func() +} + +var app = &appCallbacks{} + +func (ac *appCallbacks) UIRun(path string) { + wv.Run(path) +} + +func (*appCallbacks) UIShow() { + if wv.webview != nil { + showWindow(wv.webview.Window()) + } else { + wv.Run("/") + } +} + +func (*appCallbacks) UITerminate() { + wv.Terminate() +} + +func (*appCallbacks) UIRunning() bool { + return wv.IsRunning() +} + +func (app *appCallbacks) Quit() { + app.t.Quit() + wv.Terminate() +} + +// TODO - reconcile with above for consistency between mac/windows +func quit() { + wv.Terminate() +} + +func (app *appCallbacks) DoUpdate() { + // Safeguard in case we have requests in flight that need to drain... + slog.Info("Waiting for server to shutdown") + + app.shutdown() + + if err := updater.DoUpgrade(true); err != nil { + slog.Warn(fmt.Sprintf("upgrade attempt failed: %s", err)) + } +} + +// HandleURLScheme implements the URLSchemeHandler interface +func (app *appCallbacks) HandleURLScheme(urlScheme string) { + handleURLSchemeRequest(urlScheme) +} + +// handleURLSchemeRequest processes URL scheme requests from other instances +func handleURLSchemeRequest(urlScheme string) { + isConnect, uiPath, err := parseURLScheme(urlScheme) + if err != nil { + slog.Error("failed to parse URL scheme request", "url", urlScheme, "error", err) + return + } + + if isConnect { + handleConnectURLScheme() + } else { + sendUIRequestMessage(uiPath) + } +} + +func UpdateAvailable(ver string) error { + return app.t.UpdateAvailable(ver) +} + +func osRun(shutdown func(), hasCompletedFirstRun, startHidden bool) { + var err error + app.shutdown = shutdown + app.t, err = wintray.NewTray(app) + if err != nil { + log.Fatalf("Failed to start: %s", err) + } + + signals := make(chan os.Signal, 1) + signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM) + + // TODO - can this be generalized? + go func() { + <-signals + slog.Debug("shutting down due to signal") + app.t.Quit() + wv.Terminate() + }() + + // On windows, we run the final tasks in the main thread + // before starting the tray event loop. These final tasks + // may trigger the UI, and must do that from the main thread. + if !startHidden { + // Determine if the process was started from a shortcut + // ~\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup\Ollama + const STARTF_TITLEISLINKNAME = 0x00000800 + var info windows.StartupInfo + if err := windows.GetStartupInfo(&info); err != nil { + slog.Debug("unable to retrieve startup info", "error", err) + } else if info.Flags&STARTF_TITLEISLINKNAME == STARTF_TITLEISLINKNAME { + linkPath := windows.UTF16PtrToString(info.Title) + if strings.Contains(linkPath, "Startup") { + startHidden = true + } + } + } + if startHidden { + startHiddenTasks() + } else { + ptr := wv.Run("/") + + // Set the window icon using the tray icon + if ptr != nil { + iconHandle := app.t.GetIconHandle() + if iconHandle != 0 { + hwnd := uintptr(ptr) + const ICON_SMALL = 0 + const ICON_BIG = 1 + const WM_SETICON = 0x0080 + + pSendMessage.Call(hwnd, uintptr(WM_SETICON), uintptr(ICON_SMALL), uintptr(iconHandle)) + pSendMessage.Call(hwnd, uintptr(WM_SETICON), uintptr(ICON_BIG), uintptr(iconHandle)) + } + } + + centerWindow(ptr) + } + + if !hasCompletedFirstRun { + // Only create the login shortcut on first start + // so we can respect users deletion of the link + err = createLoginShortcut() + if err != nil { + slog.Warn("unable to create login shortcut", "error", err) + } + } + + app.t.TrayRun() // This will block the main thread +} + +func createLoginShortcut() error { + // The installer lays down a shortcut for us so we can copy it without + // having to resort to calling COM APIs to establish the shortcut + shortcutOrigin := filepath.Join(appPath, "lib", "Ollama.lnk") + + _, err := os.Stat(startupShortcut) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + in, err := os.Open(shortcutOrigin) + if err != nil { + return fmt.Errorf("unable to open shortcut %s : %w", shortcutOrigin, err) + } + defer in.Close() + out, err := os.Create(startupShortcut) + if err != nil { + return fmt.Errorf("unable to open startup link %s : %w", startupShortcut, err) + } + defer out.Close() + _, err = io.Copy(out, in) + if err != nil { + return fmt.Errorf("unable to copy shortcut %s : %w", startupShortcut, err) + } + err = out.Sync() + if err != nil { + return fmt.Errorf("unable to sync shortcut %s : %w", startupShortcut, err) + } + slog.Info("Created Startup shortcut", "shortcut", startupShortcut) + } else { + slog.Warn("unexpected error looking up Startup shortcut", "error", err) + } + } else { + slog.Debug("Startup link already exists", "shortcut", startupShortcut) + } + return nil +} + +// Send a request to the main app thread to load a UI page +func sendUIRequestMessage(path string) { + wintray.SendUIRequestMessage(path) +} + +func LaunchNewApp() { +} + +func logStartup() { + slog.Info("starting Ollama", "app", appPath, "version", version.Version, "OS", updater.UserAgentOS) +} + +const ( + SW_HIDE = 0 // Hides the window + SW_SHOW = 5 // Shows window in its current size/position + SW_SHOWNA = 8 // Shows without activating + SW_MINIMIZE = 6 // Minimizes the window + SW_RESTORE = 9 // Restores to previous size/position + SW_SHOWDEFAULT = 10 // Sets show state based on program state + SM_CXSCREEN = 0 + SM_CYSCREEN = 1 + HWND_TOP = 0 + SWP_NOSIZE = 0x0001 + SWP_NOMOVE = 0x0002 + SWP_NOZORDER = 0x0004 + SWP_SHOWWINDOW = 0x0040 + + // Menu constants + MF_STRING = 0x00000000 + MF_SEPARATOR = 0x00000800 + MF_GRAYED = 0x00000001 + TPM_RETURNCMD = 0x0100 +) + +// POINT structure for cursor position +type POINT struct { + X int32 + Y int32 +} + +// Rect structure for GetWindowRect +type Rect struct { + Left int32 + Top int32 + Right int32 + Bottom int32 +} + +func centerWindow(ptr unsafe.Pointer) { + hwnd := uintptr(ptr) + if hwnd == 0 { + return + } + + var rect Rect + pGetWindowRect.Call(hwnd, uintptr(unsafe.Pointer(&rect))) + + screenWidth, _, _ := pGetSystemMetrics.Call(uintptr(SM_CXSCREEN)) + screenHeight, _, _ := pGetSystemMetrics.Call(uintptr(SM_CYSCREEN)) + + windowWidth := rect.Right - rect.Left + windowHeight := rect.Bottom - rect.Top + + x := (int32(screenWidth) - windowWidth) / 2 + y := (int32(screenHeight) - windowHeight) / 2 + + // Ensure the window is not positioned off-screen + if x < 0 { + x = 0 + } + if y < 0 { + y = 0 + } + + pSetWindowPos.Call( + hwnd, + uintptr(HWND_TOP), + uintptr(x), + uintptr(y), + uintptr(windowWidth), // Keep original width + uintptr(windowHeight), // Keep original height + uintptr(SWP_SHOWWINDOW), + ) +} + +func showWindow(ptr unsafe.Pointer) { + hwnd := uintptr(ptr) + if hwnd != 0 { + iconHandle := app.t.GetIconHandle() + if iconHandle != 0 { + const ICON_SMALL = 0 + const ICON_BIG = 1 + const WM_SETICON = 0x0080 + + pSendMessage.Call(hwnd, uintptr(WM_SETICON), uintptr(ICON_SMALL), uintptr(iconHandle)) + pSendMessage.Call(hwnd, uintptr(WM_SETICON), uintptr(ICON_BIG), uintptr(iconHandle)) + } + + // Check if window is minimized + isMinimized, _, _ := pIsIconic.Call(hwnd) + if isMinimized != 0 { + // Restore the window if it's minimized + pShowWindow.Call(hwnd, uintptr(SW_RESTORE)) + } + + // Show the window + pShowWindow.Call(hwnd, uintptr(SW_SHOW)) + + // Bring window to top + pBringWindowToTop.Call(hwnd) + + // Force window to foreground + pSetForegroundWindow.Call(hwnd) + + // Make it the active window + pSetActiveWindow.Call(hwnd) + + // Ensure window is positioned on top + pSetWindowPos.Call( + hwnd, + uintptr(HWND_TOP), + 0, 0, 0, 0, + uintptr(SWP_NOSIZE|SWP_NOMOVE|SWP_SHOWWINDOW), + ) + } +} + +// HideWindow hides the application window +func hideWindow(ptr unsafe.Pointer) { + hwnd := uintptr(ptr) + if hwnd != 0 { + pShowWindow.Call( + hwnd, + uintptr(SW_HIDE), + ) + } +} + +func runInBackground() { + exe, err := os.Executable() + if err != nil { + slog.Error("failed to get executable path", "error", err) + os.Exit(1) + } + cmd := exec.Command(exe, "hidden") + if cmd != nil { + err = cmd.Run() + if err != nil { + slog.Error("failed to run Ollama", "exe", exe, "error", err) + os.Exit(1) + } + } else { + slog.Error("failed to start Ollama", "exe", exe) + os.Exit(1) + } +} + +func drag(ptr unsafe.Pointer) {} + +func doubleClick(ptr unsafe.Pointer) {} + +// checkAndHandleExistingInstance checks if another instance is running and sends the URL to it +func checkAndHandleExistingInstance(urlSchemeRequest string) bool { + if urlSchemeRequest == "" { + return false + } + + // Try to send URL to existing instance using wintray messaging + if wintray.CheckAndSendToExistingInstance(urlSchemeRequest) { + os.Exit(0) + return true + } + + // No existing instance, we'll handle it ourselves + return false +} diff --git a/app/cmd/app/menu.h b/app/cmd/app/menu.h new file mode 100644 index 000000000..1ff94cbbd --- /dev/null +++ b/app/cmd/app/menu.h @@ -0,0 +1,27 @@ +#ifndef MENU_H +#define MENU_H + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct +{ + char *label; + int enabled; + int separator; +} menuItem; + +// TODO (jmorganca): these need to be forward declared in the webview.h file +// for now but ideally they should be in this header file on windows too +#ifndef WIN32 +int menu_get_item_count(); +void *menu_get_items(); +void menu_handle_selection(char *item); +#endif + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/app/cmd/app/webview.go b/app/cmd/app/webview.go new file mode 100644 index 000000000..983fba9d4 --- /dev/null +++ b/app/cmd/app/webview.go @@ -0,0 +1,528 @@ +//go:build windows || darwin + +package main + +// #include "menu.h" +import "C" + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "log/slog" + "net/http" + "os" + "path/filepath" + "runtime" + "strings" + "sync" + "time" + "unsafe" + + "github.com/ollama/ollama/app/dialog" + "github.com/ollama/ollama/app/store" + "github.com/ollama/ollama/app/webview" +) + +type Webview struct { + port int + token string + webview webview.WebView + mutex sync.Mutex + + Store *store.Store +} + +// Run initializes the webview and starts its event loop. +// Note: this must be called from the primary app thread +// This returns the OS native window handle to the caller +func (w *Webview) Run(path string) unsafe.Pointer { + var url string + if devMode { + // In development mode, use the local dev server + url = fmt.Sprintf("http://localhost:5173%s", path) + } else { + url = fmt.Sprintf("http://127.0.0.1:%d%s", w.port, path) + } + w.mutex.Lock() + defer w.mutex.Unlock() + + if w.webview == nil { + // Note: turning on debug on macos throws errors but is marginally functional for debugging + // TODO (jmorganca): we should pre-create the window and then provide it here to + // webview so we can hide it from the start and make other modifications + wv := webview.New(debug) + // start the window hidden + hideWindow(wv.Window()) + wv.SetTitle("Ollama") + + // TODO (jmorganca): this isn't working yet since it needs to be set + // on the first page load, ideally in an interstitial page like `/token` + // that exists only to set the cookie and redirect to / + // wv.Init(fmt.Sprintf(`document.cookie = "token=%s; path=/"`, w.token)) + init := ` + // Disable reload + document.addEventListener('keydown', function(e) { + if ((e.ctrlKey || e.metaKey) && e.key === 'r') { + e.preventDefault(); + return false; + } + }); + + // Prevent back/forward navigation + window.addEventListener('popstate', function(e) { + e.preventDefault(); + history.pushState(null, '', window.location.pathname); + return false; + }); + + // Clear history on load + window.addEventListener('load', function() { + history.pushState(null, '', window.location.pathname); + window.history.replaceState(null, '', window.location.pathname); + }); + + // Set token cookie + document.cookie = "token=` + w.token + `; path=/"; + ` + // Windows-specific scrollbar styling + if runtime.GOOS == "windows" { + init += ` + // Fix scrollbar styling for Edge WebView2 on Windows only + function updateScrollbarStyles() { + const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches; + const existingStyle = document.getElementById('scrollbar-style'); + if (existingStyle) existingStyle.remove(); + + const style = document.createElement('style'); + style.id = 'scrollbar-style'; + + if (isDark) { + style.textContent = ` + "`" + ` + ::-webkit-scrollbar { width: 6px !important; height: 6px !important; } + ::-webkit-scrollbar-track { background: #1a1a1a !important; } + ::-webkit-scrollbar-thumb { background: #404040 !important; border-radius: 6px !important; } + ::-webkit-scrollbar-thumb:hover { background: #505050 !important; } + ::-webkit-scrollbar-corner { background: #1a1a1a !important; } + ::-webkit-scrollbar-button { + background: transparent !important; + border: none !important; + width: 0px !important; + height: 0px !important; + margin: 0 !important; + padding: 0 !important; + } + ::-webkit-scrollbar-button:vertical:start:decrement { + background: transparent !important; + height: 0px !important; + } + ::-webkit-scrollbar-button:vertical:end:increment { + background: transparent !important; + height: 0px !important; + } + ::-webkit-scrollbar-button:horizontal:start:decrement { + background: transparent !important; + width: 0px !important; + } + ::-webkit-scrollbar-button:horizontal:end:increment { + background: transparent !important; + width: 0px !important; + } + ` + "`" + `; + } else { + style.textContent = ` + "`" + ` + ::-webkit-scrollbar { width: 6px !important; height: 6px !important; } + ::-webkit-scrollbar-track { background: #f0f0f0 !important; } + ::-webkit-scrollbar-thumb { background: #c0c0c0 !important; border-radius: 6px !important; } + ::-webkit-scrollbar-thumb:hover { background: #a0a0a0 !important; } + ::-webkit-scrollbar-corner { background: #f0f0f0 !important; } + ::-webkit-scrollbar-button { + background: transparent !important; + border: none !important; + width: 0px !important; + height: 0px !important; + margin: 0 !important; + padding: 0 !important; + } + ::-webkit-scrollbar-button:vertical:start:decrement { + background: transparent !important; + height: 0px !important; + } + ::-webkit-scrollbar-button:vertical:end:increment { + background: transparent !important; + height: 0px !important; + } + ::-webkit-scrollbar-button:horizontal:start:decrement { + background: transparent !important; + width: 0px !important; + } + ::-webkit-scrollbar-button:horizontal:end:increment { + background: transparent !important; + width: 0px !important; + } + ` + "`" + `; + } + document.head.appendChild(style); + } + + window.addEventListener('load', updateScrollbarStyles); + window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', updateScrollbarStyles); + ` + } + // on windows make ctrl+n open new chat + // TODO (jmorganca): later we should use proper accelerators + // once we introduce a native menu for the window + // this is only used on windows since macOS uses the proper accelerators + if runtime.GOOS == "windows" { + init += ` + document.addEventListener('keydown', function(e) { + if ((e.ctrlKey || e.metaKey) && e.key === 'n') { + e.preventDefault(); + // Use the existing navigation method + history.pushState({}, '', '/c/new'); + window.dispatchEvent(new PopStateEvent('popstate')); + return false; + } + }); + ` + } + + init += ` + window.OLLAMA_WEBSEARCH = true; + ` + + wv.Init(init) + + // Add keyboard handler for zoom + wv.Init(` + window.addEventListener('keydown', function(e) { + // CMD/Ctrl + Plus/Equals (zoom in) + if ((e.metaKey || e.ctrlKey) && (e.key === '+' || e.key === '=')) { + e.preventDefault(); + window.zoomIn && window.zoomIn(); + return false; + } + + // CMD/Ctrl + Minus (zoom out) + if ((e.metaKey || e.ctrlKey) && e.key === '-') { + e.preventDefault(); + window.zoomOut && window.zoomOut(); + return false; + } + + // CMD/Ctrl + 0 (reset zoom) + if ((e.metaKey || e.ctrlKey) && e.key === '0') { + e.preventDefault(); + window.zoomReset && window.zoomReset(); + return false; + } + }, true); + `) + + wv.Bind("zoomIn", func() { + current := wv.GetZoom() + wv.SetZoom(current + 0.1) + }) + + wv.Bind("zoomOut", func() { + current := wv.GetZoom() + wv.SetZoom(current - 0.1) + }) + + wv.Bind("zoomReset", func() { + wv.SetZoom(1.0) + }) + + wv.Bind("ready", func() { + showWindow(wv.Window()) + }) + + wv.Bind("close", func() { + hideWindow(wv.Window()) + }) + + // Webviews do not allow access to the file system by default, so we need to + // bind file system operations here + wv.Bind("selectModelsDirectory", func() { + go func() { + // Helper function to call the JavaScript callback with data or null + callCallback := func(data interface{}) { + dataJSON, _ := json.Marshal(data) + wv.Dispatch(func() { + wv.Eval(fmt.Sprintf("window.__selectModelsDirectoryCallback && window.__selectModelsDirectoryCallback(%s)", dataJSON)) + }) + } + + directory, err := dialog.Directory().Title("Select Model Directory").ShowHidden(true).Browse() + if err != nil { + slog.Debug("Directory selection cancelled or failed", "error", err) + callCallback(nil) + return + } + slog.Debug("Directory selected", "path", directory) + callCallback(directory) + }() + }) + + // Bind selectFiles function for selecting multiple files at once + wv.Bind("selectFiles", func() { + go func() { + // Helper function to call the JavaScript callback with data or null + callCallback := func(data interface{}) { + dataJSON, _ := json.Marshal(data) + wv.Dispatch(func() { + wv.Eval(fmt.Sprintf("window.__selectFilesCallback && window.__selectFilesCallback(%s)", dataJSON)) + }) + } + + // Define allowed extensions for native dialog filtering + textExts := []string{ + "pdf", "docx", "txt", "md", "csv", "json", "xml", "html", "htm", + "js", "jsx", "ts", "tsx", "py", "java", "cpp", "c", "cc", "h", "cs", "php", "rb", + "go", "rs", "swift", "kt", "scala", "sh", "bat", "yaml", "yml", "toml", "ini", + "cfg", "conf", "log", "rtf", + } + imageExts := []string{"png", "jpg", "jpeg"} + allowedExts := append(textExts, imageExts...) + + // Use native multiple file selection with extension filtering + filenames, err := dialog.File(). + Filter("Supported Files", allowedExts...). + Title("Select Files"). + LoadMultiple() + if err != nil { + slog.Debug("Multiple file selection cancelled or failed", "error", err) + callCallback(nil) + return + } + + if len(filenames) == 0 { + callCallback(nil) + return + } + + var files []map[string]string + maxFileSize := int64(10 * 1024 * 1024) // 10MB + + for _, filename := range filenames { + // Check file extension (double-check after native dialog filtering) + ext := strings.ToLower(strings.TrimPrefix(filepath.Ext(filename), ".")) + validExt := false + for _, allowedExt := range allowedExts { + if ext == allowedExt { + validExt = true + break + } + } + if !validExt { + slog.Warn("file extension not allowed, skipping", "filename", filepath.Base(filename), "extension", ext) + continue + } + + // Check file size before reading (pre-filter large files) + fileStat, err := os.Stat(filename) + if err != nil { + slog.Error("failed to get file info", "error", err, "filename", filename) + continue + } + + if fileStat.Size() > maxFileSize { + slog.Warn("file too large, skipping", "filename", filepath.Base(filename), "size", fileStat.Size()) + continue + } + + fileBytes, err := os.ReadFile(filename) + if err != nil { + slog.Error("failed to read file", "error", err, "filename", filename) + continue + } + + mimeType := http.DetectContentType(fileBytes) + dataURL := fmt.Sprintf("data:%s;base64,%s", mimeType, base64.StdEncoding.EncodeToString(fileBytes)) + + fileResult := map[string]string{ + "filename": filepath.Base(filename), + "path": filename, + "dataURL": dataURL, + } + + files = append(files, fileResult) + } + + if len(files) == 0 { + callCallback(nil) + } else { + callCallback(files) + } + }() + }) + + wv.Bind("drag", func() { + wv.Dispatch(func() { + drag(wv.Window()) + }) + }) + + wv.Bind("doubleClick", func() { + wv.Dispatch(func() { + doubleClick(wv.Window()) + }) + }) + + // Add binding for working directory selection + wv.Bind("selectWorkingDirectory", func() { + go func() { + // Helper function to call the JavaScript callback with data or null + callCallback := func(data interface{}) { + dataJSON, _ := json.Marshal(data) + wv.Dispatch(func() { + wv.Eval(fmt.Sprintf("window.__selectWorkingDirectoryCallback && window.__selectWorkingDirectoryCallback(%s)", dataJSON)) + }) + } + + directory, err := dialog.Directory().Title("Select Working Directory").ShowHidden(true).Browse() + if err != nil { + slog.Debug("Directory selection cancelled or failed", "error", err) + callCallback(nil) + return + } + slog.Debug("Directory selected", "path", directory) + callCallback(directory) + }() + }) + + wv.Bind("setContextMenuItems", func(items []map[string]interface{}) error { + menuMutex.Lock() + defer menuMutex.Unlock() + + if len(menuItems) > 0 { + pinner.Unpin() + } + + menuItems = nil + for _, item := range items { + menuItem := C.menuItem{ + label: C.CString(item["label"].(string)), + enabled: 0, + separator: 0, + } + + if item["enabled"] != nil { + menuItem.enabled = 1 + } + + if item["separator"] != nil { + menuItem.separator = 1 + } + menuItems = append(menuItems, menuItem) + } + return nil + }) + + // Debounce resize events + var resizeTimer *time.Timer + var resizeMutex sync.Mutex + + wv.Bind("resize", func(width, height int) { + if w.Store != nil { + resizeMutex.Lock() + if resizeTimer != nil { + resizeTimer.Stop() + } + resizeTimer = time.AfterFunc(100*time.Millisecond, func() { + err := w.Store.SetWindowSize(width, height) + if err != nil { + slog.Error("failed to set window size", "error", err) + } + }) + resizeMutex.Unlock() + } + }) + + // On Darwin, we can't have 2 threads both running global event loops + // but on Windows, the event loops are tied to the window, so we're + // able to run in both the tray and webview + if runtime.GOOS != "darwin" { + slog.Debug("starting webview event loop") + go func() { + wv.Run() + slog.Debug("webview event loop exited") + }() + } + + if w.Store != nil { + width, height, err := w.Store.WindowSize() + if err != nil { + slog.Error("failed to get window size", "error", err) + } + if width > 0 && height > 0 { + wv.SetSize(width, height, webview.HintNone) + } else { + wv.SetSize(800, 600, webview.HintNone) + } + } + wv.SetSize(800, 600, webview.HintMin) + + w.webview = wv + w.webview.Navigate(url) + } else { + w.webview.Eval(fmt.Sprintf(` + history.pushState({}, '', '%s'); + `, path)) + showWindow(w.webview.Window()) + } + + return w.webview.Window() +} + +func (w *Webview) Terminate() { + w.mutex.Lock() + if w.webview == nil { + w.mutex.Unlock() + return + } + + wv := w.webview + w.webview = nil + w.mutex.Unlock() + wv.Terminate() + wv.Destroy() +} + +func (w *Webview) IsRunning() bool { + w.mutex.Lock() + defer w.mutex.Unlock() + return w.webview != nil +} + +var ( + menuItems []C.menuItem + menuMutex sync.RWMutex + pinner runtime.Pinner +) + +//export menu_get_item_count +func menu_get_item_count() C.int { + menuMutex.RLock() + defer menuMutex.RUnlock() + return C.int(len(menuItems)) +} + +//export menu_get_items +func menu_get_items() unsafe.Pointer { + menuMutex.RLock() + defer menuMutex.RUnlock() + + if len(menuItems) == 0 { + return nil + } + + // Return pointer to the slice data + pinner.Pin(&menuItems[0]) + return unsafe.Pointer(&menuItems[0]) +} + +//export menu_handle_selection +func menu_handle_selection(item *C.char) { + wv.webview.Eval(fmt.Sprintf("window.handleContextMenuResult('%s')", C.GoString(item))) +} diff --git a/app/cmd/squirrel/Info.plist b/app/cmd/squirrel/Info.plist new file mode 100644 index 000000000..14de76d23 --- /dev/null +++ b/app/cmd/squirrel/Info.plist @@ -0,0 +1,40 @@ + + + + + CFBundleDevelopmentRegion + English + CFBundleExecutable + Squirrel + CFBundleIconFile + + CFBundleIdentifier + com.github.Squirrel + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + Squirrel + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + DTCompiler + com.apple.compilers.llvm.clang.1_0 + DTSDKBuild + 22E245 + DTSDKName + macosx13.3 + DTXcode + 1431 + DTXcodeBuild + 14E300c + NSHumanReadableCopyright + Copyright © 2013 GitHub. All rights reserved. + NSPrincipalClass + + + \ No newline at end of file diff --git a/app/darwin/Ollama.app/Contents/Info.plist b/app/darwin/Ollama.app/Contents/Info.plist new file mode 100644 index 000000000..5d9b4ca66 --- /dev/null +++ b/app/darwin/Ollama.app/Contents/Info.plist @@ -0,0 +1,51 @@ + + + + + CFBundleDisplayName + Ollama + CFBundleExecutable + Ollama + CFBundleIconFile + icon.icns + CFBundleIdentifier + com.electron.ollama + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + Ollama + CFBundlePackageType + APPL + CFBundleShortVersionString + 0.0.0 + CFBundleVersion + 0.0.0 + DTCompiler + com.apple.compilers.llvm.clang.1_0 + DTSDKBuild + 22E245 + DTSDKName + macosx14.0 + DTXcode + 1431 + DTXcodeBuild + 14E300c + LSApplicationCategoryType + public.app-category.developer-tools + LSMinimumSystemVersion + 14.0 + LSUIElement + + CFBundleURLTypes + + + CFBundleURLName + Ollama URL + CFBundleURLSchemes + + ollama + + + + + diff --git a/app/darwin/Ollama.app/Contents/Library/LaunchAgents/com.ollama.ollama.plist b/app/darwin/Ollama.app/Contents/Library/LaunchAgents/com.ollama.ollama.plist new file mode 100644 index 000000000..563f7bd92 --- /dev/null +++ b/app/darwin/Ollama.app/Contents/Library/LaunchAgents/com.ollama.ollama.plist @@ -0,0 +1,25 @@ + + + + + Label + com.ollama.ollama + BundleProgram + Contents/Frameworks/Squirrel.framework/Versions/A/Squirrel + ProgramArguments + + Contents/Frameworks/Squirrel.framework/Versions/A/Squirrel + background + + RunAtLoad + + LimitLoadToSessionType + Aqua + POSIXSpawnType + Interactive + LSUIElement + + LSBackgroundOnly + + + \ No newline at end of file diff --git a/app/darwin/Ollama.app/Contents/Resources/icon.icns b/app/darwin/Ollama.app/Contents/Resources/icon.icns new file mode 100644 index 000000000..e25734dc9 Binary files /dev/null and b/app/darwin/Ollama.app/Contents/Resources/icon.icns differ diff --git a/app/darwin/Ollama.app/Contents/Resources/ollama.png b/app/darwin/Ollama.app/Contents/Resources/ollama.png new file mode 100644 index 000000000..8daa97607 Binary files /dev/null and b/app/darwin/Ollama.app/Contents/Resources/ollama.png differ diff --git a/app/darwin/Ollama.app/Contents/Resources/ollama@2x.png b/app/darwin/Ollama.app/Contents/Resources/ollama@2x.png new file mode 100644 index 000000000..d226d0f16 Binary files /dev/null and b/app/darwin/Ollama.app/Contents/Resources/ollama@2x.png differ diff --git a/app/darwin/Ollama.app/Contents/Resources/ollamaDark.png b/app/darwin/Ollama.app/Contents/Resources/ollamaDark.png new file mode 100644 index 000000000..34a8f898e Binary files /dev/null and b/app/darwin/Ollama.app/Contents/Resources/ollamaDark.png differ diff --git a/app/darwin/Ollama.app/Contents/Resources/ollamaDark@2x.png b/app/darwin/Ollama.app/Contents/Resources/ollamaDark@2x.png new file mode 100644 index 000000000..f2436e4d1 Binary files /dev/null and b/app/darwin/Ollama.app/Contents/Resources/ollamaDark@2x.png differ diff --git a/app/darwin/Ollama.app/Contents/Resources/ollamaUpdate.png b/app/darwin/Ollama.app/Contents/Resources/ollamaUpdate.png new file mode 100644 index 000000000..2e9aab35c Binary files /dev/null and b/app/darwin/Ollama.app/Contents/Resources/ollamaUpdate.png differ diff --git a/app/darwin/Ollama.app/Contents/Resources/ollamaUpdate@2x.png b/app/darwin/Ollama.app/Contents/Resources/ollamaUpdate@2x.png new file mode 100644 index 000000000..471fe130d Binary files /dev/null and b/app/darwin/Ollama.app/Contents/Resources/ollamaUpdate@2x.png differ diff --git a/app/darwin/Ollama.app/Contents/Resources/ollamaUpdateDark.png b/app/darwin/Ollama.app/Contents/Resources/ollamaUpdateDark.png new file mode 100644 index 000000000..b7f3fd619 Binary files /dev/null and b/app/darwin/Ollama.app/Contents/Resources/ollamaUpdateDark.png differ diff --git a/app/darwin/Ollama.app/Contents/Resources/ollamaUpdateDark@2x.png b/app/darwin/Ollama.app/Contents/Resources/ollamaUpdateDark@2x.png new file mode 100644 index 000000000..5672c1aa4 Binary files /dev/null and b/app/darwin/Ollama.app/Contents/Resources/ollamaUpdateDark@2x.png differ diff --git a/app/dialog/LICENSE b/app/dialog/LICENSE new file mode 100644 index 000000000..75bff9f67 --- /dev/null +++ b/app/dialog/LICENSE @@ -0,0 +1,15 @@ +ISC License + +Copyright (c) 2018, the dialog authors. + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. \ No newline at end of file diff --git a/app/dialog/cocoa/dlg.h b/app/dialog/cocoa/dlg.h new file mode 100644 index 000000000..302cd2e2b --- /dev/null +++ b/app/dialog/cocoa/dlg.h @@ -0,0 +1,43 @@ +#include + +typedef enum { + MSG_YESNO, + MSG_ERROR, + MSG_INFO, +} AlertStyle; + +typedef struct { + char* msg; + char* title; + AlertStyle style; +} AlertDlgParams; + +#define LOADDLG 0 +#define SAVEDLG 1 +#define DIRDLG 2 // browse for directory + +typedef struct { + int mode; /* which dialog style to invoke (see earlier defines) */ + char* buf; /* buffer to store selected file */ + int nbuf; /* number of bytes allocated at buf */ + char* title; /* title for dialog box (can be nil) */ + void** exts; /* list of valid extensions (elements actual type is NSString*) */ + int numext; /* number of items in exts */ + int relaxext; /* allow other extensions? */ + char* startDir; /* directory to start in (can be nil) */ + char* filename; /* default filename for dialog box (can be nil) */ + int showHidden; /* show hidden files? */ + int allowMultiple; /* allow multiple file selection? */ +} FileDlgParams; + +typedef enum { + DLG_OK, + DLG_CANCEL, + DLG_URLFAIL, +} DlgResult; + +DlgResult alertDlg(AlertDlgParams*); +DlgResult fileDlg(FileDlgParams*); + +void* NSStr(void* buf, int len); +void NSRelease(void* obj); diff --git a/app/dialog/cocoa/dlg.m b/app/dialog/cocoa/dlg.m new file mode 100644 index 000000000..7ee22a0d4 --- /dev/null +++ b/app/dialog/cocoa/dlg.m @@ -0,0 +1,195 @@ +#import +#include "dlg.h" +#include +#include + +void* NSStr(void* buf, int len) { + return (void*)[[NSString alloc] initWithBytes:buf length:len encoding:NSUTF8StringEncoding]; +} + +void checkActivationPolicy() { + NSApplicationActivationPolicy policy = [NSApp activationPolicy]; + // prohibited NSApp will not show the panel at all. + // It probably means that this is not run in a GUI app, that would set the policy on its own, + // but in a terminal app - setting it to accessory will allow dialogs to show + if (policy == NSApplicationActivationPolicyProhibited) { + [NSApp setActivationPolicy:NSApplicationActivationPolicyAccessory]; + } +} + +void NSRelease(void* obj) { + [(NSObject*)obj release]; +} + +@interface AlertDlg : NSObject { + AlertDlgParams* params; + DlgResult result; +} ++ (AlertDlg*)init:(AlertDlgParams*)params; +- (DlgResult)run; +@end + +DlgResult alertDlg(AlertDlgParams* params) { + return [[AlertDlg init:params] run]; +} + +@implementation AlertDlg ++ (AlertDlg*)init:(AlertDlgParams*)params { + AlertDlg* d = [AlertDlg alloc]; + d->params = params; + return d; +} + +- (DlgResult)run { + if(![NSThread isMainThread]) { + [self performSelectorOnMainThread:@selector(run) withObject:nil waitUntilDone:YES]; + return self->result; + } + NSAlert* alert = [[NSAlert alloc] init]; + if(self->params->title != nil) { + [[alert window] setTitle:[[NSString alloc] initWithUTF8String:self->params->title]]; + } + [alert setMessageText:[[NSString alloc] initWithUTF8String:self->params->msg]]; + switch (self->params->style) { + case MSG_YESNO: + [alert addButtonWithTitle:@"Yes"]; + [alert addButtonWithTitle:@"No"]; + break; + case MSG_ERROR: + [alert setIcon:[NSImage imageNamed:NSImageNameCaution]]; + [alert addButtonWithTitle:@"OK"]; + break; + case MSG_INFO: + [alert setIcon:[NSImage imageNamed:NSImageNameInfo]]; + [alert addButtonWithTitle:@"OK"]; + break; + } + + checkActivationPolicy(); + + self->result = [alert runModal] == NSAlertFirstButtonReturn ? DLG_OK : DLG_CANCEL; + return self->result; +} +@end + +@interface FileDlg : NSObject { + FileDlgParams* params; + DlgResult result; +} ++ (FileDlg*)init:(FileDlgParams*)params; +- (DlgResult)run; +@end + +DlgResult fileDlg(FileDlgParams* params) { + return [[FileDlg init:params] run]; +} + +@implementation FileDlg ++ (FileDlg*)init:(FileDlgParams*)params { + FileDlg* d = [FileDlg alloc]; + d->params = params; + return d; +} + +- (DlgResult)run { + if(![NSThread isMainThread]) { + [self performSelectorOnMainThread:@selector(run) withObject:nil waitUntilDone:YES]; + } else if(self->params->mode == SAVEDLG) { + self->result = [self save]; + } else { + self->result = [self load]; + } + return self->result; +} + +- (NSInteger)runPanel:(NSSavePanel*)panel { + [panel setFloatingPanel:YES]; + [panel setShowsHiddenFiles:self->params->showHidden ? YES : NO]; + [panel setCanCreateDirectories:YES]; + if(self->params->title != nil) { + [panel setTitle:[[NSString alloc] initWithUTF8String:self->params->title]]; + } +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + if(self->params->numext > 0) { + [panel setAllowedFileTypes:[NSArray arrayWithObjects:(NSString**)self->params->exts count:self->params->numext]]; + } +#pragma clang diagnostic pop + if(self->params->relaxext) { + [panel setAllowsOtherFileTypes:YES]; + } + if(self->params->startDir) { + [panel setDirectoryURL:[NSURL URLWithString:[[NSString alloc] initWithUTF8String:self->params->startDir]]]; + } + if(self->params->filename != nil) { + [panel setNameFieldStringValue:[[NSString alloc] initWithUTF8String:self->params->filename]]; + } + + checkActivationPolicy(); + + return [panel runModal]; +} + +- (DlgResult)save { + NSSavePanel* panel = [NSSavePanel savePanel]; + if(![self runPanel:panel]) { + return DLG_CANCEL; + } else if(![[panel URL] getFileSystemRepresentation:self->params->buf maxLength:self->params->nbuf]) { + return DLG_URLFAIL; + } + return DLG_OK; +} + +- (DlgResult)load { + NSOpenPanel* panel = [NSOpenPanel openPanel]; + if(self->params->mode == DIRDLG) { + [panel setCanChooseDirectories:YES]; + [panel setCanChooseFiles:NO]; + } + + if(self->params->allowMultiple) { + [panel setAllowsMultipleSelection:YES]; + } + + if(![self runPanel:panel]) { + return DLG_CANCEL; + } + + NSArray* urls = [panel URLs]; + if(self->params->allowMultiple && [urls count] >= 1) { + // For multiple files, we need to return all paths separated by null bytes + char* bufPtr = self->params->buf; + int remainingBuf = self->params->nbuf; + + // Calculate total required buffer size first + int totalSize = 0; + for(NSURL* url in urls) { + char tempBuf[PATH_MAX]; + if(![url getFileSystemRepresentation:tempBuf maxLength:PATH_MAX]) { + return DLG_URLFAIL; + } + totalSize += strlen(tempBuf) + 1; // +1 for null terminator + } + totalSize += 1; // Final null terminator + + if(totalSize > self->params->nbuf) { + // Not enough buffer space + return DLG_URLFAIL; + } + + // Now actually copy the paths (we know we have space) + bufPtr = self->params->buf; + for(NSURL* url in urls) { + char tempBuf[PATH_MAX]; + [url getFileSystemRepresentation:tempBuf maxLength:PATH_MAX]; + int pathLen = strlen(tempBuf); + strcpy(bufPtr, tempBuf); + bufPtr += pathLen + 1; + } + *bufPtr = '\0'; // Final null terminator + } + + return DLG_OK; +} + +@end diff --git a/app/dialog/cocoa/dlg_darwin.go b/app/dialog/cocoa/dlg_darwin.go new file mode 100644 index 000000000..a71275cf0 --- /dev/null +++ b/app/dialog/cocoa/dlg_darwin.go @@ -0,0 +1,183 @@ +package cocoa + +// #cgo darwin LDFLAGS: -framework Cocoa +// #include +// #include +// #include "dlg.h" +import "C" + +import ( + "bytes" + "errors" + "unsafe" +) + +type AlertParams struct { + p C.AlertDlgParams +} + +func mkAlertParams(msg, title string, style C.AlertStyle) *AlertParams { + a := AlertParams{C.AlertDlgParams{msg: C.CString(msg), style: style}} + if title != "" { + a.p.title = C.CString(title) + } + return &a +} + +func (a *AlertParams) run() C.DlgResult { + return C.alertDlg(&a.p) +} + +func (a *AlertParams) free() { + C.free(unsafe.Pointer(a.p.msg)) + if a.p.title != nil { + C.free(unsafe.Pointer(a.p.title)) + } +} + +func nsStr(s string) unsafe.Pointer { + return C.NSStr(unsafe.Pointer(&[]byte(s)[0]), C.int(len(s))) +} + +func YesNoDlg(msg, title string) bool { + a := mkAlertParams(msg, title, C.MSG_YESNO) + defer a.free() + return a.run() == C.DLG_OK +} + +func InfoDlg(msg, title string) { + a := mkAlertParams(msg, title, C.MSG_INFO) + defer a.free() + a.run() +} + +func ErrorDlg(msg, title string) { + a := mkAlertParams(msg, title, C.MSG_ERROR) + defer a.free() + a.run() +} + +const ( + BUFSIZE = C.PATH_MAX + MULTI_FILE_BUF_SIZE = 32768 +) + +// MultiFileDlg opens a file dialog that allows multiple file selection +func MultiFileDlg(title string, exts []string, relaxExt bool, startDir string, showHidden bool) ([]string, error) { + return fileDlgWithOptions(C.LOADDLG, title, exts, relaxExt, startDir, "", showHidden, true) +} + +// FileDlg opens a file dialog for single file selection (kept for compatibility) +func FileDlg(save bool, title string, exts []string, relaxExt bool, startDir string, filename string, showHidden bool) (string, error) { + mode := C.LOADDLG + if save { + mode = C.SAVEDLG + } + files, err := fileDlgWithOptions(mode, title, exts, relaxExt, startDir, filename, showHidden, false) + if err != nil { + return "", err + } + if len(files) == 0 { + return "", nil + } + return files[0], nil +} + +func DirDlg(title string, startDir string, showHidden bool) (string, error) { + files, err := fileDlgWithOptions(C.DIRDLG, title, nil, false, startDir, "", showHidden, false) + if err != nil { + return "", err + } + if len(files) == 0 { + return "", nil + } + return files[0], nil +} + +// fileDlgWithOptions is the unified file dialog function that handles both single and multiple selection +func fileDlgWithOptions(mode int, title string, exts []string, relaxExt bool, startDir, filename string, showHidden, allowMultiple bool) ([]string, error) { + // Use larger buffer for multiple files, smaller for single + bufSize := BUFSIZE + if allowMultiple { + bufSize = MULTI_FILE_BUF_SIZE + } + + p := C.FileDlgParams{ + mode: C.int(mode), + nbuf: C.int(bufSize), + } + + if allowMultiple { + p.allowMultiple = C.int(1) // Enable multiple selection //nolint:structcheck + } + if showHidden { + p.showHidden = 1 + } + + p.buf = (*C.char)(C.malloc(C.size_t(bufSize))) + defer C.free(unsafe.Pointer(p.buf)) + buf := (*(*[MULTI_FILE_BUF_SIZE]byte)(unsafe.Pointer(p.buf)))[:bufSize] + + if title != "" { + p.title = C.CString(title) + defer C.free(unsafe.Pointer(p.title)) + } + if startDir != "" { + p.startDir = C.CString(startDir) + defer C.free(unsafe.Pointer(p.startDir)) + } + if filename != "" { + p.filename = C.CString(filename) + defer C.free(unsafe.Pointer(p.filename)) + } + + if len(exts) > 0 { + if len(exts) > 999 { + panic("more than 999 extensions not supported") + } + ptrSize := int(unsafe.Sizeof(&title)) + p.exts = (*unsafe.Pointer)(C.malloc(C.size_t(ptrSize * len(exts)))) + defer C.free(unsafe.Pointer(p.exts)) + cext := (*(*[999]unsafe.Pointer)(unsafe.Pointer(p.exts)))[:] + for i, ext := range exts { + cext[i] = nsStr(ext) + defer C.NSRelease(cext[i]) + } + p.numext = C.int(len(exts)) + if relaxExt { + p.relaxext = 1 + } + } + + // Execute dialog and parse results + switch C.fileDlg(&p) { + case C.DLG_OK: + if allowMultiple { + // Parse multiple null-terminated strings from buffer + var files []string + start := 0 + for i := range len(buf) - 1 { + if buf[i] == 0 { + if i > start { + files = append(files, string(buf[start:i])) + } + start = i + 1 + // Check for double null (end of list) + if i+1 < len(buf) && buf[i+1] == 0 { + break + } + } + } + return files, nil + } else { + // Single file - return as array for consistency + filename := string(buf[:bytes.Index(buf, []byte{0})]) + return []string{filename}, nil + } + case C.DLG_CANCEL: + return nil, nil + case C.DLG_URLFAIL: + return nil, errors.New("failed to get file-system representation for selected URL") + } + panic("unhandled case") +} diff --git a/app/dialog/dlgs.go b/app/dialog/dlgs.go new file mode 100644 index 000000000..700f79fc4 --- /dev/null +++ b/app/dialog/dlgs.go @@ -0,0 +1,182 @@ +//go:build windows || darwin + +// Package dialog provides a simple cross-platform common dialog API. +// Eg. to prompt the user with a yes/no dialog: +// +// if dialog.MsgDlg("%s", "Do you want to continue?").YesNo() { +// // user pressed Yes +// } +// +// The general usage pattern is to call one of the toplevel *Dlg functions +// which return a *Builder structure. From here you can optionally call +// configuration functions (eg. Title) to customise the dialog, before +// using a launcher function to run the dialog. +package dialog + +import ( + "errors" + "fmt" +) + +// ErrCancelled is an error returned when a user cancels/closes a dialog. +var ErrCancelled = errors.New("Cancelled") + +// Cancelled refers to ErrCancelled. +// Deprecated: Use ErrCancelled instead. +var Cancelled = ErrCancelled + +// Dlg is the common type for dialogs. +type Dlg struct { + Title string +} + +// MsgBuilder is used for creating message boxes. +type MsgBuilder struct { + Dlg + Msg string +} + +// Message initialises a MsgBuilder with the provided message. +func Message(format string, args ...interface{}) *MsgBuilder { + return &MsgBuilder{Msg: fmt.Sprintf(format, args...)} +} + +// Title specifies what the title of the message dialog will be. +func (b *MsgBuilder) Title(title string) *MsgBuilder { + b.Dlg.Title = title + return b +} + +// YesNo spawns the message dialog with two buttons, "Yes" and "No". +// Returns true iff the user selected "Yes". +func (b *MsgBuilder) YesNo() bool { + return b.yesNo() +} + +// Info spawns the message dialog with an information icon and single button, "Ok". +func (b *MsgBuilder) Info() { + b.info() +} + +// Error spawns the message dialog with an error icon and single button, "Ok". +func (b *MsgBuilder) Error() { + b.error() +} + +// FileFilter represents a category of files (eg. audio files, spreadsheets). +type FileFilter struct { + Desc string + Extensions []string +} + +// FileBuilder is used for creating file browsing dialogs. +type FileBuilder struct { + Dlg + StartDir string + StartFile string + Filters []FileFilter + ShowHiddenFiles bool +} + +// File initialises a FileBuilder using the default configuration. +func File() *FileBuilder { + return &FileBuilder{} +} + +// Title specifies the title to be used for the dialog. +func (b *FileBuilder) Title(title string) *FileBuilder { + b.Dlg.Title = title + return b +} + +// Filter adds a category of files to the types allowed by the dialog. Multiple +// calls to Filter are cumulative - any of the provided categories will be allowed. +// By default all files can be selected. +// +// The special extension '*' allows all files to be selected when the Filter is active. +func (b *FileBuilder) Filter(desc string, extensions ...string) *FileBuilder { + filt := FileFilter{desc, extensions} + if len(filt.Extensions) == 0 { + filt.Extensions = append(filt.Extensions, "*") + } + b.Filters = append(b.Filters, filt) + return b +} + +// SetStartDir specifies the initial directory of the dialog. +func (b *FileBuilder) SetStartDir(startDir string) *FileBuilder { + b.StartDir = startDir + return b +} + +// SetStartFile specifies the initial file name of the dialog. +func (b *FileBuilder) SetStartFile(startFile string) *FileBuilder { + b.StartFile = startFile + return b +} + +// ShowHiddenFiles sets whether hidden files should be visible in the dialog. +func (b *FileBuilder) ShowHidden(show bool) *FileBuilder { + b.ShowHiddenFiles = show + return b +} + +// Load spawns the file selection dialog using the configured settings, +// asking the user to select a single file. Returns ErrCancelled as the error +// if the user cancels or closes the dialog. +func (b *FileBuilder) Load() (string, error) { + return b.load() +} + +// LoadMultiple spawns the file selection dialog using the configured settings, +// asking the user to select multiple files. Returns ErrCancelled as the error +// if the user cancels or closes the dialog. +func (b *FileBuilder) LoadMultiple() ([]string, error) { + return b.loadMultiple() +} + +// Save spawns the file selection dialog using the configured settings, +// asking the user for a filename to save as. If the chosen file exists, the +// user is prompted whether they want to overwrite the file. Returns +// ErrCancelled as the error if the user cancels/closes the dialog, or selects +// not to overwrite the file. +func (b *FileBuilder) Save() (string, error) { + return b.save() +} + +// DirectoryBuilder is used for directory browse dialogs. +type DirectoryBuilder struct { + Dlg + StartDir string + ShowHiddenFiles bool +} + +// Directory initialises a DirectoryBuilder using the default configuration. +func Directory() *DirectoryBuilder { + return &DirectoryBuilder{} +} + +// Browse spawns the directory selection dialog using the configured settings, +// asking the user to select a single folder. Returns ErrCancelled as the error +// if the user cancels or closes the dialog. +func (b *DirectoryBuilder) Browse() (string, error) { + return b.browse() +} + +// Title specifies the title to be used for the dialog. +func (b *DirectoryBuilder) Title(title string) *DirectoryBuilder { + b.Dlg.Title = title + return b +} + +// StartDir specifies the initial directory to be used for the dialog. +func (b *DirectoryBuilder) SetStartDir(dir string) *DirectoryBuilder { + b.StartDir = dir + return b +} + +// ShowHiddenFiles sets whether hidden files should be visible in the dialog. +func (b *DirectoryBuilder) ShowHidden(show bool) *DirectoryBuilder { + b.ShowHiddenFiles = show + return b +} diff --git a/app/dialog/dlgs_darwin.go b/app/dialog/dlgs_darwin.go new file mode 100644 index 000000000..8d0f08d65 --- /dev/null +++ b/app/dialog/dlgs_darwin.go @@ -0,0 +1,82 @@ +package dialog + +import ( + "github.com/ollama/ollama/app/dialog/cocoa" +) + +func (b *MsgBuilder) yesNo() bool { + return cocoa.YesNoDlg(b.Msg, b.Dlg.Title) +} + +func (b *MsgBuilder) info() { + cocoa.InfoDlg(b.Msg, b.Dlg.Title) +} + +func (b *MsgBuilder) error() { + cocoa.ErrorDlg(b.Msg, b.Dlg.Title) +} + +func (b *FileBuilder) load() (string, error) { + return b.run(false) +} + +func (b *FileBuilder) loadMultiple() ([]string, error) { + return b.runMultiple() +} + +func (b *FileBuilder) save() (string, error) { + return b.run(true) +} + +func (b *FileBuilder) run(save bool) (string, error) { + star := false + var exts []string + for _, filt := range b.Filters { + for _, ext := range filt.Extensions { + if ext == "*" { + star = true + } else { + exts = append(exts, ext) + } + } + } + if star && save { + /* OSX doesn't allow the user to switch visible file types/extensions. Also + ** NSSavePanel's allowsOtherFileTypes property has no effect for an open + ** dialog, so if "*" is a possible extension we must always show all files. */ + exts = nil + } + f, err := cocoa.FileDlg(save, b.Dlg.Title, exts, star, b.StartDir, b.StartFile, b.ShowHiddenFiles) + if f == "" && err == nil { + return "", ErrCancelled + } + return f, err +} + +func (b *FileBuilder) runMultiple() ([]string, error) { + star := false + var exts []string + for _, filt := range b.Filters { + for _, ext := range filt.Extensions { + if ext == "*" { + star = true + } else { + exts = append(exts, ext) + } + } + } + + files, err := cocoa.MultiFileDlg(b.Dlg.Title, exts, star, b.StartDir, b.ShowHiddenFiles) + if len(files) == 0 && err == nil { + return nil, ErrCancelled + } + return files, err +} + +func (b *DirectoryBuilder) browse() (string, error) { + f, err := cocoa.DirDlg(b.Dlg.Title, b.StartDir, b.ShowHiddenFiles) + if f == "" && err == nil { + return "", ErrCancelled + } + return f, err +} diff --git a/app/dialog/dlgs_windows.go b/app/dialog/dlgs_windows.go new file mode 100644 index 000000000..c5b175caa --- /dev/null +++ b/app/dialog/dlgs_windows.go @@ -0,0 +1,241 @@ +package dialog + +import ( + "fmt" + "reflect" + "syscall" + "unicode/utf16" + "unsafe" + + "github.com/TheTitanrain/w32" +) + +const multiFileBufferSize = w32.MAX_PATH * 10 + +type WinDlgError int + +func (e WinDlgError) Error() string { + return fmt.Sprintf("CommDlgExtendedError: %#x", e) +} + +func err() error { + e := w32.CommDlgExtendedError() + if e == 0 { + return ErrCancelled + } + return WinDlgError(e) +} + +func (b *MsgBuilder) yesNo() bool { + r := w32.MessageBox(w32.HWND(0), b.Msg, firstOf(b.Dlg.Title, "Confirm?"), w32.MB_YESNO) + return r == w32.IDYES +} + +func (b *MsgBuilder) info() { + w32.MessageBox(w32.HWND(0), b.Msg, firstOf(b.Dlg.Title, "Information"), w32.MB_OK|w32.MB_ICONINFORMATION) +} + +func (b *MsgBuilder) error() { + w32.MessageBox(w32.HWND(0), b.Msg, firstOf(b.Dlg.Title, "Error"), w32.MB_OK|w32.MB_ICONERROR) +} + +type filedlg struct { + buf []uint16 + filters []uint16 + opf *w32.OPENFILENAME +} + +func (d filedlg) Filename() string { + i := 0 + for i < len(d.buf) && d.buf[i] != 0 { + i++ + } + return string(utf16.Decode(d.buf[:i])) +} + +func (d filedlg) parseMultipleFilenames() []string { + var files []string + i := 0 + + // Find first null terminator (directory path) + for i < len(d.buf) && d.buf[i] != 0 { + i++ + } + + if i >= len(d.buf) { + return files + } + + // Get directory path + dirPath := string(utf16.Decode(d.buf[:i])) + i++ // Skip null terminator + + // Check if there are more files (multiple selection) + if i < len(d.buf) && d.buf[i] != 0 { + // Multiple files selected - parse filenames + for i < len(d.buf) { + start := i + // Find next null terminator + for i < len(d.buf) && d.buf[i] != 0 { + i++ + } + if i >= len(d.buf) { + break + } + + if start < i { + filename := string(utf16.Decode(d.buf[start:i])) + if dirPath != "" { + files = append(files, dirPath+"\\"+filename) + } else { + files = append(files, filename) + } + } + i++ // Skip null terminator + if i >= len(d.buf) || d.buf[i] == 0 { + break // End of list + } + } + } else { + // Single file selected + files = append(files, dirPath) + } + + return files +} + +func (b *FileBuilder) load() (string, error) { + d := openfile(w32.OFN_FILEMUSTEXIST|w32.OFN_NOCHANGEDIR, b) + if w32.GetOpenFileName(d.opf) { + return d.Filename(), nil + } + return "", err() +} + +func (b *FileBuilder) loadMultiple() ([]string, error) { + d := openfile(w32.OFN_FILEMUSTEXIST|w32.OFN_NOCHANGEDIR|w32.OFN_ALLOWMULTISELECT|w32.OFN_EXPLORER, b) + d.buf = make([]uint16, multiFileBufferSize) + d.opf.File = utf16ptr(d.buf) + d.opf.MaxFile = uint32(len(d.buf)) + + if w32.GetOpenFileName(d.opf) { + return d.parseMultipleFilenames(), nil + } + return nil, err() +} + +func (b *FileBuilder) save() (string, error) { + d := openfile(w32.OFN_OVERWRITEPROMPT|w32.OFN_NOCHANGEDIR, b) + if w32.GetSaveFileName(d.opf) { + return d.Filename(), nil + } + return "", err() +} + +/* syscall.UTF16PtrFromString not sufficient because we need to encode embedded NUL bytes */ +func utf16ptr(utf16 []uint16) *uint16 { + if utf16[len(utf16)-1] != 0 { + panic("refusing to make ptr to non-NUL terminated utf16 slice") + } + h := (*reflect.SliceHeader)(unsafe.Pointer(&utf16)) + return (*uint16)(unsafe.Pointer(h.Data)) +} + +func utf16slice(ptr *uint16) []uint16 { //nolint:unused + hdr := reflect.SliceHeader{Data: uintptr(unsafe.Pointer(ptr)), Len: 1, Cap: 1} + slice := *((*[]uint16)(unsafe.Pointer(&hdr))) //nolint:govet + i := 0 + for slice[len(slice)-1] != 0 { + i++ + } + hdr.Len = i + slice = *((*[]uint16)(unsafe.Pointer(&hdr))) //nolint:govet + return slice +} + +func openfile(flags uint32, b *FileBuilder) (d filedlg) { + d.buf = make([]uint16, w32.MAX_PATH) + if b.StartFile != "" { + initialName, _ := syscall.UTF16FromString(b.StartFile) + for i := 0; i < len(initialName) && i < w32.MAX_PATH; i++ { + d.buf[i] = initialName[i] + } + } + d.opf = &w32.OPENFILENAME{ + File: utf16ptr(d.buf), + MaxFile: uint32(len(d.buf)), + Flags: flags, + } + d.opf.StructSize = uint32(unsafe.Sizeof(*d.opf)) + if b.StartDir != "" { + d.opf.InitialDir, _ = syscall.UTF16PtrFromString(b.StartDir) + } + if b.Dlg.Title != "" { + d.opf.Title, _ = syscall.UTF16PtrFromString(b.Dlg.Title) + } + for _, filt := range b.Filters { + /* build utf16 string of form "Music File\0*.mp3;*.ogg;*.wav;\0" */ + d.filters = append(d.filters, utf16.Encode([]rune(filt.Desc))...) + d.filters = append(d.filters, 0) + for _, ext := range filt.Extensions { + s := fmt.Sprintf("*.%s;", ext) + d.filters = append(d.filters, utf16.Encode([]rune(s))...) + } + d.filters = append(d.filters, 0) + } + if d.filters != nil { + d.filters = append(d.filters, 0, 0) // two extra NUL chars to terminate the list + d.opf.Filter = utf16ptr(d.filters) + } + return d +} + +type dirdlg struct { + bi *w32.BROWSEINFO +} + +const ( + bffm_INITIALIZED = 1 + bffm_SELCHANGED = 2 + bffm_VALIDATEFAILEDA = 3 + bffm_VALIDATEFAILEDW = 4 + bffm_SETSTATUSTEXTA = (w32.WM_USER + 100) + bffm_SETSTATUSTEXTW = (w32.WM_USER + 104) + bffm_ENABLEOK = (w32.WM_USER + 101) + bffm_SETSELECTIONA = (w32.WM_USER + 102) + bffm_SETSELECTIONW = (w32.WM_USER + 103) + bffm_SETOKTEXT = (w32.WM_USER + 105) + bffm_SETEXPANDED = (w32.WM_USER + 106) + bffm_SETSTATUSTEXT = bffm_SETSTATUSTEXTW + bffm_SETSELECTION = bffm_SETSELECTIONW + bffm_VALIDATEFAILED = bffm_VALIDATEFAILEDW +) + +func callbackDefaultDir(hwnd w32.HWND, msg uint, lParam, lpData uintptr) int { + if msg == bffm_INITIALIZED { + _ = w32.SendMessage(hwnd, bffm_SETSELECTION, w32.TRUE, lpData) + } + return 0 +} + +func selectdir(b *DirectoryBuilder) (d dirdlg) { + d.bi = &w32.BROWSEINFO{Flags: w32.BIF_RETURNONLYFSDIRS | w32.BIF_NEWDIALOGSTYLE} + if b.Dlg.Title != "" { + d.bi.Title, _ = syscall.UTF16PtrFromString(b.Dlg.Title) + } + if b.StartDir != "" { + s16, _ := syscall.UTF16PtrFromString(b.StartDir) + d.bi.LParam = uintptr(unsafe.Pointer(s16)) + d.bi.CallbackFunc = syscall.NewCallback(callbackDefaultDir) + } + return d +} + +func (b *DirectoryBuilder) browse() (string, error) { + d := selectdir(b) + res := w32.SHBrowseForFolder(d.bi) + if res == 0 { + return "", ErrCancelled + } + return w32.SHGetPathFromIDList(res), nil +} diff --git a/app/dialog/util.go b/app/dialog/util.go new file mode 100644 index 000000000..2848c4798 --- /dev/null +++ b/app/dialog/util.go @@ -0,0 +1,12 @@ +//go:build windows + +package dialog + +func firstOf(args ...string) string { + for _, arg := range args { + if arg != "" { + return arg + } + } + return "" +} diff --git a/app/format/field.go b/app/format/field.go new file mode 100644 index 000000000..090bdf7dc --- /dev/null +++ b/app/format/field.go @@ -0,0 +1,30 @@ +//go:build windows || darwin + +package format + +import ( + "strings" + "unicode" +) + +// KebabCase converts a string from camelCase or PascalCase to kebab-case. +// (e.g. "camelCase" -> "camel-case") +func KebabCase(str string) string { + var result strings.Builder + + for i, char := range str { + if i > 0 { + prevChar := rune(str[i-1]) + + // Add hyphen before uppercase letters + if unicode.IsUpper(char) && + (unicode.IsLower(prevChar) || unicode.IsDigit(prevChar) || + (i < len(str)-1 && unicode.IsLower(rune(str[i+1])))) { + result.WriteRune('-') + } + } + result.WriteRune(unicode.ToLower(char)) + } + + return result.String() +} diff --git a/app/format/field_test.go b/app/format/field_test.go new file mode 100644 index 000000000..9ea843183 --- /dev/null +++ b/app/format/field_test.go @@ -0,0 +1,34 @@ +//go:build windows || darwin + +package format + +import "testing" + +func TestKebabCase(t *testing.T) { + tests := []struct { + input string + expected string + }{ + {"already-kebab-case", "already-kebab-case"}, + {"simpleCamelCase", "simple-camel-case"}, + {"PascalCase", "pascal-case"}, + {"camelCaseWithNumber123", "camel-case-with-number123"}, + {"APIResponse", "api-response"}, + {"mixedCASE", "mixed-case"}, + {"WithACRONYMS", "with-acronyms"}, + {"ALLCAPS", "allcaps"}, + {"camelCaseWITHMixedACRONYMS", "camel-case-with-mixed-acronyms"}, + {"numbers123in456string", "numbers123in456string"}, + {"5", "5"}, + {"S", "s"}, + } + + for _, tt := range tests { + t.Run(tt.input, func(t *testing.T) { + result := KebabCase(tt.input) + if result != tt.expected { + t.Errorf("toKebabCase(%q) = %q, want %q", tt.input, result, tt.expected) + } + }) + } +} diff --git a/app/lifecycle/getstarted_nonwindows.go b/app/lifecycle/getstarted_nonwindows.go deleted file mode 100644 index 2af87ab92..000000000 --- a/app/lifecycle/getstarted_nonwindows.go +++ /dev/null @@ -1,9 +0,0 @@ -//go:build !windows - -package lifecycle - -import "errors" - -func GetStarted() error { - return errors.New("not implemented") -} diff --git a/app/lifecycle/getstarted_windows.go b/app/lifecycle/getstarted_windows.go deleted file mode 100644 index f39dc31c0..000000000 --- a/app/lifecycle/getstarted_windows.go +++ /dev/null @@ -1,43 +0,0 @@ -package lifecycle - -import ( - "fmt" - "log/slog" - "os" - "os/exec" - "path/filepath" - "syscall" -) - -func GetStarted() error { - const CREATE_NEW_CONSOLE = 0x00000010 - var err error - bannerScript := filepath.Join(AppDir, "ollama_welcome.ps1") - args := []string{ - // TODO once we're signed, the execution policy bypass should be removed - "powershell", "-noexit", "-ExecutionPolicy", "Bypass", "-nologo", "-file", bannerScript, - } - args[0], err = exec.LookPath(args[0]) - if err != nil { - return err - } - - // Make sure the script actually exists - _, err = os.Stat(bannerScript) - if err != nil { - return fmt.Errorf("getting started banner script error %s", err) - } - - slog.Info(fmt.Sprintf("opening getting started terminal with %v", args)) - attrs := &os.ProcAttr{ - Files: []*os.File{os.Stdin, os.Stdout, os.Stderr}, - Sys: &syscall.SysProcAttr{CreationFlags: CREATE_NEW_CONSOLE, HideWindow: false}, - } - proc, err := os.StartProcess(args[0], args, attrs) - if err != nil { - return fmt.Errorf("unable to start getting started shell %w", err) - } - - slog.Debug(fmt.Sprintf("getting started terminal PID: %d", proc.Pid)) - return proc.Release() -} diff --git a/app/lifecycle/lifecycle.go b/app/lifecycle/lifecycle.go deleted file mode 100644 index c24fe6462..000000000 --- a/app/lifecycle/lifecycle.go +++ /dev/null @@ -1,94 +0,0 @@ -package lifecycle - -import ( - "context" - "fmt" - "log" - "log/slog" - "os" - "os/signal" - "syscall" - - "github.com/ollama/ollama/app/store" - "github.com/ollama/ollama/app/tray" - "github.com/ollama/ollama/envconfig" -) - -func Run() { - InitLogging() - slog.Info("app config", "env", envconfig.Values()) - - ctx, cancel := context.WithCancel(context.Background()) - var done chan int - - t, err := tray.NewTray() - if err != nil { - log.Fatalf("Failed to start: %s", err) - } - callbacks := t.GetCallbacks() - - signals := make(chan os.Signal, 1) - signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM) - - go func() { - slog.Debug("starting callback loop") - for { - select { - case <-callbacks.Quit: - slog.Debug("quit called") - t.Quit() - case <-signals: - slog.Debug("shutting down due to signal") - t.Quit() - case <-callbacks.Update: - err := DoUpgrade(cancel, done) - if err != nil { - slog.Warn(fmt.Sprintf("upgrade attempt failed: %s", err)) - } - case <-callbacks.ShowLogs: - ShowLogs() - case <-callbacks.DoFirstUse: - err := GetStarted() - if err != nil { - slog.Warn(fmt.Sprintf("Failed to launch getting started shell: %s", err)) - } - } - } - }() - - // Are we first use? - if !store.GetFirstTimeRun() { - slog.Debug("First time run") - err = t.DisplayFirstUseNotification() - if err != nil { - slog.Debug(fmt.Sprintf("XXX failed to display first use notification %v", err)) - } - store.SetFirstTimeRun(true) - } else { - slog.Debug("Not first time, skipping first run notification") - } - - if IsServerRunning(ctx) { - slog.Info("Detected another instance of ollama running, exiting") - os.Exit(1) - } else { - done, err = SpawnServer(ctx, CLIName) - if err != nil { - // TODO - should we retry in a backoff loop? - // TODO - should we pop up a warning and maybe add a menu item to view application logs? - slog.Error(fmt.Sprintf("Failed to spawn ollama server %s", err)) - done = make(chan int, 1) - done <- 1 - } - } - - StartBackgroundUpdaterChecker(ctx, t.UpdateAvailable) - - t.Run() - cancel() - slog.Info("Waiting for ollama server to shutdown...") - if done != nil { - <-done - } - slog.Info("Ollama app exiting") -} diff --git a/app/lifecycle/logging.go b/app/lifecycle/logging.go deleted file mode 100644 index 22e3de194..000000000 --- a/app/lifecycle/logging.go +++ /dev/null @@ -1,62 +0,0 @@ -package lifecycle - -import ( - "fmt" - "log/slog" - "os" - "strconv" - "strings" - - "github.com/ollama/ollama/envconfig" - "github.com/ollama/ollama/logutil" -) - -func InitLogging() { - var logFile *os.File - var err error - // Detect if we're a GUI app on windows, and if not, send logs to console - if os.Stderr.Fd() != 0 { - // Console app detected - logFile = os.Stderr - // TODO - write one-line to the app.log file saying we're running in console mode to help avoid confusion - } else { - rotateLogs(AppLogFile) - logFile, err = os.OpenFile(AppLogFile, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0o755) - if err != nil { - slog.Error(fmt.Sprintf("failed to create server log %v", err)) - return - } - } - - slog.SetDefault(logutil.NewLogger(logFile, envconfig.LogLevel())) - slog.Info("ollama app started") -} - -func rotateLogs(logFile string) { - if _, err := os.Stat(logFile); os.IsNotExist(err) { - return - } - index := strings.LastIndex(logFile, ".") - pre := logFile[:index] - post := "." + logFile[index+1:] - for i := LogRotationCount; i > 0; i-- { - older := pre + "-" + strconv.Itoa(i) + post - newer := pre + "-" + strconv.Itoa(i-1) + post - if i == 1 { - newer = pre + post - } - if _, err := os.Stat(newer); err == nil { - if _, err := os.Stat(older); err == nil { - err := os.Remove(older) - if err != nil { - slog.Warn("Failed to remove older log", "older", older, "error", err) - continue - } - } - err := os.Rename(newer, older) - if err != nil { - slog.Warn("Failed to rotate log", "older", older, "newer", newer, "error", err) - } - } - } -} diff --git a/app/lifecycle/logging_nonwindows.go b/app/lifecycle/logging_nonwindows.go deleted file mode 100644 index 205e47d77..000000000 --- a/app/lifecycle/logging_nonwindows.go +++ /dev/null @@ -1,9 +0,0 @@ -//go:build !windows - -package lifecycle - -import "log/slog" - -func ShowLogs() { - slog.Warn("not implemented") -} diff --git a/app/lifecycle/logging_test.go b/app/lifecycle/logging_test.go deleted file mode 100644 index 8d5cdf6e7..000000000 --- a/app/lifecycle/logging_test.go +++ /dev/null @@ -1,44 +0,0 @@ -package lifecycle - -import ( - "os" - "path/filepath" - "strconv" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestRotateLogs(t *testing.T) { - logDir := t.TempDir() - logFile := filepath.Join(logDir, "testlog.log") - - // No log exists - rotateLogs(logFile) - - require.NoError(t, os.WriteFile(logFile, []byte("1"), 0o644)) - assert.FileExists(t, logFile) - // First rotation - rotateLogs(logFile) - assert.FileExists(t, filepath.Join(logDir, "testlog-1.log")) - assert.NoFileExists(t, filepath.Join(logDir, "testlog-2.log")) - assert.NoFileExists(t, logFile) - - // Should be a no-op without a new log - rotateLogs(logFile) - assert.FileExists(t, filepath.Join(logDir, "testlog-1.log")) - assert.NoFileExists(t, filepath.Join(logDir, "testlog-2.log")) - assert.NoFileExists(t, logFile) - - for i := 2; i <= LogRotationCount+1; i++ { - require.NoError(t, os.WriteFile(logFile, []byte(strconv.Itoa(i)), 0o644)) - assert.FileExists(t, logFile) - rotateLogs(logFile) - assert.NoFileExists(t, logFile) - for j := 1; j < i; j++ { - assert.FileExists(t, filepath.Join(logDir, "testlog-"+strconv.Itoa(j)+".log")) - } - assert.NoFileExists(t, filepath.Join(logDir, "testlog-"+strconv.Itoa(i+1)+".log")) - } -} diff --git a/app/lifecycle/logging_windows.go b/app/lifecycle/logging_windows.go deleted file mode 100644 index 8f20337f5..000000000 --- a/app/lifecycle/logging_windows.go +++ /dev/null @@ -1,19 +0,0 @@ -package lifecycle - -import ( - "fmt" - "log/slog" - "os/exec" - "syscall" -) - -func ShowLogs() { - cmd_path := "c:\\Windows\\system32\\cmd.exe" - slog.Debug(fmt.Sprintf("viewing logs with start %s", AppDataDir)) - cmd := exec.Command(cmd_path, "/c", "start", AppDataDir) - cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: false, CreationFlags: 0x08000000} - err := cmd.Start() - if err != nil { - slog.Error(fmt.Sprintf("Failed to open log dir: %s", err)) - } -} diff --git a/app/lifecycle/paths.go b/app/lifecycle/paths.go deleted file mode 100644 index 42ae8267a..000000000 --- a/app/lifecycle/paths.go +++ /dev/null @@ -1,84 +0,0 @@ -package lifecycle - -import ( - "errors" - "fmt" - "log/slog" - "os" - "path/filepath" - "runtime" - "strings" -) - -var ( - AppName = "ollama app" - CLIName = "ollama" - AppDir = "/opt/Ollama" - AppDataDir = "/opt/Ollama" - // TODO - should there be a distinct log dir? - UpdateStageDir = "/tmp" - AppLogFile = "/tmp/ollama_app.log" - ServerLogFile = "/tmp/ollama.log" - UpgradeLogFile = "/tmp/ollama_update.log" - Installer = "OllamaSetup.exe" - LogRotationCount = 5 -) - -func init() { - if runtime.GOOS == "windows" { - AppName += ".exe" - CLIName += ".exe" - // Logs, configs, downloads go to LOCALAPPDATA - localAppData := os.Getenv("LOCALAPPDATA") - AppDataDir = filepath.Join(localAppData, "Ollama") - UpdateStageDir = filepath.Join(AppDataDir, "updates") - AppLogFile = filepath.Join(AppDataDir, "app.log") - ServerLogFile = filepath.Join(AppDataDir, "server.log") - UpgradeLogFile = filepath.Join(AppDataDir, "upgrade.log") - - exe, err := os.Executable() - if err != nil { - slog.Warn("error discovering executable directory", "error", err) - AppDir = filepath.Join(localAppData, "Programs", "Ollama") - } else { - AppDir = filepath.Dir(exe) - } - - // Make sure we have PATH set correctly for any spawned children - paths := strings.Split(os.Getenv("PATH"), ";") - // Start with whatever we find in the PATH/LD_LIBRARY_PATH - found := false - for _, path := range paths { - d, err := filepath.Abs(path) - if err != nil { - continue - } - if strings.EqualFold(AppDir, d) { - found = true - } - } - if !found { - paths = append(paths, AppDir) - - pathVal := strings.Join(paths, ";") - slog.Debug("setting PATH=" + pathVal) - err := os.Setenv("PATH", pathVal) - if err != nil { - slog.Error(fmt.Sprintf("failed to update PATH: %s", err)) - } - } - - // Make sure our logging dir exists - _, err = os.Stat(AppDataDir) - if errors.Is(err, os.ErrNotExist) { - if err := os.MkdirAll(AppDataDir, 0o755); err != nil { - slog.Error(fmt.Sprintf("create ollama dir %s: %v", AppDataDir, err)) - } - } - } else if runtime.GOOS == "darwin" { - // TODO - AppName += ".app" - // } else if runtime.GOOS == "linux" { - // TODO - } -} diff --git a/app/lifecycle/server.go b/app/lifecycle/server.go deleted file mode 100644 index f7aa20264..000000000 --- a/app/lifecycle/server.go +++ /dev/null @@ -1,186 +0,0 @@ -package lifecycle - -import ( - "context" - "errors" - "fmt" - "io" - "log/slog" - "os" - "os/exec" - "path/filepath" - "time" - - "github.com/ollama/ollama/api" -) - -func getCLIFullPath(command string) string { - var cmdPath string - appExe, err := os.Executable() - if err == nil { - // Check both the same location as the tray app, as well as ./bin - cmdPath = filepath.Join(filepath.Dir(appExe), command) - _, err := os.Stat(cmdPath) - if err == nil { - return cmdPath - } - cmdPath = filepath.Join(filepath.Dir(appExe), "bin", command) - _, err = os.Stat(cmdPath) - if err == nil { - return cmdPath - } - } - cmdPath, err = exec.LookPath(command) - if err == nil { - _, err := os.Stat(cmdPath) - if err == nil { - return cmdPath - } - } - pwd, err := os.Getwd() - if err == nil { - cmdPath = filepath.Join(pwd, command) - _, err = os.Stat(cmdPath) - if err == nil { - return cmdPath - } - } - - return command -} - -func start(ctx context.Context, command string) (*exec.Cmd, error) { - cmd := getCmd(ctx, getCLIFullPath(command)) - stdout, err := cmd.StdoutPipe() - if err != nil { - return nil, fmt.Errorf("failed to spawn server stdout pipe: %w", err) - } - stderr, err := cmd.StderrPipe() - if err != nil { - return nil, fmt.Errorf("failed to spawn server stderr pipe: %w", err) - } - - rotateLogs(ServerLogFile) - logFile, err := os.OpenFile(ServerLogFile, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0o755) - if err != nil { - return nil, fmt.Errorf("failed to create server log: %w", err) - } - - logDir := filepath.Dir(ServerLogFile) - _, err = os.Stat(logDir) - if err != nil { - if !errors.Is(err, os.ErrNotExist) { - return nil, fmt.Errorf("stat ollama server log dir %s: %v", logDir, err) - } - - if err := os.MkdirAll(logDir, 0o755); err != nil { - return nil, fmt.Errorf("create ollama server log dir %s: %v", logDir, err) - } - } - - go func() { - defer logFile.Close() - io.Copy(logFile, stdout) //nolint:errcheck - }() - go func() { - defer logFile.Close() - io.Copy(logFile, stderr) //nolint:errcheck - }() - - // Re-wire context done behavior to attempt a graceful shutdown of the server - cmd.Cancel = func() error { - if cmd.Process != nil { - err := terminate(cmd) - if err != nil { - slog.Warn("error trying to gracefully terminate server", "err", err) - return cmd.Process.Kill() - } - - tick := time.NewTicker(10 * time.Millisecond) - defer tick.Stop() - - for { - select { - case <-tick.C: - exited, err := isProcessExited(cmd.Process.Pid) - if err != nil { - return err - } - - if exited { - return nil - } - case <-time.After(5 * time.Second): - slog.Warn("graceful server shutdown timeout, killing", "pid", cmd.Process.Pid) - return cmd.Process.Kill() - } - } - } - return nil - } - - // run the command and wait for it to finish - if err := cmd.Start(); err != nil { - return nil, fmt.Errorf("failed to start server %w", err) - } - if cmd.Process != nil { - slog.Info(fmt.Sprintf("started ollama server with pid %d", cmd.Process.Pid)) - } - slog.Info(fmt.Sprintf("ollama server logs %s", ServerLogFile)) - - return cmd, nil -} - -func SpawnServer(ctx context.Context, command string) (chan int, error) { - done := make(chan int) - - go func() { - // Keep the server running unless we're shuttind down the app - crashCount := 0 - for { - slog.Info("starting server...") - cmd, err := start(ctx, command) - if err != nil { - crashCount++ - slog.Error(fmt.Sprintf("failed to start server %s", err)) - time.Sleep(500 * time.Millisecond * time.Duration(crashCount)) - continue - } - - cmd.Wait() //nolint:errcheck - var code int - if cmd.ProcessState != nil { - code = cmd.ProcessState.ExitCode() - } - - select { - case <-ctx.Done(): - slog.Info(fmt.Sprintf("server shutdown with exit code %d", code)) - done <- code - return - default: - crashCount++ - slog.Warn(fmt.Sprintf("server crash %d - exit code %d - respawning", crashCount, code)) - time.Sleep(500 * time.Millisecond * time.Duration(crashCount)) - break - } - } - }() - - return done, nil -} - -func IsServerRunning(ctx context.Context) bool { - client, err := api.ClientFromEnvironment() - if err != nil { - slog.Info("unable to connect to server") - return false - } - err = client.Heartbeat(ctx) - if err != nil { - slog.Debug(fmt.Sprintf("heartbeat from server: %s", err)) - slog.Info("unable to connect to server") - return false - } - return true -} diff --git a/app/lifecycle/server_unix.go b/app/lifecycle/server_unix.go deleted file mode 100644 index 705739134..000000000 --- a/app/lifecycle/server_unix.go +++ /dev/null @@ -1,38 +0,0 @@ -//go:build !windows - -package lifecycle - -import ( - "context" - "errors" - "fmt" - "os" - "os/exec" - "syscall" -) - -func getCmd(ctx context.Context, cmd string) *exec.Cmd { - return exec.CommandContext(ctx, cmd, "serve") -} - -func terminate(cmd *exec.Cmd) error { - return cmd.Process.Signal(os.Interrupt) -} - -func isProcessExited(pid int) (bool, error) { - proc, err := os.FindProcess(pid) - if err != nil { - return false, fmt.Errorf("failed to find process: %v", err) - } - - err = proc.Signal(syscall.Signal(0)) - if err != nil { - if errors.Is(err, os.ErrProcessDone) || errors.Is(err, syscall.ESRCH) { - return true, nil - } - - return false, fmt.Errorf("error signaling process: %v", err) - } - - return false, nil -} diff --git a/app/lifecycle/server_windows.go b/app/lifecycle/server_windows.go deleted file mode 100644 index 5f9fe1246..000000000 --- a/app/lifecycle/server_windows.go +++ /dev/null @@ -1,91 +0,0 @@ -package lifecycle - -import ( - "context" - "fmt" - "os/exec" - "syscall" - - "golang.org/x/sys/windows" -) - -func getCmd(ctx context.Context, exePath string) *exec.Cmd { - cmd := exec.CommandContext(ctx, exePath, "serve") - cmd.SysProcAttr = &syscall.SysProcAttr{ - HideWindow: true, - CreationFlags: windows.CREATE_NEW_PROCESS_GROUP, - } - - return cmd -} - -func terminate(cmd *exec.Cmd) error { - dll, err := windows.LoadDLL("kernel32.dll") - if err != nil { - return err - } - //nolint:errcheck - defer dll.Release() - - pid := cmd.Process.Pid - - f, err := dll.FindProc("AttachConsole") - if err != nil { - return err - } - - r1, _, err := f.Call(uintptr(pid)) - if r1 == 0 && err != syscall.ERROR_ACCESS_DENIED { - return err - } - - f, err = dll.FindProc("SetConsoleCtrlHandler") - if err != nil { - return err - } - - r1, _, err = f.Call(0, 1) - if r1 == 0 { - return err - } - - f, err = dll.FindProc("GenerateConsoleCtrlEvent") - if err != nil { - return err - } - - r1, _, err = f.Call(windows.CTRL_BREAK_EVENT, uintptr(pid)) - if r1 == 0 { - return err - } - - r1, _, err = f.Call(windows.CTRL_C_EVENT, uintptr(pid)) - if r1 == 0 { - return err - } - - return nil -} - -const STILL_ACTIVE = 259 - -func isProcessExited(pid int) (bool, error) { - hProcess, err := windows.OpenProcess(windows.PROCESS_QUERY_INFORMATION, false, uint32(pid)) - if err != nil { - return false, fmt.Errorf("failed to open process: %v", err) - } - //nolint:errcheck - defer windows.CloseHandle(hProcess) - - var exitCode uint32 - err = windows.GetExitCodeProcess(hProcess, &exitCode) - if err != nil { - return false, fmt.Errorf("failed to get exit code: %v", err) - } - - if exitCode == STILL_ACTIVE { - return false, nil - } - - return true, nil -} diff --git a/app/lifecycle/updater_nonwindows.go b/app/lifecycle/updater_nonwindows.go deleted file mode 100644 index 1d2dda801..000000000 --- a/app/lifecycle/updater_nonwindows.go +++ /dev/null @@ -1,12 +0,0 @@ -//go:build !windows - -package lifecycle - -import ( - "context" - "errors" -) - -func DoUpgrade(cancel context.CancelFunc, done chan int) error { - return errors.New("not implemented") -} diff --git a/app/lifecycle/updater_windows.go b/app/lifecycle/updater_windows.go deleted file mode 100644 index 293dd6038..000000000 --- a/app/lifecycle/updater_windows.go +++ /dev/null @@ -1,74 +0,0 @@ -package lifecycle - -import ( - "context" - "errors" - "fmt" - "log/slog" - "os" - "os/exec" - "path/filepath" -) - -func DoUpgrade(cancel context.CancelFunc, done chan int) error { - files, err := filepath.Glob(filepath.Join(UpdateStageDir, "*", "*.exe")) // TODO generalize for multiplatform - if err != nil { - return fmt.Errorf("failed to lookup downloads: %s", err) - } - if len(files) == 0 { - return errors.New("no update downloads found") - } else if len(files) > 1 { - // Shouldn't happen - slog.Warn(fmt.Sprintf("multiple downloads found, using first one %v", files)) - } - installerExe := files[0] - - slog.Info("starting upgrade with " + installerExe) - slog.Info("upgrade log file " + UpgradeLogFile) - - // make the upgrade show progress, but non interactive - installArgs := []string{ - "/CLOSEAPPLICATIONS", // Quit the tray app if it's still running - "/LOG=" + filepath.Base(UpgradeLogFile), // Only relative seems reliable, so set pwd - "/FORCECLOSEAPPLICATIONS", // Force close the tray app - might be needed - "/SP", // Skip the "This will install... Do you wish to continue" prompt - "/NOCANCEL", // Disable the ability to cancel upgrade mid-flight to avoid partially installed upgrades - "/SILENT", - } - - // Safeguard in case we have requests in flight that need to drain... - slog.Info("Waiting for server to shutdown") - cancel() - if done != nil { - <-done - } else { - // Shouldn't happen - slog.Warn("done chan was nil, not actually waiting") - } - - slog.Debug(fmt.Sprintf("starting installer: %s %v", installerExe, installArgs)) - os.Chdir(filepath.Dir(UpgradeLogFile)) //nolint:errcheck - cmd := exec.Command(installerExe, installArgs...) - - if err := cmd.Start(); err != nil { - return fmt.Errorf("unable to start ollama app %w", err) - } - - if cmd.Process != nil { - err = cmd.Process.Release() - if err != nil { - slog.Error(fmt.Sprintf("failed to release server process: %s", err)) - } - } else { - // TODO - some details about why it didn't start, or is this a pedantic error case? - return errors.New("installer process did not start") - } - - // TODO should we linger for a moment and check to make sure it's actually running by checking the pid? - - slog.Info("Installer started in background, exiting") - - os.Exit(0) - // Not reached - return nil -} diff --git a/app/logrotate/logrotate.go b/app/logrotate/logrotate.go new file mode 100644 index 000000000..df8ba9c01 --- /dev/null +++ b/app/logrotate/logrotate.go @@ -0,0 +1,45 @@ +//go:build windows || darwin + +// package logrotate provides utilities for rotating logs +// TODO (jmorgan): this most likely doesn't need it's own +// package and can be moved to app where log files are created +package logrotate + +import ( + "log/slog" + "os" + "strconv" + "strings" +) + +const MaxLogFiles = 5 + +func Rotate(filename string) { + if _, err := os.Stat(filename); os.IsNotExist(err) { + return + } + + index := strings.LastIndex(filename, ".") + pre := filename[:index] + post := "." + filename[index+1:] + for i := MaxLogFiles; i > 0; i-- { + older := pre + "-" + strconv.Itoa(i) + post + newer := pre + "-" + strconv.Itoa(i-1) + post + if i == 1 { + newer = pre + post + } + if _, err := os.Stat(newer); err == nil { + if _, err := os.Stat(older); err == nil { + err := os.Remove(older) + if err != nil { + slog.Warn("Failed to remove older log", "older", older, "error", err) + continue + } + } + err := os.Rename(newer, older) + if err != nil { + slog.Warn("Failed to rotate log", "older", older, "newer", newer, "error", err) + } + } + } +} diff --git a/app/logrotate/logrotate_test.go b/app/logrotate/logrotate_test.go new file mode 100644 index 000000000..5eef5b4f1 --- /dev/null +++ b/app/logrotate/logrotate_test.go @@ -0,0 +1,70 @@ +//go:build windows || darwin + +package logrotate + +import ( + "os" + "path/filepath" + "strconv" + "testing" +) + +func TestRotate(t *testing.T) { + logDir := t.TempDir() + logFile := filepath.Join(logDir, "testlog.log") + + // No log exists + Rotate(logFile) + + if err := os.WriteFile(logFile, []byte("1"), 0o644); err != nil { + t.Fatal(err) + } + if _, err := os.Stat(logFile); os.IsNotExist(err) { + t.Fatal("expected log file to exist") + } + + // First rotation + Rotate(logFile) + if _, err := os.Stat(filepath.Join(logDir, "testlog-1.log")); os.IsNotExist(err) { + t.Fatal("expected rotated log file to exist") + } + if _, err := os.Stat(filepath.Join(logDir, "testlog-2.log")); !os.IsNotExist(err) { + t.Fatal("expected no second rotated log file") + } + if _, err := os.Stat(logFile); !os.IsNotExist(err) { + t.Fatal("expected original log file to be moved") + } + + // Should be a no-op without a new log + Rotate(logFile) + if _, err := os.Stat(filepath.Join(logDir, "testlog-1.log")); os.IsNotExist(err) { + t.Fatal("expected rotated log file to still exist") + } + if _, err := os.Stat(filepath.Join(logDir, "testlog-2.log")); !os.IsNotExist(err) { + t.Fatal("expected no second rotated log file") + } + if _, err := os.Stat(logFile); !os.IsNotExist(err) { + t.Fatal("expected no original log file") + } + + for i := 2; i <= MaxLogFiles+1; i++ { + if err := os.WriteFile(logFile, []byte(strconv.Itoa(i)), 0o644); err != nil { + t.Fatal(err) + } + if _, err := os.Stat(logFile); os.IsNotExist(err) { + t.Fatal("expected log file to exist") + } + Rotate(logFile) + if _, err := os.Stat(logFile); !os.IsNotExist(err) { + t.Fatal("expected log file to be moved") + } + for j := 1; j < i; j++ { + if _, err := os.Stat(filepath.Join(logDir, "testlog-"+strconv.Itoa(j)+".log")); os.IsNotExist(err) { + t.Fatalf("expected rotated log file %d to exist", j) + } + } + if _, err := os.Stat(filepath.Join(logDir, "testlog-"+strconv.Itoa(i+1)+".log")); !os.IsNotExist(err) { + t.Fatalf("expected no rotated log file %d", i+1) + } + } +} diff --git a/app/main.go b/app/main.go deleted file mode 100644 index db8297954..000000000 --- a/app/main.go +++ /dev/null @@ -1,12 +0,0 @@ -package main - -// Compile with the following to get rid of the cmd pop up on windows -// go build -ldflags="-H windowsgui" . - -import ( - "github.com/ollama/ollama/app/lifecycle" -) - -func main() { - lifecycle.Run() -} diff --git a/app/ollama.iss b/app/ollama.iss index d575fc7f6..2f2aa230d 100644 --- a/app/ollama.iss +++ b/app/ollama.iss @@ -37,8 +37,10 @@ PrivilegesRequired=lowest OutputBaseFilename="OllamaSetup" SetupIconFile={#MyIcon} UninstallDisplayIcon={uninstallexe} -Compression=lzma2 -SolidCompression=no +Compression=lzma2/ultra64 +LZMAUseSeparateProcess=yes +LZMANumBlockThreads=8 +SolidCompression=yes WizardStyle=modern ChangesEnvironment=yes OutputDir=..\dist\ @@ -46,7 +48,7 @@ OutputDir=..\dist\ ; Disable logging once everything's battle tested ; Filename will be %TEMP%\Setup Log*.txt SetupLogging=yes -CloseApplications=yes +CloseApplications=no RestartApplications=no RestartIfNeededByRun=no @@ -68,7 +70,6 @@ DisableFinishedPage=yes DisableReadyMemo=yes DisableReadyPage=yes DisableStartupPrompt=yes -DisableWelcomePage=yes ; TODO - percentage can't be set less than 100, so how to make it shorter? ; WizardSizePercent=100,80 @@ -87,30 +88,42 @@ Name: "english"; MessagesFile: "compiler:Default.isl" DialogFontSize=12 [Files] -#if DirExists("..\dist\windows-amd64") -Source: "..\dist\windows-amd64-app.exe"; DestDir: "{app}"; DestName: "{#MyAppExeName}" ;Check: not IsArm64(); Flags: ignoreversion 64bit -Source: "..\dist\windows-amd64\ollama.exe"; DestDir: "{app}"; Check: not IsArm64(); Flags: ignoreversion 64bit +#if FileExists("..\dist\windows-ollama-app-amd64.exe") +Source: "..\dist\windows-ollama-app-amd64.exe"; DestDir: "{app}"; DestName: "{#MyAppExeName}" ;Check: not IsArm64(); Flags: ignoreversion 64bit; BeforeInstall: TaskKill('{#MyAppExeName}') +Source: "..\dist\windows-amd64\vc_redist.x64.exe"; DestDir: "{tmp}"; Check: not IsArm64() and vc_redist_needed(); Flags: deleteafterinstall +Source: "..\dist\windows-amd64\ollama.exe"; DestDir: "{app}"; Check: not IsArm64(); Flags: ignoreversion 64bit; BeforeInstall: TaskKill('ollama.exe') Source: "..\dist\windows-amd64\lib\ollama\*"; DestDir: "{app}\lib\ollama\"; Check: not IsArm64(); Flags: ignoreversion 64bit recursesubdirs #endif -#if DirExists("..\dist\windows-arm64") -Source: "..\dist\windows-arm64\vc_redist.arm64.exe"; DestDir: "{tmp}"; Check: IsArm64() and vc_redist_needed(); Flags: deleteafterinstall -Source: "..\dist\windows-arm64-app.exe"; DestDir: "{app}"; DestName: "{#MyAppExeName}" ;Check: IsArm64(); Flags: ignoreversion 64bit -Source: "..\dist\windows-arm64\ollama.exe"; DestDir: "{app}"; Check: IsArm64(); Flags: ignoreversion 64bit +; For local development, rely on binary compatibility at runtime since we can't cross compile +#if FileExists("..\dist\windows-ollama-app-arm64.exe") +Source: "..\dist\windows-ollama-app-arm64.exe"; DestDir: "{app}"; DestName: "{#MyAppExeName}" ;Check: IsArm64(); Flags: ignoreversion 64bit; BeforeInstall: TaskKill('{#MyAppExeName}') +#else +Source: "..\dist\windows-ollama-app-amd64.exe"; DestDir: "{app}"; DestName: "{#MyAppExeName}" ;Check: IsArm64(); Flags: ignoreversion 64bit; BeforeInstall: TaskKill('{#MyAppExeName}') +#endif + +#if FileExists("..\dist\windows-arm64\ollama.exe") +Source: "..\dist\windows-arm64\vc_redist.arm64.exe"; DestDir: "{tmp}"; Check: IsArm64() and vc_redist_needed(); Flags: deleteafterinstall +Source: "..\dist\windows-arm64\ollama.exe"; DestDir: "{app}"; Check: IsArm64(); Flags: ignoreversion 64bit; BeforeInstall: TaskKill('ollama.exe') #endif -Source: "..\dist\ollama_welcome.ps1"; DestDir: "{app}"; Flags: ignoreversion Source: ".\assets\app.ico"; DestDir: "{app}"; Flags: ignoreversion [Icons] Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; IconFilename: "{app}\app.ico" -Name: "{userstartup}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; IconFilename: "{app}\app.ico" +Name: "{app}\lib\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; IconFilename: "{app}\app.ico" Name: "{userprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; IconFilename: "{app}\app.ico" +[InstallDelete] +Type: files; Name: "{%LOCALAPPDATA}\Ollama\updates" + [Run] #if DirExists("..\dist\windows-arm64") Filename: "{tmp}\vc_redist.arm64.exe"; Parameters: "/install /passive /norestart"; Check: IsArm64() and vc_redist_needed(); StatusMsg: "Installing VC++ Redistributables..."; Flags: waituntilterminated #endif +#if DirExists("..\dist\windows-amd64") +Filename: "{tmp}\vc_redist.x64.exe"; Parameters: "/install /passive /norestart"; Check: not IsArm64() and vc_redist_needed(); StatusMsg: "Installing VC++ Redistributables..."; Flags: waituntilterminated +#endif Filename: "{cmd}"; Parameters: "/C set PATH={app};%PATH% & ""{app}\{#MyAppExeName}"""; Flags: postinstall nowait runhidden [UninstallRun] @@ -126,13 +139,13 @@ Filename: "{cmd}"; Parameters: "/c timeout 5"; Flags: runhidden Type: filesandordirs; Name: "{%TEMP}\ollama*" Type: filesandordirs; Name: "{%LOCALAPPDATA}\Ollama" Type: filesandordirs; Name: "{%LOCALAPPDATA}\Programs\Ollama" -Type: filesandordirs; Name: "{%USERPROFILE}\.ollama\models" Type: filesandordirs; Name: "{%USERPROFILE}\.ollama\history" +Type: filesandordirs; Name: "{userstartup}\{#MyAppName}.lnk" ; NOTE: if the user has a custom OLLAMA_MODELS it will be preserved [InstallDelete] Type: filesandordirs; Name: "{%TEMP}\ollama*" -Type: filesandordirs; Name: "{%LOCALAPPDATA}\Programs\Ollama" +Type: filesandordirs; Name: "{app}\lib\ollama" [Messages] WizardReady=Ollama @@ -148,6 +161,10 @@ SetupAppRunningError=Another Ollama installer is running.%n%nPlease cancel or fi Root: HKCU; Subkey: "Environment"; \ ValueType: expandsz; ValueName: "Path"; ValueData: "{olddata};{app}"; \ Check: NeedsAddPath('{app}') +; Register ollama:// URL protocol +Root: HKCU; Subkey: "Software\Classes\ollama"; ValueType: string; ValueName: ""; ValueData: "URL:Ollama Protocol"; Flags: uninsdeletekey +Root: HKCU; Subkey: "Software\Classes\ollama"; ValueType: string; ValueName: "URL Protocol"; ValueData: ""; Flags: uninsdeletekey +Root: HKCU; Subkey: "Software\Classes\ollama\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#MyAppExeName}"" ""%1"""; Flags: uninsdeletekey [Code] @@ -182,7 +199,11 @@ var v3: Cardinal; v4: Cardinal; begin - sRegKey := 'SOFTWARE\WOW6432Node\Microsoft\VisualStudio\14.0\VC\Runtimes\arm64'; + if (IsArm64()) then begin + sRegKey := 'SOFTWARE\WOW6432Node\Microsoft\VisualStudio\14.0\VC\Runtimes\arm64'; + end else begin + sRegKey := 'SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\x64'; + end; if (RegQueryDWordValue (HKEY_LOCAL_MACHINE, sRegKey, 'Major', v1) and RegQueryDWordValue (HKEY_LOCAL_MACHINE, sRegKey, 'Minor', v2) and RegQueryDWordValue (HKEY_LOCAL_MACHINE, sRegKey, 'Bld', v3) and @@ -202,3 +223,152 @@ begin else Result := TRUE; end; + +function GetDirSize(Path: String): Int64; +var + FindRec: TFindRec; + FilePath: string; + Size: Int64; +begin + if FindFirst(Path + '\*', FindRec) then begin + Result := 0; + try + repeat + if (FindRec.Name <> '.') and (FindRec.Name <> '..') then begin + FilePath := Path + '\' + FindRec.Name; + if (FindRec.Attributes and FILE_ATTRIBUTE_DIRECTORY) <> 0 then begin + Size := GetDirSize(FilePath); + end else begin + Size := Int64(FindRec.SizeHigh) shl 32 + FindRec.SizeLow; + end; + Result := Result + Size; + end; + until not FindNext(FindRec); + finally + FindClose(FindRec); + end; + end else begin + Log(Format('Failed to list %s', [Path])); + Result := -1; + end; +end; + +var + DeleteModelsChecked: Boolean; + ModelsDir: string; + +procedure InitializeUninstallProgressForm(); +var + UninstallPage: TNewNotebookPage; + UninstallButton: TNewButton; + DeleteModelsCheckbox: TNewCheckBox; + OriginalPageNameLabel: string; + OriginalPageDescriptionLabel: string; + OriginalCancelButtonEnabled: Boolean; + OriginalCancelButtonModalResult: Integer; + ctrl: TWinControl; + ModelDirA: AnsiString; + ModelsSize: Int64; +begin + if not UninstallSilent then begin + ctrl := UninstallProgressForm.CancelButton; + UninstallButton := TNewButton.Create(UninstallProgressForm); + UninstallButton.Parent := UninstallProgressForm; + UninstallButton.Left := ctrl.Left - ctrl.Width - ScaleX(10); + UninstallButton.Top := ctrl.Top; + UninstallButton.Width := ctrl.Width; + UninstallButton.Height := ctrl.Height; + UninstallButton.TabOrder := ctrl.TabOrder; + UninstallButton.Caption := 'Uninstall'; + UninstallButton.ModalResult := mrOK; + UninstallProgressForm.CancelButton.TabOrder := UninstallButton.TabOrder + 1; + UninstallPage := TNewNotebookPage.Create(UninstallProgressForm); + UninstallPage.Notebook := UninstallProgressForm.InnerNotebook; + UninstallPage.Parent := UninstallProgressForm.InnerNotebook; + UninstallPage.Align := alClient; + UninstallProgressForm.InnerNotebook.ActivePage := UninstallPage; + + ctrl := UninstallProgressForm.StatusLabel; + with TNewStaticText.Create(UninstallProgressForm) do begin + Parent := UninstallPage; + Top := ctrl.Top; + Left := ctrl.Left; + Width := ctrl.Width; + Height := ctrl.Height; + AutoSize := False; + ShowAccelChar := False; + Caption := ''; + end; + + if (DirExists(GetEnv('USERPROFILE') + '\.ollama\models\blobs')) then begin + ModelsDir := GetEnv('USERPROFILE') + '\.ollama\models'; + ModelsSize := GetDirSize(ModelsDir); + end; + + DeleteModelsCheckbox := TNewCheckBox.Create(UninstallProgressForm); + DeleteModelsCheckbox.Parent := UninstallPage; + DeleteModelsCheckbox.Top := ctrl.Top + ScaleY(30); + DeleteModelsCheckbox.Left := ctrl.Left; + DeleteModelsCheckbox.Width := ScaleX(300); + if ModelsSize > 1024*1024*1024 then begin + DeleteModelsCheckbox.Caption := 'Remove models (' + IntToStr(ModelsSize/(1024*1024*1024)) + ' GB) ' + ModelsDir; + end else if ModelsSize > 1024*1024 then begin + DeleteModelsCheckbox.Caption := 'Remove models (' + IntToStr(ModelsSize/(1024*1024)) + ' MB) ' + ModelsDir; + end else begin + DeleteModelsCheckbox.Caption := 'Remove models ' + ModelsDir; + end; + DeleteModelsCheckbox.Checked := True; + + OriginalPageNameLabel := UninstallProgressForm.PageNameLabel.Caption; + OriginalPageDescriptionLabel := UninstallProgressForm.PageDescriptionLabel.Caption; + OriginalCancelButtonEnabled := UninstallProgressForm.CancelButton.Enabled; + OriginalCancelButtonModalResult := UninstallProgressForm.CancelButton.ModalResult; + + UninstallProgressForm.PageNameLabel.Caption := ''; + UninstallProgressForm.PageDescriptionLabel.Caption := ''; + UninstallProgressForm.CancelButton.Enabled := True; + UninstallProgressForm.CancelButton.ModalResult := mrCancel; + + if UninstallProgressForm.ShowModal = mrCancel then Abort; + + UninstallButton.Visible := False; + UninstallProgressForm.PageNameLabel.Caption := OriginalPageNameLabel; + UninstallProgressForm.PageDescriptionLabel.Caption := OriginalPageDescriptionLabel; + UninstallProgressForm.CancelButton.Enabled := OriginalCancelButtonEnabled; + UninstallProgressForm.CancelButton.ModalResult := OriginalCancelButtonModalResult; + + UninstallProgressForm.InnerNotebook.ActivePage := UninstallProgressForm.InstallingPage; + + if DeleteModelsCheckbox.Checked then begin + DeleteModelsChecked:=True; + end else begin + DeleteModelsChecked:=False; + end; + end; +end; + +procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep); +begin + if CurUninstallStep = usDone then begin + if DeleteModelsChecked then begin + Log('user requested model cleanup'); + if (VarIsEmpty(ModelsDir)) then begin + Log('cleaning up home directory models') + DelTree(GetEnv('USERPROFILE') + '\.ollama\models', True, True, True); + end else begin + Log('cleaning up custom directory models ' + ModelsDir) + DelTree(ModelsDir + '\blobs', True, True, True); + DelTree(ModelsDir + '\manifests', True, True, True); + end; + end else begin + Log('user requested to preserve model dir'); + end; + end; +end; + +procedure TaskKill(FileName: String); +var + ResultCode: Integer; +begin + Exec('taskkill.exe', '/f /im ' + '"' + FileName + '"', '', SW_HIDE, ewWaitUntilTerminated, ResultCode); +end; diff --git a/app/ollama_welcome.ps1 b/app/ollama_welcome.ps1 deleted file mode 100644 index e96957486..000000000 --- a/app/ollama_welcome.ps1 +++ /dev/null @@ -1,8 +0,0 @@ -# TODO - consider ANSI colors and maybe ASCII art... -write-host "" -write-host "Welcome to Ollama!" -write-host "" -write-host "Run your first model:" -write-host "" -write-host "`tollama run llama3.2" -write-host "" \ No newline at end of file diff --git a/app/server/server.go b/app/server/server.go new file mode 100644 index 000000000..64b96b1fd --- /dev/null +++ b/app/server/server.go @@ -0,0 +1,357 @@ +//go:build windows || darwin + +package server + +import ( + "bufio" + "context" + "errors" + "fmt" + "io" + "log/slog" + "os" + "os/exec" + "path/filepath" + "regexp" + "runtime" + "strconv" + "strings" + "time" + + "github.com/ollama/ollama/app/logrotate" + "github.com/ollama/ollama/app/store" +) + +const restartDelay = time.Second + +// Server is a managed ollama server process +type Server struct { + store *store.Store + bin string // resolved path to `ollama` + log io.WriteCloser + dev bool // true if running with the dev flag +} + +type InferenceCompute struct { + Library string + Variant string + Compute string + Driver string + Name string + VRAM string +} + +func New(s *store.Store, devMode bool) *Server { + p := resolvePath("ollama") + return &Server{store: s, bin: p, dev: devMode} +} + +func resolvePath(name string) string { + // look in the app bundle first + if exe, _ := os.Executable(); exe != "" { + var dir string + if runtime.GOOS == "windows" { + dir = filepath.Dir(exe) + } else { + dir = filepath.Join(filepath.Dir(exe), "..", "Resources") + } + if _, err := os.Stat(filepath.Join(dir, name)); err == nil { + return filepath.Join(dir, name) + } + } + + // check the development dist path + for _, path := range []string{ + filepath.Join("dist", runtime.GOOS, name), + filepath.Join("dist", runtime.GOOS+"-"+runtime.GOARCH, name), + } { + if _, err := os.Stat(path); err == nil { + return path + } + } + + // fallback to system path + if p, _ := exec.LookPath(name); p != "" { + return p + } + + return name +} + +// cleanup checks the pid file for a running ollama process +// and shuts it down gracefully if it is running +func cleanup() error { + data, err := os.ReadFile(pidFile) + if err != nil { + if os.IsNotExist(err) { + return nil + } + return err + } + defer os.Remove(pidFile) + + pid, err := strconv.Atoi(strings.TrimSpace(string(data))) + if err != nil { + return err + } + + proc, err := os.FindProcess(pid) + if err != nil { + return nil + } + + ok, err := terminated(pid) + if err != nil { + slog.Debug("cleanup: error checking if terminated", "pid", pid, "err", err) + } + if ok { + return nil + } + + slog.Info("detected previous ollama process, cleaning up", "pid", pid) + return stop(proc) +} + +// stop waits for a process with the provided pid to exit by polling +// `terminated(pid)`. If the process has not exited within 5 seconds, it logs a +// warning and kills the process. +func stop(proc *os.Process) error { + if proc == nil { + return nil + } + + if err := terminate(proc); err != nil { + slog.Warn("graceful terminate failed, killing", "err", err) + return proc.Kill() + } + + deadline := time.NewTimer(5 * time.Second) + defer deadline.Stop() + + for { + select { + case <-deadline.C: + slog.Warn("timeout waiting for graceful shutdown; killing", "pid", proc.Pid) + return proc.Kill() + default: + ok, err := terminated(proc.Pid) + if err != nil { + slog.Error("error checking if ollama process is terminated", "err", err) + return err + } + if ok { + return nil + } + time.Sleep(10 * time.Millisecond) + } + } +} + +func (s *Server) Run(ctx context.Context) error { + l, err := openRotatingLog() + if err != nil { + return err + } + s.log = l + defer s.log.Close() + + if err := cleanup(); err != nil { + slog.Warn("failed to cleanup previous ollama process", "err", err) + } + + reaped := false + for ctx.Err() == nil { + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(restartDelay): + } + + cmd, err := s.cmd(ctx) + if err != nil { + return err + } + + if err := cmd.Start(); err != nil { + return err + } + + err = os.WriteFile(pidFile, []byte(strconv.Itoa(cmd.Process.Pid)), 0o644) + if err != nil { + slog.Warn("failed to write pid file", "file", pidFile, "err", err) + } + + if err = cmd.Wait(); err != nil && !errors.Is(err, context.Canceled) { + var exitErr *exec.ExitError + if errors.As(err, &exitErr) && exitErr.ExitCode() == 1 && !s.dev && !reaped { + reaped = true + // This could be a port conflict, try to kill any existing ollama processes + if err := reapServers(); err != nil { + slog.Warn("failed to stop existing ollama server", "err", err) + } else { + slog.Debug("conflicting server stopped, waiting for port to be released") + continue + } + } + slog.Error("ollama exited", "err", err) + } + } + return ctx.Err() +} + +func (s *Server) cmd(ctx context.Context) (*exec.Cmd, error) { + settings, err := s.store.Settings() + if err != nil { + return nil, err + } + + cmd := commandContext(ctx, s.bin, "serve") + cmd.Stdout, cmd.Stderr = s.log, s.log + + // Copy and mutate the environment to merge in settings the user has specified without dups + env := map[string]string{} + for _, kv := range os.Environ() { + s := strings.SplitN(kv, "=", 2) + env[s[0]] = s[1] + } + if settings.Expose { + env["OLLAMA_HOST"] = "0.0.0.0" + } + if settings.Browser { + env["OLLAMA_ORIGINS"] = "*" + } + if settings.Models != "" { + if _, err := os.Stat(settings.Models); err == nil { + env["OLLAMA_MODELS"] = settings.Models + } else { + slog.Warn("models path not accessible, clearing models setting", "path", settings.Models, "err", err) + settings.Models = "" + s.store.SetSettings(settings) + } + } + if settings.ContextLength > 0 { + env["OLLAMA_CONTEXT_LENGTH"] = strconv.Itoa(settings.ContextLength) + } + cmd.Env = []string{} + for k, v := range env { + cmd.Env = append(cmd.Env, k+"="+v) + } + + cmd.Cancel = func() error { + if cmd.Process == nil { + return nil + } + return stop(cmd.Process) + } + + return cmd, nil +} + +func openRotatingLog() (io.WriteCloser, error) { + // TODO consider rotation based on size or time, not just every server invocation + dir := filepath.Dir(serverLogPath) + if err := os.MkdirAll(dir, 0o755); err != nil { + return nil, fmt.Errorf("create log directory: %w", err) + } + + logrotate.Rotate(serverLogPath) + f, err := os.OpenFile(serverLogPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644) + if err != nil { + return nil, fmt.Errorf("open log file: %w", err) + } + return f, nil +} + +// Attempt to retrieve inference compute information from the server +// log. Set ctx to timeout to control how long to wait for the logs to appear +func GetInferenceComputer(ctx context.Context) ([]InferenceCompute, error) { + inference := []InferenceCompute{} + marker := regexp.MustCompile(`inference compute.*library=`) + q := `inference compute.*%s=["]([^"]*)["]` + nq := `inference compute.*%s=(\S+)\s` + type regex struct { + q *regexp.Regexp + nq *regexp.Regexp + } + regexes := map[string]regex{ + "library": { + q: regexp.MustCompile(fmt.Sprintf(q, "library")), + nq: regexp.MustCompile(fmt.Sprintf(nq, "library")), + }, + "variant": { + q: regexp.MustCompile(fmt.Sprintf(q, "variant")), + nq: regexp.MustCompile(fmt.Sprintf(nq, "variant")), + }, + "compute": { + q: regexp.MustCompile(fmt.Sprintf(q, "compute")), + nq: regexp.MustCompile(fmt.Sprintf(nq, "compute")), + }, + "driver": { + q: regexp.MustCompile(fmt.Sprintf(q, "driver")), + nq: regexp.MustCompile(fmt.Sprintf(nq, "driver")), + }, + "name": { + q: regexp.MustCompile(fmt.Sprintf(q, "name")), + nq: regexp.MustCompile(fmt.Sprintf(nq, "name")), + }, + "total": { + q: regexp.MustCompile(fmt.Sprintf(q, "total")), + nq: regexp.MustCompile(fmt.Sprintf(nq, "total")), + }, + } + get := func(field, line string) string { + regex, ok := regexes[field] + if !ok { + slog.Warn("missing field", "field", field) + return "" + } + match := regex.q.FindStringSubmatch(line) + + if len(match) > 1 { + return match[1] + } + match = regex.nq.FindStringSubmatch(line) + if len(match) > 1 { + return match[1] + } + return "" + } + for { + select { + case <-ctx.Done(): + return nil, fmt.Errorf("timeout scanning server log for inference compute details") + default: + } + file, err := os.Open(serverLogPath) + if err != nil { + slog.Debug("failed to open server log", "log", serverLogPath, "error", err) + time.Sleep(time.Second) + continue + } + defer file.Close() + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := scanner.Text() + match := marker.FindStringSubmatch(line) + if len(match) > 0 { + ic := InferenceCompute{ + Library: get("library", line), + Variant: get("variant", line), + Compute: get("compute", line), + Driver: get("driver", line), + Name: get("name", line), + VRAM: get("total", line), + } + + slog.Info("Matched", "inference compute", ic) + inference = append(inference, ic) + } else { + // Break out on first non matching line after we start matching + if len(inference) > 0 { + return inference, nil + } + } + } + time.Sleep(100 * time.Millisecond) + } +} diff --git a/app/server/server_test.go b/app/server/server_test.go new file mode 100644 index 000000000..f533073d3 --- /dev/null +++ b/app/server/server_test.go @@ -0,0 +1,249 @@ +//go:build windows || darwin + +package server + +import ( + "context" + "os" + "path/filepath" + "reflect" + "strings" + "testing" + "time" + + "github.com/ollama/ollama/app/store" +) + +func TestNew(t *testing.T) { + tmpDir := t.TempDir() + st := &store.Store{DBPath: filepath.Join(tmpDir, "db.sqlite")} + defer st.Close() // Ensure database is closed before cleanup + s := New(st, false) + + if s == nil { + t.Fatal("expected non-nil server") + } + + if s.bin == "" { + t.Error("expected non-empty bin path") + } +} + +func TestServerCmd(t *testing.T) { + os.Unsetenv("OLLAMA_HOST") + os.Unsetenv("OLLAMA_ORIGINS") + os.Unsetenv("OLLAMA_MODELS") + var defaultModels string + home, err := os.UserHomeDir() + if err == nil { + defaultModels = filepath.Join(home, ".ollama", "models") + os.MkdirAll(defaultModels, 0o755) + } + + tmpModels := t.TempDir() + tests := []struct { + name string + settings store.Settings + want []string + dont []string + }{ + { + name: "default", + settings: store.Settings{}, + want: []string{"OLLAMA_MODELS=" + defaultModels}, + dont: []string{"OLLAMA_HOST=", "OLLAMA_ORIGINS="}, + }, + { + name: "expose", + settings: store.Settings{Expose: true}, + want: []string{"OLLAMA_HOST=0.0.0.0", "OLLAMA_MODELS=" + defaultModels}, + dont: []string{"OLLAMA_ORIGINS="}, + }, + { + name: "browser", + settings: store.Settings{Browser: true}, + want: []string{"OLLAMA_ORIGINS=*", "OLLAMA_MODELS=" + defaultModels}, + dont: []string{"OLLAMA_HOST="}, + }, + { + name: "models", + settings: store.Settings{Models: tmpModels}, + want: []string{"OLLAMA_MODELS=" + tmpModels}, + dont: []string{"OLLAMA_HOST=", "OLLAMA_ORIGINS="}, + }, + { + name: "inaccessible_models", + settings: store.Settings{Models: "/nonexistent/external/drive/models"}, + want: []string{}, + dont: []string{"OLLAMA_MODELS="}, + }, + { + name: "all", + settings: store.Settings{ + Expose: true, + Browser: true, + Models: tmpModels, + }, + want: []string{ + "OLLAMA_HOST=0.0.0.0", + "OLLAMA_ORIGINS=*", + "OLLAMA_MODELS=" + tmpModels, + }, + dont: []string{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tmpDir := t.TempDir() + st := &store.Store{DBPath: filepath.Join(tmpDir, "db.sqlite")} + defer st.Close() // Ensure database is closed before cleanup + st.SetSettings(tt.settings) + s := &Server{ + store: st, + } + + cmd, err := s.cmd(t.Context()) + if err != nil { + t.Fatalf("s.cmd() error = %v", err) + } + + for _, want := range tt.want { + found := false + for _, env := range cmd.Env { + if strings.Contains(env, want) { + found = true + break + } + } + if !found { + t.Errorf("expected environment variable containing %s", want) + } + } + + for _, dont := range tt.dont { + for _, env := range cmd.Env { + if strings.Contains(env, dont) { + t.Errorf("unexpected environment variable: %s", env) + } + } + } + + if cmd.Cancel == nil { + t.Error("expected non-nil cancel function") + } + }) + } +} + +func TestGetInferenceComputer(t *testing.T) { + tests := []struct { + name string + log string + exp []InferenceCompute + }{ + { + name: "metal", + log: `time=2025-06-30T09:23:07.374-07:00 level=DEBUG source=sched.go:108 msg="starting llm scheduler" +time=2025-06-30T09:23:07.416-07:00 level=INFO source=types.go:130 msg="inference compute" id=0 library=metal variant="" compute="" driver=0.0 name="" total="96.0 GiB" available="96.0 GiB" +time=2025-06-30T09:25:56.197-07:00 level=DEBUG source=ggml.go:155 msg="key not found" key=general.alignment default=32 +`, + exp: []InferenceCompute{{ + Library: "metal", + Driver: "0.0", + VRAM: "96.0 GiB", + }}, + }, + { + name: "cpu", + log: `time=2025-07-01T17:59:51.470Z level=INFO source=gpu.go:377 msg="no compatible GPUs were discovered" +time=2025-07-01T17:59:51.470Z level=INFO source=types.go:130 msg="inference compute" id=0 library=cpu variant="" compute="" driver=0.0 name="" total="31.3 GiB" available="30.4 GiB" +[GIN] 2025/07/01 - 18:00:09 | 200 | 50.263µs | 100.126.204.152 | HEAD "/" +`, + exp: []InferenceCompute{{ + Library: "cpu", + Driver: "0.0", + VRAM: "31.3 GiB", + }}, + }, + { + name: "cuda1", + log: `time=2025-07-01T19:33:43.162Z level=DEBUG source=amd_linux.go:419 msg="amdgpu driver not detected /sys/module/amdgpu" +releasing cuda driver library +time=2025-07-01T19:33:43.162Z level=INFO source=types.go:130 msg="inference compute" id=GPU-452cac9f-6960-839c-4fb3-0cec83699196 library=cuda variant=v12 compute=6.1 driver=12.7 name="NVIDIA GeForce GT 1030" total="3.9 GiB" available="3.9 GiB" +[GIN] 2025/07/01 - 18:00:09 | 200 | 50.263µs | 100.126.204.152 | HEAD "/" +`, + exp: []InferenceCompute{{ + Library: "cuda", + Variant: "v12", + Compute: "6.1", + Driver: "12.7", + Name: "NVIDIA GeForce GT 1030", + VRAM: "3.9 GiB", + }}, + }, + { + name: "frank", + log: `time=2025-07-01T19:36:13.315Z level=INFO source=amd_linux.go:386 msg="amdgpu is supported" gpu=GPU-9abb57639fa80c50 gpu_type=gfx1030 + releasing cuda driver library + time=2025-07-01T19:36:13.315Z level=INFO source=types.go:130 msg="inference compute" id=GPU-d6de3398-9932-6902-11ec-fee8e424c8a2 library=cuda variant=v12 compute=7.5 driver=12.8 name="NVIDIA GeForce RTX 2080 Ti" total="10.6 GiB" available="10.4 GiB" + time=2025-07-01T19:36:13.315Z level=INFO source=types.go:130 msg="inference compute" id=GPU-9abb57639fa80c50 library=rocm variant="" compute=gfx1030 driver=6.3 name=1002:73bf total="16.0 GiB" available="1.3 GiB" + [GIN] 2025/07/01 - 18:00:09 | 200 | 50.263µs | 100.126.204.152 | HEAD "/" + `, + exp: []InferenceCompute{ + { + Library: "cuda", + Variant: "v12", + Compute: "7.5", + Driver: "12.8", + Name: "NVIDIA GeForce RTX 2080 Ti", + VRAM: "10.6 GiB", + }, + { + Library: "rocm", + Compute: "gfx1030", + Driver: "6.3", + Name: "1002:73bf", + VRAM: "16.0 GiB", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tmpDir := t.TempDir() + serverLogPath = filepath.Join(tmpDir, "server.log") + err := os.WriteFile(serverLogPath, []byte(tt.log), 0o644) + if err != nil { + t.Fatalf("failed to write log file %s: %s", serverLogPath, err) + } + ctx, cancel := context.WithTimeout(t.Context(), 10*time.Millisecond) + defer cancel() + ics, err := GetInferenceComputer(ctx) + if err != nil { + t.Fatalf(" failed to get inference compute: %v", err) + } + if !reflect.DeepEqual(ics, tt.exp) { + t.Fatalf("got:\n%#v\nwant:\n%#v", ics, tt.exp) + } + }) + } +} + +func TestGetInferenceComputerTimeout(t *testing.T) { + ctx, cancel := context.WithTimeout(t.Context(), 10*time.Millisecond) + defer cancel() + tmpDir := t.TempDir() + serverLogPath = filepath.Join(tmpDir, "server.log") + err := os.WriteFile(serverLogPath, []byte("foo\nbar\nbaz\n"), 0o644) + if err != nil { + t.Fatalf("failed to write log file %s: %s", serverLogPath, err) + } + _, err = GetInferenceComputer(ctx) + if err == nil { + t.Fatal("expected timeout") + } + if !strings.Contains(err.Error(), "timeout") { + t.Fatalf("unexpected error: %s", err) + } +} diff --git a/app/server/server_unix.go b/app/server/server_unix.go new file mode 100644 index 000000000..2c50716b2 --- /dev/null +++ b/app/server/server_unix.go @@ -0,0 +1,104 @@ +//go:build darwin + +package server + +import ( + "context" + "errors" + "fmt" + "log/slog" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + "syscall" +) + +var ( + pidFile = filepath.Join(os.Getenv("HOME"), "Library", "Application Support", "Ollama", "ollama.pid") + serverLogPath = filepath.Join(os.Getenv("HOME"), ".ollama", "logs", "server.log") +) + +func commandContext(ctx context.Context, name string, arg ...string) *exec.Cmd { + return exec.CommandContext(ctx, name, arg...) +} + +func terminate(proc *os.Process) error { + return proc.Signal(os.Interrupt) +} + +func terminated(pid int) (bool, error) { + proc, err := os.FindProcess(pid) + if err != nil { + return false, fmt.Errorf("failed to find process: %v", err) + } + + err = proc.Signal(syscall.Signal(0)) + if err != nil { + if errors.Is(err, os.ErrProcessDone) || errors.Is(err, syscall.ESRCH) { + return true, nil + } + + return false, fmt.Errorf("error signaling process: %v", err) + } + + return false, nil +} + +// reapServers kills all ollama processes except our own +func reapServers() error { + // Get our own PID to avoid killing ourselves + currentPID := os.Getpid() + + // Use pkill to kill ollama processes + // -x matches the whole command name exactly + // We'll get the list first, then kill selectively + cmd := exec.Command("pgrep", "-x", "ollama") + output, err := cmd.Output() + if err != nil { + // No ollama processes found + slog.Debug("no ollama processes found") + return nil //nolint:nilerr + } + + pidsStr := strings.TrimSpace(string(output)) + if pidsStr == "" { + return nil + } + + pids := strings.Split(pidsStr, "\n") + for _, pidStr := range pids { + pidStr = strings.TrimSpace(pidStr) + if pidStr == "" { + continue + } + + pid, err := strconv.Atoi(pidStr) + if err != nil { + slog.Debug("failed to parse PID", "pidStr", pidStr, "err", err) + continue + } + if pid == currentPID { + continue + } + + proc, err := os.FindProcess(pid) + if err != nil { + slog.Debug("failed to find process", "pid", pid, "err", err) + continue + } + + if err := proc.Signal(syscall.SIGTERM); err != nil { + // Try SIGKILL if SIGTERM fails + if err := proc.Signal(syscall.SIGKILL); err != nil { + slog.Warn("failed to stop external ollama process", "pid", pid, "err", err) + continue + } + } + + slog.Info("stopped external ollama process", "pid", pid) + } + + return nil +} diff --git a/app/server/server_windows.go b/app/server/server_windows.go new file mode 100644 index 000000000..c2e7f4b9e --- /dev/null +++ b/app/server/server_windows.go @@ -0,0 +1,149 @@ +package server + +import ( + "context" + "fmt" + "log/slog" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + "syscall" + + "golang.org/x/sys/windows" +) + +var ( + pidFile = filepath.Join(os.Getenv("LOCALAPPDATA"), "Ollama", "ollama.pid") + serverLogPath = filepath.Join(os.Getenv("LOCALAPPDATA"), "Ollama", "server.log") +) + +func commandContext(ctx context.Context, name string, arg ...string) *exec.Cmd { + cmd := exec.CommandContext(ctx, name, arg...) + cmd.SysProcAttr = &syscall.SysProcAttr{ + HideWindow: true, + CreationFlags: windows.CREATE_NEW_PROCESS_GROUP, + } + + return cmd +} + +func terminate(proc *os.Process) error { + dll, err := windows.LoadDLL("kernel32.dll") + if err != nil { + return err + } + defer dll.Release() + + pid := proc.Pid + + f, err := dll.FindProc("AttachConsole") + if err != nil { + return err + } + + r1, _, err := f.Call(uintptr(pid)) + if r1 == 0 && err != syscall.ERROR_ACCESS_DENIED { + return err + } + + f, err = dll.FindProc("SetConsoleCtrlHandler") + if err != nil { + return err + } + + r1, _, err = f.Call(0, 1) + if r1 == 0 { + return err + } + + f, err = dll.FindProc("GenerateConsoleCtrlEvent") + if err != nil { + return err + } + + r1, _, err = f.Call(windows.CTRL_BREAK_EVENT, uintptr(pid)) + if r1 == 0 { + return err + } + + r1, _, err = f.Call(windows.CTRL_C_EVENT, uintptr(pid)) + if r1 == 0 { + return err + } + + return nil +} + +const STILL_ACTIVE = 259 + +func terminated(pid int) (bool, error) { + hProcess, err := windows.OpenProcess(windows.PROCESS_QUERY_INFORMATION, false, uint32(pid)) + if err != nil { + if errno, ok := err.(windows.Errno); ok && errno == windows.ERROR_INVALID_PARAMETER { + return true, nil + } + return false, fmt.Errorf("failed to open process: %v", err) + } + defer windows.CloseHandle(hProcess) + + var exitCode uint32 + err = windows.GetExitCodeProcess(hProcess, &exitCode) + if err != nil { + return false, fmt.Errorf("failed to get exit code: %v", err) + } + + if exitCode == STILL_ACTIVE { + return false, nil + } + + return true, nil +} + +// reapServers kills all ollama processes except our own +func reapServers() error { + // Get current process ID to avoid killing ourselves + currentPID := os.Getpid() + + // Use wmic to find ollama processes + cmd := exec.Command("wmic", "process", "where", "name='ollama.exe'", "get", "ProcessId") + cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true} + output, err := cmd.Output() + if err != nil { + // No ollama processes found + slog.Debug("no ollama processes found") + return nil //nolint:nilerr + } + + lines := strings.Split(string(output), "\n") + var pids []string + for _, line := range lines { + line = strings.TrimSpace(line) + if line == "" || line == "ProcessId" { + continue + } + + if _, err := strconv.Atoi(line); err == nil { + pids = append(pids, line) + } + } + + for _, pidStr := range pids { + pid, err := strconv.Atoi(pidStr) + if err != nil { + continue + } + + if pid == currentPID { + continue + } + + cmd := exec.Command("taskkill", "/F", "/PID", pidStr) + if err := cmd.Run(); err != nil { + slog.Warn("failed to kill ollama process", "pid", pid, "err", err) + } + } + + return nil +} diff --git a/app/store/database.go b/app/store/database.go new file mode 100644 index 000000000..0f268c6fa --- /dev/null +++ b/app/store/database.go @@ -0,0 +1,1222 @@ +//go:build windows || darwin + +package store + +import ( + "database/sql" + "encoding/json" + "fmt" + "strings" + "time" + + sqlite3 "github.com/mattn/go-sqlite3" +) + +// currentSchemaVersion defines the current database schema version. +// Increment this when making schema changes that require migrations. +const currentSchemaVersion = 12 + +// database wraps the SQLite connection. +// SQLite handles its own locking for concurrent access: +// - Multiple readers can access the database simultaneously +// - Writers are serialized (only one writer at a time) +// - WAL mode allows readers to not block writers +// This means we don't need application-level locks for database operations. +type database struct { + conn *sql.DB +} + +func newDatabase(dbPath string) (*database, error) { + // Open database connection + conn, err := sql.Open("sqlite3", dbPath+"?_foreign_keys=on&_journal_mode=WAL&_busy_timeout=5000&_txlock=immediate") + if err != nil { + return nil, fmt.Errorf("open database: %w", err) + } + + // Test the connection + if err := conn.Ping(); err != nil { + conn.Close() + return nil, fmt.Errorf("ping database: %w", err) + } + + db := &database{conn: conn} + + // Initialize schema + if err := db.init(); err != nil { + conn.Close() + return nil, fmt.Errorf("initialize database: %w", err) + } + + return db, nil +} + +func (db *database) Close() error { + _, _ = db.conn.Exec("PRAGMA wal_checkpoint(TRUNCATE);") + + return db.conn.Close() +} + +func (db *database) init() error { + if _, err := db.conn.Exec("PRAGMA foreign_keys = ON"); err != nil { + return fmt.Errorf("enable foreign keys: %w", err) + } + + schema := fmt.Sprintf(` + CREATE TABLE IF NOT EXISTS settings ( + id INTEGER PRIMARY KEY CHECK (id = 1), + device_id TEXT NOT NULL DEFAULT '', + has_completed_first_run BOOLEAN NOT NULL DEFAULT 0, + expose BOOLEAN NOT NULL DEFAULT 0, + survey BOOLEAN NOT NULL DEFAULT TRUE, + browser BOOLEAN NOT NULL DEFAULT 0, + models TEXT NOT NULL DEFAULT '', + agent BOOLEAN NOT NULL DEFAULT 0, + tools BOOLEAN NOT NULL DEFAULT 0, + working_dir TEXT NOT NULL DEFAULT '', + context_length INTEGER NOT NULL DEFAULT 4096, + window_width INTEGER NOT NULL DEFAULT 0, + window_height INTEGER NOT NULL DEFAULT 0, + config_migrated BOOLEAN NOT NULL DEFAULT 0, + airplane_mode BOOLEAN NOT NULL DEFAULT 0, + turbo_enabled BOOLEAN NOT NULL DEFAULT 0, + websearch_enabled BOOLEAN NOT NULL DEFAULT 0, + selected_model TEXT NOT NULL DEFAULT '', + sidebar_open BOOLEAN NOT NULL DEFAULT 0, + think_enabled BOOLEAN NOT NULL DEFAULT 0, + think_level TEXT NOT NULL DEFAULT '', + remote TEXT NOT NULL DEFAULT '', -- deprecated + schema_version INTEGER NOT NULL DEFAULT %d + ); + + -- Insert default settings row if it doesn't exist + INSERT OR IGNORE INTO settings (id) VALUES (1); + + CREATE TABLE IF NOT EXISTS chats ( + id TEXT PRIMARY KEY, + title TEXT NOT NULL DEFAULT '', + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + browser_state TEXT + ); + + CREATE TABLE IF NOT EXISTS messages ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + chat_id TEXT NOT NULL, + role TEXT NOT NULL, + content TEXT NOT NULL DEFAULT '', + thinking TEXT NOT NULL DEFAULT '', + stream BOOLEAN NOT NULL DEFAULT 0, + model_name TEXT, + model_cloud BOOLEAN, -- deprecated + model_ollama_host BOOLEAN, -- deprecated + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + thinking_time_start TIMESTAMP, + thinking_time_end TIMESTAMP, + tool_result TEXT, + FOREIGN KEY (chat_id) REFERENCES chats(id) ON DELETE CASCADE + ); + + CREATE INDEX IF NOT EXISTS idx_messages_chat_id ON messages(chat_id); + + CREATE TABLE IF NOT EXISTS tool_calls ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + message_id INTEGER NOT NULL, + type TEXT NOT NULL, + function_name TEXT NOT NULL, + function_arguments TEXT NOT NULL, + function_result TEXT, + FOREIGN KEY (message_id) REFERENCES messages(id) ON DELETE CASCADE + ); + + CREATE INDEX IF NOT EXISTS idx_tool_calls_message_id ON tool_calls(message_id); + + CREATE TABLE IF NOT EXISTS attachments ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + message_id INTEGER NOT NULL, + filename TEXT NOT NULL, + data BLOB NOT NULL, + FOREIGN KEY (message_id) REFERENCES messages(id) ON DELETE CASCADE + ); + + CREATE INDEX IF NOT EXISTS idx_attachments_message_id ON attachments(message_id); + + CREATE TABLE IF NOT EXISTS users ( + name TEXT NOT NULL DEFAULT '', + email TEXT NOT NULL DEFAULT '', + plan TEXT NOT NULL DEFAULT '', + cached_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP + ); + `, currentSchemaVersion) + + _, err := db.conn.Exec(schema) + if err != nil { + return err + } + + // Check and upgrade schema version if needed + if err := db.migrate(); err != nil { + return fmt.Errorf("migrate schema: %w", err) + } + + // Clean up orphaned records created before foreign key constraints were properly enforced + // TODO: Can eventually be removed - cleans up data from foreign key bug (ollama/ollama#11785, ollama/app#476) + if err := db.cleanupOrphanedData(); err != nil { + return fmt.Errorf("cleanup orphaned data: %w", err) + } + + return nil +} + +// migrate handles database schema migrations +func (db *database) migrate() error { + // Get current schema version + version, err := db.getSchemaVersion() + if err != nil { + return fmt.Errorf("get schema version after migration attempt: %w", err) + } + + // Run migrations for each version + for version < currentSchemaVersion { + switch version { + case 1: + // Migrate from version 1 to 2: add context_length column + if err := db.migrateV1ToV2(); err != nil { + return fmt.Errorf("migrate v1 to v2: %w", err) + } + version = 2 + case 2: + // Migrate from version 2 to 3: create attachments table + if err := db.migrateV2ToV3(); err != nil { + return fmt.Errorf("migrate v2 to v3: %w", err) + } + version = 3 + case 3: + // Migrate from version 3 to 4: add tool_result column to messages table + if err := db.migrateV3ToV4(); err != nil { + return fmt.Errorf("migrate v3 to v4: %w", err) + } + version = 4 + case 4: + // add airplane_mode column to settings table + if err := db.migrateV4ToV5(); err != nil { + return fmt.Errorf("migrate v4 to v5: %w", err) + } + version = 5 + case 5: + // add turbo_enabled column to settings table + if err := db.migrateV5ToV6(); err != nil { + return fmt.Errorf("migrate v5 to v6: %w", err) + } + version = 6 + case 6: + // add missing index for attachments table + if err := db.migrateV6ToV7(); err != nil { + return fmt.Errorf("migrate v6 to v7: %w", err) + } + version = 7 + case 7: + // add think_enabled and think_level columns to settings table + if err := db.migrateV7ToV8(); err != nil { + return fmt.Errorf("migrate v7 to v8: %w", err) + } + version = 8 + case 8: + // add browser_state column to chats table + if err := db.migrateV8ToV9(); err != nil { + return fmt.Errorf("migrate v8 to v9: %w", err) + } + version = 9 + case 9: + // add cached user table + if err := db.migrateV9ToV10(); err != nil { + return fmt.Errorf("migrate v9 to v10: %w", err) + } + version = 10 + case 10: + // remove remote column from settings table + if err := db.migrateV10ToV11(); err != nil { + return fmt.Errorf("migrate v10 to v11: %w", err) + } + version = 11 + case 11: + // bring back remote column for backwards compatibility (deprecated) + if err := db.migrateV11ToV12(); err != nil { + return fmt.Errorf("migrate v11 to v12: %w", err) + } + version = 12 + default: + // If we have a version we don't recognize, just set it to current + // This might happen during development + version = currentSchemaVersion + } + } + + return nil +} + +// migrateV1ToV2 adds the context_length column to the settings table +func (db *database) migrateV1ToV2() error { + _, err := db.conn.Exec(`ALTER TABLE settings ADD COLUMN context_length INTEGER NOT NULL DEFAULT 4096;`) + if err != nil && !duplicateColumnError(err) { + return fmt.Errorf("add context_length column: %w", err) + } + + _, err = db.conn.Exec(`ALTER TABLE settings ADD COLUMN survey BOOLEAN NOT NULL DEFAULT TRUE;`) + if err != nil && !duplicateColumnError(err) { + return fmt.Errorf("add survey column: %w", err) + } + + _, err = db.conn.Exec(`UPDATE settings SET schema_version = 2;`) + if err != nil { + return fmt.Errorf("update schema version: %w", err) + } + return nil +} + +// migrateV2ToV3 creates the attachments table +func (db *database) migrateV2ToV3() error { + _, err := db.conn.Exec(` + CREATE TABLE IF NOT EXISTS attachments ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + message_id INTEGER NOT NULL, + filename TEXT NOT NULL, + data BLOB NOT NULL, + FOREIGN KEY (message_id) REFERENCES messages(id) ON DELETE CASCADE + ) + `) + if err != nil { + return fmt.Errorf("create attachments table: %w", err) + } + + _, err = db.conn.Exec(`UPDATE settings SET schema_version = 3`) + if err != nil { + return fmt.Errorf("update schema version: %w", err) + } + + return nil +} + +func (db *database) migrateV3ToV4() error { + _, err := db.conn.Exec(`ALTER TABLE messages ADD COLUMN tool_result TEXT;`) + if err != nil && !duplicateColumnError(err) { + return fmt.Errorf("add tool_result column: %w", err) + } + + _, err = db.conn.Exec(`UPDATE settings SET schema_version = 4;`) + if err != nil { + return fmt.Errorf("update schema version: %w", err) + } + + return nil +} + +// migrateV4ToV5 adds the airplane_mode column to the settings table +func (db *database) migrateV4ToV5() error { + _, err := db.conn.Exec(`ALTER TABLE settings ADD COLUMN airplane_mode BOOLEAN NOT NULL DEFAULT 0;`) + if err != nil && !duplicateColumnError(err) { + return fmt.Errorf("add airplane_mode column: %w", err) + } + + _, err = db.conn.Exec(`UPDATE settings SET schema_version = 5;`) + if err != nil { + return fmt.Errorf("update schema version: %w", err) + } + + return nil +} + +// migrateV5ToV6 adds the turbo_enabled, websearch_enabled, selected_model, sidebar_open columns to the settings table +func (db *database) migrateV5ToV6() error { + _, err := db.conn.Exec(`ALTER TABLE settings ADD COLUMN turbo_enabled BOOLEAN NOT NULL DEFAULT 0;`) + if err != nil && !duplicateColumnError(err) { + return fmt.Errorf("add turbo_enabled column: %w", err) + } + + _, err = db.conn.Exec(`ALTER TABLE settings ADD COLUMN websearch_enabled BOOLEAN NOT NULL DEFAULT 0;`) + if err != nil && !duplicateColumnError(err) { + return fmt.Errorf("add websearch_enabled column: %w", err) + } + + _, err = db.conn.Exec(`ALTER TABLE settings ADD COLUMN selected_model TEXT NOT NULL DEFAULT '';`) + if err != nil && !duplicateColumnError(err) { + return fmt.Errorf("add selected_model column: %w", err) + } + + _, err = db.conn.Exec(`ALTER TABLE settings ADD COLUMN sidebar_open BOOLEAN NOT NULL DEFAULT 0;`) + if err != nil && !duplicateColumnError(err) { + return fmt.Errorf("add sidebar_open column: %w", err) + } + + _, err = db.conn.Exec(`UPDATE settings SET schema_version = 6;`) + if err != nil { + return fmt.Errorf("update schema version: %w", err) + } + + return nil +} + +// migrateV6ToV7 adds the missing index for the attachments table +func (db *database) migrateV6ToV7() error { + _, err := db.conn.Exec(`CREATE INDEX IF NOT EXISTS idx_attachments_message_id ON attachments(message_id);`) + if err != nil { + return fmt.Errorf("create attachments index: %w", err) + } + + _, err = db.conn.Exec(`UPDATE settings SET schema_version = 7;`) + if err != nil { + return fmt.Errorf("update schema version: %w", err) + } + + return nil +} + +// migrateV7ToV8 adds the think_enabled and think_level columns to the settings table +func (db *database) migrateV7ToV8() error { + _, err := db.conn.Exec(`ALTER TABLE settings ADD COLUMN think_enabled BOOLEAN NOT NULL DEFAULT 0;`) + if err != nil && !duplicateColumnError(err) { + return fmt.Errorf("add think_enabled column: %w", err) + } + + _, err = db.conn.Exec(`ALTER TABLE settings ADD COLUMN think_level TEXT NOT NULL DEFAULT '';`) + if err != nil && !duplicateColumnError(err) { + return fmt.Errorf("add think_level column: %w", err) + } + + _, err = db.conn.Exec(`UPDATE settings SET schema_version = 8;`) + if err != nil { + return fmt.Errorf("update schema version: %w", err) + } + + return nil +} + +// migrateV8ToV9 adds browser_state to chats and bumps schema +func (db *database) migrateV8ToV9() error { + _, err := db.conn.Exec(` + ALTER TABLE chats ADD COLUMN browser_state TEXT; + UPDATE settings SET schema_version = 9; + `) + + if err != nil && !duplicateColumnError(err) { + return fmt.Errorf("add browser_state column: %w", err) + } + + return nil +} + +// migrateV9ToV10 adds users table +func (db *database) migrateV9ToV10() error { + _, err := db.conn.Exec(` + CREATE TABLE IF NOT EXISTS users ( + name TEXT NOT NULL DEFAULT '', + email TEXT NOT NULL DEFAULT '', + plan TEXT NOT NULL DEFAULT '', + cached_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP + ); + UPDATE settings SET schema_version = 10; + `) + if err != nil { + return fmt.Errorf("create users table: %w", err) + } + + return nil +} + +// migrateV10ToV11 removes the remote column from the settings table +func (db *database) migrateV10ToV11() error { + _, err := db.conn.Exec(`ALTER TABLE settings DROP COLUMN remote`) + if err != nil && !columnNotExists(err) { + return fmt.Errorf("drop remote column: %w", err) + } + + _, err = db.conn.Exec(`UPDATE settings SET schema_version = 11`) + if err != nil { + return fmt.Errorf("update schema version: %w", err) + } + + return nil +} + +// migrateV11ToV12 brings back the remote column for backwards compatibility (deprecated) +func (db *database) migrateV11ToV12() error { + _, err := db.conn.Exec(`ALTER TABLE settings ADD COLUMN remote TEXT NOT NULL DEFAULT ''`) + if err != nil && !duplicateColumnError(err) { + return fmt.Errorf("add remote column: %w", err) + } + + _, err = db.conn.Exec(`UPDATE settings SET schema_version = 12`) + if err != nil { + return fmt.Errorf("update schema version: %w", err) + } + + return nil +} + +// cleanupOrphanedData removes orphaned records that may exist due to the foreign key bug +func (db *database) cleanupOrphanedData() error { + _, err := db.conn.Exec(` + DELETE FROM tool_calls + WHERE message_id NOT IN (SELECT id FROM messages) + `) + if err != nil { + return fmt.Errorf("cleanup orphaned tool_calls: %w", err) + } + + _, err = db.conn.Exec(` + DELETE FROM attachments + WHERE message_id NOT IN (SELECT id FROM messages) + `) + if err != nil { + return fmt.Errorf("cleanup orphaned attachments: %w", err) + } + + _, err = db.conn.Exec(` + DELETE FROM messages + WHERE chat_id NOT IN (SELECT id FROM chats) + `) + if err != nil { + return fmt.Errorf("cleanup orphaned messages: %w", err) + } + + return nil +} + +func duplicateColumnError(err error) bool { + if sqlite3Err, ok := err.(sqlite3.Error); ok { + return sqlite3Err.Code == sqlite3.ErrError && + strings.Contains(sqlite3Err.Error(), "duplicate column name") + } + return false +} + +func columnNotExists(err error) bool { + if sqlite3Err, ok := err.(sqlite3.Error); ok { + return sqlite3Err.Code == sqlite3.ErrError && + strings.Contains(sqlite3Err.Error(), "no such column") + } + return false +} + +func (db *database) getAllChats() ([]Chat, error) { + // Query chats with their first user message and latest update time + query := ` + SELECT + c.id, + c.title, + c.created_at, + COALESCE(first_msg.content, '') as first_user_content, + COALESCE(datetime(MAX(m.updated_at)), datetime(c.created_at)) as last_updated + FROM chats c + LEFT JOIN ( + SELECT chat_id, content, MIN(id) as min_id + FROM messages + WHERE role = 'user' + GROUP BY chat_id + ) first_msg ON c.id = first_msg.chat_id + LEFT JOIN messages m ON c.id = m.chat_id + GROUP BY c.id, c.title, c.created_at, first_msg.content + ORDER BY last_updated DESC + ` + + rows, err := db.conn.Query(query) + if err != nil { + return nil, fmt.Errorf("query chats: %w", err) + } + defer rows.Close() + + var chats []Chat + for rows.Next() { + var chat Chat + var createdAt time.Time + var firstUserContent string + var lastUpdatedStr string + + err := rows.Scan( + &chat.ID, + &chat.Title, + &createdAt, + &firstUserContent, + &lastUpdatedStr, + ) + + // Parse the last updated time + lastUpdated, _ := time.Parse("2006-01-02 15:04:05", lastUpdatedStr) + if err != nil { + return nil, fmt.Errorf("scan chat: %w", err) + } + + chat.CreatedAt = createdAt + + // Add a dummy first user message for the UI to display + // This is just for the excerpt, full messages are loaded when needed + chat.Messages = []Message{} + if firstUserContent != "" { + chat.Messages = append(chat.Messages, Message{ + Role: "user", + Content: firstUserContent, + UpdatedAt: lastUpdated, + }) + } + + chats = append(chats, chat) + } + + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("iterate chats: %w", err) + } + + return chats, nil +} + +func (db *database) getChatWithOptions(id string, loadAttachmentData bool) (*Chat, error) { + query := ` + SELECT id, title, created_at, browser_state + FROM chats + WHERE id = ? + ` + + var chat Chat + var createdAt time.Time + var browserState sql.NullString + + err := db.conn.QueryRow(query, id).Scan( + &chat.ID, + &chat.Title, + &createdAt, + &browserState, + ) + if err != nil { + if err == sql.ErrNoRows { + return nil, fmt.Errorf("chat not found") + } + return nil, fmt.Errorf("query chat: %w", err) + } + + chat.CreatedAt = createdAt + if browserState.Valid && browserState.String != "" { + var raw json.RawMessage + if err := json.Unmarshal([]byte(browserState.String), &raw); err == nil { + chat.BrowserState = raw + } + } + + messages, err := db.getMessages(id, loadAttachmentData) + if err != nil { + return nil, fmt.Errorf("get messages: %w", err) + } + chat.Messages = messages + + return &chat, nil +} + +func (db *database) saveChat(chat Chat) error { + tx, err := db.conn.Begin() + if err != nil { + return fmt.Errorf("begin transaction: %w", err) + } + defer tx.Rollback() + + // Use COALESCE for browser_state to avoid wiping an existing + // chat-level browser_state when saving a chat that doesn't include a new state payload. + // Many code paths call SetChat to update metadata/messages only; without COALESCE the + // UPSERT would overwrite browser_state with NULL, breaking revisit rendering that relies + // on the last persisted full tool state. + query := ` + INSERT INTO chats (id, title, created_at, browser_state) + VALUES (?, ?, ?, ?) + ON CONFLICT(id) DO UPDATE SET + title = excluded.title, + browser_state = COALESCE(excluded.browser_state, chats.browser_state) + ` + + var browserState sql.NullString + if chat.BrowserState != nil { + browserState = sql.NullString{String: string(chat.BrowserState), Valid: true} + } + + _, err = tx.Exec(query, + chat.ID, + chat.Title, + chat.CreatedAt, + browserState, + ) + if err != nil { + return fmt.Errorf("save chat: %w", err) + } + + // Delete existing messages (we'll re-insert all) + _, err = tx.Exec("DELETE FROM messages WHERE chat_id = ?", chat.ID) + if err != nil { + return fmt.Errorf("delete messages: %w", err) + } + + // Insert messages + for _, msg := range chat.Messages { + messageID, err := db.insertMessage(tx, chat.ID, msg) + if err != nil { + return fmt.Errorf("insert message: %w", err) + } + + // Insert tool calls if any + for _, toolCall := range msg.ToolCalls { + err := db.insertToolCall(tx, messageID, toolCall) + if err != nil { + return fmt.Errorf("insert tool call: %w", err) + } + } + } + + return tx.Commit() +} + +// updateChatBrowserState updates only the browser_state for a chat +func (db *database) updateChatBrowserState(chatID string, state json.RawMessage) error { + _, err := db.conn.Exec(`UPDATE chats SET browser_state = ? WHERE id = ?`, string(state), chatID) + if err != nil { + return fmt.Errorf("update chat browser state: %w", err) + } + return nil +} + +func (db *database) deleteChat(id string) error { + _, err := db.conn.Exec("DELETE FROM chats WHERE id = ?", id) + if err != nil { + return fmt.Errorf("delete chat: %w", err) + } + + _, _ = db.conn.Exec("PRAGMA wal_checkpoint(TRUNCATE);") + + return nil +} + +func (db *database) updateLastMessage(chatID string, msg Message) error { + tx, err := db.conn.Begin() + if err != nil { + return fmt.Errorf("begin transaction: %w", err) + } + defer tx.Rollback() + + // Get the ID of the last message + var messageID int64 + err = tx.QueryRow(` + SELECT MAX(id) FROM messages WHERE chat_id = ? + `, chatID).Scan(&messageID) + if err != nil { + return fmt.Errorf("get last message id: %w", err) + } + + query := ` + UPDATE messages + SET content = ?, thinking = ?, model_name = ?, updated_at = ?, thinking_time_start = ?, thinking_time_end = ?, tool_result = ? + WHERE id = ? + ` + + var thinkingTimeStart, thinkingTimeEnd sql.NullTime + if msg.ThinkingTimeStart != nil { + thinkingTimeStart = sql.NullTime{Time: *msg.ThinkingTimeStart, Valid: true} + } + if msg.ThinkingTimeEnd != nil { + thinkingTimeEnd = sql.NullTime{Time: *msg.ThinkingTimeEnd, Valid: true} + } + + var modelName sql.NullString + if msg.Model != "" { + modelName = sql.NullString{String: msg.Model, Valid: true} + } + + var toolResultJSON sql.NullString + if msg.ToolResult != nil { + resultBytes, err := json.Marshal(msg.ToolResult) + if err != nil { + return fmt.Errorf("marshal tool result: %w", err) + } + toolResultJSON = sql.NullString{String: string(resultBytes), Valid: true} + } + + result, err := tx.Exec(query, + msg.Content, + msg.Thinking, + modelName, + msg.UpdatedAt, + thinkingTimeStart, + thinkingTimeEnd, + toolResultJSON, + messageID, + ) + if err != nil { + return fmt.Errorf("update last message: %w", err) + } + + rowsAffected, err := result.RowsAffected() + if err != nil { + return fmt.Errorf("get rows affected: %w", err) + } + if rowsAffected == 0 { + return fmt.Errorf("no message found to update") + } + + _, err = tx.Exec("DELETE FROM attachments WHERE message_id = ?", messageID) + if err != nil { + return fmt.Errorf("delete existing attachments: %w", err) + } + for _, att := range msg.Attachments { + err := db.insertAttachment(tx, messageID, att) + if err != nil { + return fmt.Errorf("insert attachment: %w", err) + } + } + + _, err = tx.Exec("DELETE FROM tool_calls WHERE message_id = ?", messageID) + if err != nil { + return fmt.Errorf("delete existing tool calls: %w", err) + } + for _, toolCall := range msg.ToolCalls { + err := db.insertToolCall(tx, messageID, toolCall) + if err != nil { + return fmt.Errorf("insert tool call: %w", err) + } + } + + return tx.Commit() +} + +func (db *database) appendMessage(chatID string, msg Message) error { + tx, err := db.conn.Begin() + if err != nil { + return fmt.Errorf("begin transaction: %w", err) + } + defer tx.Rollback() + + messageID, err := db.insertMessage(tx, chatID, msg) + if err != nil { + return fmt.Errorf("insert message: %w", err) + } + + // Insert tool calls if any + for _, toolCall := range msg.ToolCalls { + err := db.insertToolCall(tx, messageID, toolCall) + if err != nil { + return fmt.Errorf("insert tool call: %w", err) + } + } + + return tx.Commit() +} + +func (db *database) getMessages(chatID string, loadAttachmentData bool) ([]Message, error) { + query := ` + SELECT id, role, content, thinking, stream, model_name, created_at, updated_at, thinking_time_start, thinking_time_end, tool_result + FROM messages + WHERE chat_id = ? + ORDER BY id ASC + ` + + rows, err := db.conn.Query(query, chatID) + if err != nil { + return nil, fmt.Errorf("query messages: %w", err) + } + defer rows.Close() + + var messages []Message + for rows.Next() { + var msg Message + var messageID int64 + var thinkingTimeStart, thinkingTimeEnd sql.NullTime + var modelName sql.NullString + var toolResult sql.NullString + + err := rows.Scan( + &messageID, + &msg.Role, + &msg.Content, + &msg.Thinking, + &msg.Stream, + &modelName, + &msg.CreatedAt, + &msg.UpdatedAt, + &thinkingTimeStart, + &thinkingTimeEnd, + &toolResult, + ) + if err != nil { + return nil, fmt.Errorf("scan message: %w", err) + } + + attachments, err := db.getAttachments(messageID, loadAttachmentData) + if err != nil { + return nil, fmt.Errorf("get attachments: %w", err) + } + msg.Attachments = attachments + + if thinkingTimeStart.Valid { + msg.ThinkingTimeStart = &thinkingTimeStart.Time + } + if thinkingTimeEnd.Valid { + msg.ThinkingTimeEnd = &thinkingTimeEnd.Time + } + + // Parse tool result from JSON if present + if toolResult.Valid && toolResult.String != "" { + var result json.RawMessage + if err := json.Unmarshal([]byte(toolResult.String), &result); err == nil { + msg.ToolResult = &result + } + } + + // Set model if present + if modelName.Valid && modelName.String != "" { + msg.Model = modelName.String + } + + // Get tool calls for this message + toolCalls, err := db.getToolCalls(messageID) + if err != nil { + return nil, fmt.Errorf("get tool calls: %w", err) + } + msg.ToolCalls = toolCalls + + messages = append(messages, msg) + } + + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("iterate messages: %w", err) + } + + return messages, nil +} + +func (db *database) insertMessage(tx *sql.Tx, chatID string, msg Message) (int64, error) { + query := ` + INSERT INTO messages (chat_id, role, content, thinking, stream, model_name, created_at, updated_at, thinking_time_start, thinking_time_end, tool_result) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ` + + var thinkingTimeStart, thinkingTimeEnd sql.NullTime + if msg.ThinkingTimeStart != nil { + thinkingTimeStart = sql.NullTime{Time: *msg.ThinkingTimeStart, Valid: true} + } + if msg.ThinkingTimeEnd != nil { + thinkingTimeEnd = sql.NullTime{Time: *msg.ThinkingTimeEnd, Valid: true} + } + + var modelName sql.NullString + if msg.Model != "" { + modelName = sql.NullString{String: msg.Model, Valid: true} + } + + var toolResultJSON sql.NullString + if msg.ToolResult != nil { + resultBytes, err := json.Marshal(msg.ToolResult) + if err != nil { + return 0, fmt.Errorf("marshal tool result: %w", err) + } + toolResultJSON = sql.NullString{String: string(resultBytes), Valid: true} + } + + result, err := tx.Exec(query, + chatID, + msg.Role, + msg.Content, + msg.Thinking, + msg.Stream, + modelName, + msg.CreatedAt, + msg.UpdatedAt, + thinkingTimeStart, + thinkingTimeEnd, + toolResultJSON, + ) + if err != nil { + return 0, err + } + + messageID, err := result.LastInsertId() + if err != nil { + return 0, err + } + + for _, att := range msg.Attachments { + err := db.insertAttachment(tx, messageID, att) + if err != nil { + return 0, fmt.Errorf("insert attachment: %w", err) + } + } + + return messageID, nil +} + +func (db *database) getAttachments(messageID int64, loadData bool) ([]File, error) { + var query string + if loadData { + query = ` + SELECT filename, data + FROM attachments + WHERE message_id = ? + ORDER BY id ASC + ` + } else { + query = ` + SELECT filename, '' as data + FROM attachments + WHERE message_id = ? + ORDER BY id ASC + ` + } + + rows, err := db.conn.Query(query, messageID) + if err != nil { + return nil, fmt.Errorf("query attachments: %w", err) + } + defer rows.Close() + + var attachments []File + for rows.Next() { + var file File + err := rows.Scan(&file.Filename, &file.Data) + if err != nil { + return nil, fmt.Errorf("scan attachment: %w", err) + } + attachments = append(attachments, file) + } + + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("iterate attachments: %w", err) + } + + return attachments, nil +} + +func (db *database) getToolCalls(messageID int64) ([]ToolCall, error) { + query := ` + SELECT type, function_name, function_arguments, function_result + FROM tool_calls + WHERE message_id = ? + ORDER BY id ASC + ` + + rows, err := db.conn.Query(query, messageID) + if err != nil { + return nil, fmt.Errorf("query tool calls: %w", err) + } + defer rows.Close() + + var toolCalls []ToolCall + for rows.Next() { + var tc ToolCall + var functionResult sql.NullString + + err := rows.Scan( + &tc.Type, + &tc.Function.Name, + &tc.Function.Arguments, + &functionResult, + ) + if err != nil { + return nil, fmt.Errorf("scan tool call: %w", err) + } + + if functionResult.Valid && functionResult.String != "" { + // Parse the JSON result + var result json.RawMessage + if err := json.Unmarshal([]byte(functionResult.String), &result); err == nil { + tc.Function.Result = &result + } + } + + toolCalls = append(toolCalls, tc) + } + + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("iterate tool calls: %w", err) + } + + return toolCalls, nil +} + +func (db *database) insertAttachment(tx *sql.Tx, messageID int64, file File) error { + query := ` + INSERT INTO attachments (message_id, filename, data) + VALUES (?, ?, ?) + ` + _, err := tx.Exec(query, messageID, file.Filename, file.Data) + return err +} + +func (db *database) insertToolCall(tx *sql.Tx, messageID int64, tc ToolCall) error { + query := ` + INSERT INTO tool_calls (message_id, type, function_name, function_arguments, function_result) + VALUES (?, ?, ?, ?, ?) + ` + + var functionResult sql.NullString + if tc.Function.Result != nil { + // Convert result to JSON + resultJSON, err := json.Marshal(tc.Function.Result) + if err != nil { + return fmt.Errorf("marshal tool result: %w", err) + } + functionResult = sql.NullString{String: string(resultJSON), Valid: true} + } + + _, err := tx.Exec(query, + messageID, + tc.Type, + tc.Function.Name, + tc.Function.Arguments, + functionResult, + ) + return err +} + +// Settings operations + +func (db *database) getID() (string, error) { + var id string + err := db.conn.QueryRow("SELECT device_id FROM settings").Scan(&id) + if err != nil { + return "", fmt.Errorf("get device id: %w", err) + } + return id, nil +} + +func (db *database) setID(id string) error { + _, err := db.conn.Exec("UPDATE settings SET device_id = ?", id) + if err != nil { + return fmt.Errorf("set device id: %w", err) + } + return nil +} + +func (db *database) getHasCompletedFirstRun() (bool, error) { + var hasCompletedFirstRun bool + err := db.conn.QueryRow("SELECT has_completed_first_run FROM settings").Scan(&hasCompletedFirstRun) + if err != nil { + return false, fmt.Errorf("get has completed first run: %w", err) + } + return hasCompletedFirstRun, nil +} + +func (db *database) setHasCompletedFirstRun(hasCompletedFirstRun bool) error { + _, err := db.conn.Exec("UPDATE settings SET has_completed_first_run = ?", hasCompletedFirstRun) + if err != nil { + return fmt.Errorf("set has completed first run: %w", err) + } + return nil +} + +func (db *database) getSettings() (Settings, error) { + var s Settings + + err := db.conn.QueryRow(` + SELECT expose, survey, browser, models, agent, tools, working_dir, context_length, airplane_mode, turbo_enabled, websearch_enabled, selected_model, sidebar_open, think_enabled, think_level + FROM settings + `).Scan(&s.Expose, &s.Survey, &s.Browser, &s.Models, &s.Agent, &s.Tools, &s.WorkingDir, &s.ContextLength, &s.AirplaneMode, &s.TurboEnabled, &s.WebSearchEnabled, &s.SelectedModel, &s.SidebarOpen, &s.ThinkEnabled, &s.ThinkLevel) + if err != nil { + return Settings{}, fmt.Errorf("get settings: %w", err) + } + + return s, nil +} + +func (db *database) setSettings(s Settings) error { + _, err := db.conn.Exec(` + UPDATE settings + SET expose = ?, survey = ?, browser = ?, models = ?, agent = ?, tools = ?, working_dir = ?, context_length = ?, airplane_mode = ?, turbo_enabled = ?, websearch_enabled = ?, selected_model = ?, sidebar_open = ?, think_enabled = ?, think_level = ? + `, s.Expose, s.Survey, s.Browser, s.Models, s.Agent, s.Tools, s.WorkingDir, s.ContextLength, s.AirplaneMode, s.TurboEnabled, s.WebSearchEnabled, s.SelectedModel, s.SidebarOpen, s.ThinkEnabled, s.ThinkLevel) + if err != nil { + return fmt.Errorf("set settings: %w", err) + } + return nil +} + +func (db *database) getWindowSize() (int, int, error) { + var width, height int + err := db.conn.QueryRow("SELECT window_width, window_height FROM settings").Scan(&width, &height) + if err != nil { + return 0, 0, fmt.Errorf("get window size: %w", err) + } + return width, height, nil +} + +func (db *database) setWindowSize(width, height int) error { + _, err := db.conn.Exec("UPDATE settings SET window_width = ?, window_height = ?", width, height) + if err != nil { + return fmt.Errorf("set window size: %w", err) + } + return nil +} + +func (db *database) isConfigMigrated() (bool, error) { + var migrated bool + err := db.conn.QueryRow("SELECT config_migrated FROM settings").Scan(&migrated) + if err != nil { + return false, fmt.Errorf("get config migrated: %w", err) + } + return migrated, nil +} + +func (db *database) setConfigMigrated(migrated bool) error { + _, err := db.conn.Exec("UPDATE settings SET config_migrated = ?", migrated) + if err != nil { + return fmt.Errorf("set config migrated: %w", err) + } + return nil +} + +func (db *database) getSchemaVersion() (int, error) { + var version int + err := db.conn.QueryRow("SELECT schema_version FROM settings").Scan(&version) + if err != nil { + return 0, fmt.Errorf("get schema version: %w", err) + } + return version, nil +} + +func (db *database) setSchemaVersion(version int) error { + _, err := db.conn.Exec("UPDATE settings SET schema_version = ?", version) + if err != nil { + return fmt.Errorf("set schema version: %w", err) + } + return nil +} + +func (db *database) getUser() (*User, error) { + var user User + err := db.conn.QueryRow(` + SELECT name, email, plan, cached_at + FROM users + LIMIT 1 + `).Scan(&user.Name, &user.Email, &user.Plan, &user.CachedAt) + if err != nil { + if err == sql.ErrNoRows { + return nil, nil // No user cached yet + } + return nil, fmt.Errorf("get user: %w", err) + } + + return &user, nil +} + +func (db *database) setUser(user User) error { + if err := db.clearUser(); err != nil { + return fmt.Errorf("before set: %w", err) + } + + _, err := db.conn.Exec(` + INSERT INTO users (name, email, plan, cached_at) + VALUES (?, ?, ?, ?) + `, user.Name, user.Email, user.Plan, user.CachedAt) + if err != nil { + return fmt.Errorf("set user: %w", err) + } + + return nil +} + +func (db *database) clearUser() error { + _, err := db.conn.Exec("DELETE FROM users") + if err != nil { + return fmt.Errorf("clear user: %w", err) + } + return nil +} diff --git a/app/store/database_test.go b/app/store/database_test.go new file mode 100644 index 000000000..1b037a75d --- /dev/null +++ b/app/store/database_test.go @@ -0,0 +1,407 @@ +//go:build windows || darwin + +package store + +import ( + "database/sql" + "fmt" + "os" + "path/filepath" + "sort" + "strings" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + _ "github.com/mattn/go-sqlite3" +) + +func TestSchemaMigrations(t *testing.T) { + t.Run("schema comparison after migration", func(t *testing.T) { + tmpDir := t.TempDir() + migratedDBPath := filepath.Join(tmpDir, "migrated.db") + migratedDB := loadV2Schema(t, migratedDBPath) + defer migratedDB.Close() + + if err := migratedDB.migrate(); err != nil { + t.Fatalf("migration failed: %v", err) + } + + // Create fresh database with current schema + freshDBPath := filepath.Join(tmpDir, "fresh.db") + freshDB, err := newDatabase(freshDBPath) + if err != nil { + t.Fatalf("failed to create fresh database: %v", err) + } + defer freshDB.Close() + + // Extract tables and indexes from both databases, directly comparing their schemas won't work due to ordering + migratedSchema := schemaMap(migratedDB) + freshSchema := schemaMap(freshDB) + + if !cmp.Equal(migratedSchema, freshSchema) { + t.Errorf("Schema difference found:\n%s", cmp.Diff(freshSchema, migratedSchema)) + } + + // Verify both databases have the same final schema version + migratedVersion, _ := migratedDB.getSchemaVersion() + freshVersion, _ := freshDB.getSchemaVersion() + if migratedVersion != freshVersion { + t.Errorf("schema version mismatch: migrated=%d, fresh=%d", migratedVersion, freshVersion) + } + }) + + t.Run("idempotent migrations", func(t *testing.T) { + tmpDir := t.TempDir() + dbPath := filepath.Join(tmpDir, "test.db") + db := loadV2Schema(t, dbPath) + defer db.Close() + + // Run migration twice + if err := db.migrate(); err != nil { + t.Fatalf("first migration failed: %v", err) + } + + if err := db.migrate(); err != nil { + t.Fatalf("second migration failed: %v", err) + } + + // Verify schema version is still correct + version, err := db.getSchemaVersion() + if err != nil { + t.Fatalf("failed to get schema version: %v", err) + } + if version != currentSchemaVersion { + t.Errorf("expected schema version %d after double migration, got %d", currentSchemaVersion, version) + } + }) + + t.Run("init database has correct schema version", func(t *testing.T) { + tmpDir := t.TempDir() + dbPath := filepath.Join(tmpDir, "test.db") + db, err := newDatabase(dbPath) + if err != nil { + t.Fatalf("failed to create database: %v", err) + } + defer db.Close() + + // Get the schema version from the newly initialized database + version, err := db.getSchemaVersion() + if err != nil { + t.Fatalf("failed to get schema version: %v", err) + } + + // Verify it matches the currentSchemaVersion constant + if version != currentSchemaVersion { + t.Errorf("expected schema version %d in initialized database, got %d", currentSchemaVersion, version) + } + }) +} + +func TestChatDeletionWithCascade(t *testing.T) { + t.Run("chat deletion cascades to related messages", func(t *testing.T) { + tmpDir := t.TempDir() + dbPath := filepath.Join(tmpDir, "test.db") + db, err := newDatabase(dbPath) + if err != nil { + t.Fatalf("failed to create database: %v", err) + } + defer db.Close() + + // Create test chat + testChatID := "test-chat-cascade-123" + testChat := Chat{ + ID: testChatID, + Title: "Test Chat for Cascade Delete", + CreatedAt: time.Now(), + Messages: []Message{ + { + Role: "user", + Content: "Hello, this is a test message", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + }, + { + Role: "assistant", + Content: "Hi there! This is a response.", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + }, + }, + } + + // Save the chat with messages + if err := db.saveChat(testChat); err != nil { + t.Fatalf("failed to save test chat: %v", err) + } + + // Verify chat and messages exist + chatCount := countRows(t, db, "chats") + messageCount := countRows(t, db, "messages") + + if chatCount != 1 { + t.Errorf("expected 1 chat, got %d", chatCount) + } + if messageCount != 2 { + t.Errorf("expected 2 messages, got %d", messageCount) + } + + // Verify specific chat exists + var exists bool + err = db.conn.QueryRow("SELECT EXISTS(SELECT 1 FROM chats WHERE id = ?)", testChatID).Scan(&exists) + if err != nil { + t.Fatalf("failed to check chat existence: %v", err) + } + if !exists { + t.Error("test chat should exist before deletion") + } + + // Verify messages exist for this chat + messageCountForChat := countRowsWithCondition(t, db, "messages", "chat_id = ?", testChatID) + if messageCountForChat != 2 { + t.Errorf("expected 2 messages for test chat, got %d", messageCountForChat) + } + + // Delete the chat + if err := db.deleteChat(testChatID); err != nil { + t.Fatalf("failed to delete chat: %v", err) + } + + // Verify chat is deleted + chatCountAfter := countRows(t, db, "chats") + if chatCountAfter != 0 { + t.Errorf("expected 0 chats after deletion, got %d", chatCountAfter) + } + + // Verify messages are CASCADE deleted + messageCountAfter := countRows(t, db, "messages") + if messageCountAfter != 0 { + t.Errorf("expected 0 messages after CASCADE deletion, got %d", messageCountAfter) + } + + // Verify specific chat no longer exists + err = db.conn.QueryRow("SELECT EXISTS(SELECT 1 FROM chats WHERE id = ?)", testChatID).Scan(&exists) + if err != nil { + t.Fatalf("failed to check chat existence after deletion: %v", err) + } + if exists { + t.Error("test chat should not exist after deletion") + } + + // Verify no orphaned messages remain + orphanedCount := countRowsWithCondition(t, db, "messages", "chat_id = ?", testChatID) + if orphanedCount != 0 { + t.Errorf("expected 0 orphaned messages, got %d", orphanedCount) + } + }) + + t.Run("foreign keys are enabled", func(t *testing.T) { + tmpDir := t.TempDir() + dbPath := filepath.Join(tmpDir, "test.db") + db, err := newDatabase(dbPath) + if err != nil { + t.Fatalf("failed to create database: %v", err) + } + defer db.Close() + + // Verify foreign keys are enabled + var foreignKeysEnabled int + err = db.conn.QueryRow("PRAGMA foreign_keys").Scan(&foreignKeysEnabled) + if err != nil { + t.Fatalf("failed to check foreign keys: %v", err) + } + if foreignKeysEnabled != 1 { + t.Errorf("expected foreign keys to be enabled (1), got %d", foreignKeysEnabled) + } + }) + + // This test is only relevant for v8 migrations, but we keep it here for now + // since it's a useful test to ensure that we don't introduce any new orphaned data + t.Run("cleanup orphaned data", func(t *testing.T) { + tmpDir := t.TempDir() + dbPath := filepath.Join(tmpDir, "test.db") + db, err := newDatabase(dbPath) + if err != nil { + t.Fatalf("failed to create database: %v", err) + } + defer db.Close() + + // First disable foreign keys to simulate the bug from ollama/ollama#11785 + _, err = db.conn.Exec("PRAGMA foreign_keys = OFF") + if err != nil { + t.Fatalf("failed to disable foreign keys: %v", err) + } + + // Create a chat and message + testChatID := "orphaned-test-chat" + testMessageID := int64(999) + + _, err = db.conn.Exec("INSERT INTO chats (id, title) VALUES (?, ?)", testChatID, "Orphaned Test Chat") + if err != nil { + t.Fatalf("failed to insert test chat: %v", err) + } + + _, err = db.conn.Exec("INSERT INTO messages (id, chat_id, role, content) VALUES (?, ?, ?, ?)", + testMessageID, testChatID, "user", "test message") + if err != nil { + t.Fatalf("failed to insert test message: %v", err) + } + + // Delete chat but keep message (simulating the bug from ollama/ollama#11785) + _, err = db.conn.Exec("DELETE FROM chats WHERE id = ?", testChatID) + if err != nil { + t.Fatalf("failed to delete chat: %v", err) + } + + // Verify we have orphaned message + orphanedCount := countRowsWithCondition(t, db, "messages", "chat_id = ?", testChatID) + if orphanedCount != 1 { + t.Errorf("expected 1 orphaned message, got %d", orphanedCount) + } + + // Run cleanup + if err := db.cleanupOrphanedData(); err != nil { + t.Fatalf("failed to cleanup orphaned data: %v", err) + } + + // Verify orphaned message is gone + orphanedCountAfter := countRowsWithCondition(t, db, "messages", "chat_id = ?", testChatID) + if orphanedCountAfter != 0 { + t.Errorf("expected 0 orphaned messages after cleanup, got %d", orphanedCountAfter) + } + }) +} + +func countRows(t *testing.T, db *database, table string) int { + t.Helper() + var count int + err := db.conn.QueryRow(fmt.Sprintf("SELECT COUNT(*) FROM %s", table)).Scan(&count) + if err != nil { + t.Fatalf("failed to count rows in %s: %v", table, err) + } + return count +} + +func countRowsWithCondition(t *testing.T, db *database, table, condition string, args ...interface{}) int { + t.Helper() + var count int + query := fmt.Sprintf("SELECT COUNT(*) FROM %s WHERE %s", table, condition) + err := db.conn.QueryRow(query, args...).Scan(&count) + if err != nil { + t.Fatalf("failed to count rows with condition: %v", err) + } + return count +} + +// Test helpers for schema migration testing + +// schemaMap returns both tables/columns and indexes (ignoring order) +func schemaMap(db *database) map[string]interface{} { + result := make(map[string]any) + + result["tables"] = columnMap(db) + result["indexes"] = indexMap(db) + + return result +} + +// columnMap returns a map of table names to their column sets (ignoring order) +func columnMap(db *database) map[string][]string { + result := make(map[string][]string) + + // Get all table names + tableQuery := `SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name` + rows, _ := db.conn.Query(tableQuery) + defer rows.Close() + + for rows.Next() { + var tableName string + rows.Scan(&tableName) + + // Get columns for this table + colQuery := fmt.Sprintf("PRAGMA table_info(%s)", tableName) + colRows, _ := db.conn.Query(colQuery) + + var columns []string + for colRows.Next() { + var cid int + var name, dataType sql.NullString + var notNull, primaryKey int + var defaultValue sql.NullString + + colRows.Scan(&cid, &name, &dataType, ¬Null, &defaultValue, &primaryKey) + + // Create a normalized column description + colDesc := fmt.Sprintf("%s %s", name.String, dataType.String) + if notNull == 1 { + colDesc += " NOT NULL" + } + if defaultValue.Valid && defaultValue.String != "" { + // Skip DEFAULT for schema_version as it doesn't get updated during migrations + if name.String != "schema_version" { + colDesc += " DEFAULT " + defaultValue.String + } + } + if primaryKey == 1 { + colDesc += " PRIMARY KEY" + } + + columns = append(columns, colDesc) + } + colRows.Close() + + // Sort columns to ignore order differences + sort.Strings(columns) + result[tableName] = columns + } + + return result +} + +// indexMap returns a map of index names to their definitions +func indexMap(db *database) map[string]string { + result := make(map[string]string) + + // Get all indexes (excluding auto-created primary key indexes) + indexQuery := `SELECT name, sql FROM sqlite_master WHERE type='index' AND name NOT LIKE 'sqlite_%' AND sql IS NOT NULL ORDER BY name` + rows, _ := db.conn.Query(indexQuery) + defer rows.Close() + + for rows.Next() { + var name, sql string + rows.Scan(&name, &sql) + + // Normalize the SQL by removing extra whitespace + sql = strings.Join(strings.Fields(sql), " ") + result[name] = sql + } + + return result +} + +// loadV2Schema loads the version 2 schema from testdata/schema.sql +func loadV2Schema(t *testing.T, dbPath string) *database { + t.Helper() + + // Read the v1 schema file + schemaFile := filepath.Join("testdata", "schema.sql") + schemaSQL, err := os.ReadFile(schemaFile) + if err != nil { + t.Fatalf("failed to read schema file: %v", err) + } + + // Open database connection + conn, err := sql.Open("sqlite3", dbPath+"?_foreign_keys=on&_journal_mode=WAL&_busy_timeout=5000&_txlock=immediate") + if err != nil { + t.Fatalf("failed to open database: %v", err) + } + + // Execute the v1 schema + _, err = conn.Exec(string(schemaSQL)) + if err != nil { + conn.Close() + t.Fatalf("failed to execute v1 schema: %v", err) + } + + return &database{conn: conn} +} diff --git a/app/store/image.go b/app/store/image.go new file mode 100644 index 000000000..c7e6f9fd8 --- /dev/null +++ b/app/store/image.go @@ -0,0 +1,128 @@ +//go:build windows || darwin + +package store + +import ( + "crypto/sha256" + "encoding/hex" + "fmt" + "os" + "path/filepath" + "strings" +) + +type Image struct { + Filename string `json:"filename"` + Path string `json:"path"` + Size int64 `json:"size,omitempty"` + MimeType string `json:"mime_type,omitempty"` +} + +// Bytes loads image data from disk for a given ImageData reference +func (i *Image) Bytes() ([]byte, error) { + return ImgBytes(i.Path) +} + +// ImgBytes reads image data from the specified file path +func ImgBytes(path string) ([]byte, error) { + if path == "" { + return nil, fmt.Errorf("empty image path") + } + + data, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("read image file %s: %w", path, err) + } + + return data, nil +} + +// ImgDir returns the directory path for storing images for a specific chat +func (s *Store) ImgDir() string { + dbPath := s.DBPath + if dbPath == "" { + dbPath = defaultDBPath + } + storeDir := filepath.Dir(dbPath) + return filepath.Join(storeDir, "cache", "images") +} + +// ImgToFile saves image data to disk and returns ImageData reference +func (s *Store) ImgToFile(chatID string, imageBytes []byte, filename, mimeType string) (Image, error) { + baseImageDir := s.ImgDir() + if err := os.MkdirAll(baseImageDir, 0o755); err != nil { + return Image{}, fmt.Errorf("create base image directory: %w", err) + } + + // Root prevents path traversal issues + root, err := os.OpenRoot(baseImageDir) + if err != nil { + return Image{}, fmt.Errorf("open image root directory: %w", err) + } + defer root.Close() + + // Create chat-specific subdirectory within the root + chatDir := sanitize(chatID) + if err := root.Mkdir(chatDir, 0o755); err != nil && !os.IsExist(err) { + return Image{}, fmt.Errorf("create chat directory: %w", err) + } + + // Generate a unique filename to avoid conflicts + // Use hash of content + original filename for uniqueness + hash := sha256.Sum256(imageBytes) + hashStr := hex.EncodeToString(hash[:])[:16] // Use first 16 chars of hash + + // Extract file extension from original filename or mime type + ext := filepath.Ext(filename) + if ext == "" { + switch mimeType { + case "image/jpeg": + ext = ".jpg" + case "image/png": + ext = ".png" + case "image/webp": + ext = ".webp" + default: + ext = ".img" + } + } + + // Create unique filename: hash + original name + extension + baseFilename := sanitize(strings.TrimSuffix(filename, ext)) + uniqueFilename := fmt.Sprintf("%s_%s%s", hashStr, baseFilename, ext) + relativePath := filepath.Join(chatDir, uniqueFilename) + file, err := root.Create(relativePath) + if err != nil { + return Image{}, fmt.Errorf("create image file: %w", err) + } + defer file.Close() + + if _, err := file.Write(imageBytes); err != nil { + return Image{}, fmt.Errorf("write image data: %w", err) + } + + return Image{ + Filename: uniqueFilename, + Path: filepath.Join(baseImageDir, relativePath), + Size: int64(len(imageBytes)), + MimeType: mimeType, + }, nil +} + +// sanitize removes unsafe characters from filenames +func sanitize(filename string) string { + // Convert to safe characters only + safe := strings.Map(func(r rune) rune { + if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '-' { + return r + } + return '_' + }, filename) + + // Clean up and validate + safe = strings.Trim(safe, "_") + if safe == "" { + return "image" + } + return safe +} diff --git a/app/store/migration_test.go b/app/store/migration_test.go new file mode 100644 index 000000000..57b37b706 --- /dev/null +++ b/app/store/migration_test.go @@ -0,0 +1,231 @@ +//go:build windows || darwin + +package store + +import ( + "database/sql" + "encoding/json" + "os" + "path/filepath" + "testing" +) + +func TestConfigMigration(t *testing.T) { + tmpDir := t.TempDir() + // Create a legacy config.json + legacyConfig := legacyData{ + ID: "test-device-id-12345", + FirstTimeRun: true, // In old system, true meant "has completed first run" + } + + configData, err := json.MarshalIndent(legacyConfig, "", " ") + if err != nil { + t.Fatal(err) + } + + configPath := filepath.Join(tmpDir, "config.json") + if err := os.WriteFile(configPath, configData, 0o644); err != nil { + t.Fatal(err) + } + + // Override the legacy config path for testing + oldLegacyConfigPath := legacyConfigPath + legacyConfigPath = configPath + defer func() { legacyConfigPath = oldLegacyConfigPath }() + + // Create store with database in same directory + s := Store{DBPath: filepath.Join(tmpDir, "db.sqlite")} + defer s.Close() + + // First access should trigger migration + id, err := s.ID() + if err != nil { + t.Fatalf("failed to get ID: %v", err) + } + + if id != "test-device-id-12345" { + t.Errorf("expected migrated ID 'test-device-id-12345', got '%s'", id) + } + + // Check HasCompletedFirstRun + hasCompleted, err := s.HasCompletedFirstRun() + if err != nil { + t.Fatalf("failed to get has completed first run: %v", err) + } + + if !hasCompleted { + t.Error("expected has completed first run to be true after migration") + } + + // Verify migration is marked as complete + migrated, err := s.db.isConfigMigrated() + if err != nil { + t.Fatalf("failed to check migration status: %v", err) + } + + if !migrated { + t.Error("expected config to be marked as migrated") + } + + // Create a new store instance to verify migration doesn't run again + s2 := Store{DBPath: filepath.Join(tmpDir, "db.sqlite")} + defer s2.Close() + + // Delete the config file to ensure we're not reading from it + os.Remove(configPath) + + // Verify data is still there + id2, err := s2.ID() + if err != nil { + t.Fatalf("failed to get ID from second store: %v", err) + } + + if id2 != "test-device-id-12345" { + t.Errorf("expected persisted ID 'test-device-id-12345', got '%s'", id2) + } +} + +func TestNoConfigToMigrate(t *testing.T) { + tmpDir := t.TempDir() + // Override the legacy config path for testing + oldLegacyConfigPath := legacyConfigPath + legacyConfigPath = filepath.Join(tmpDir, "config.json") + defer func() { legacyConfigPath = oldLegacyConfigPath }() + + // Create store without any config.json + s := Store{DBPath: filepath.Join(tmpDir, "db.sqlite")} + defer s.Close() + + // Should generate a new ID + id, err := s.ID() + if err != nil { + t.Fatalf("failed to get ID: %v", err) + } + + if id == "" { + t.Error("expected auto-generated ID, got empty string") + } + + // HasCompletedFirstRun should be false (default) + hasCompleted, err := s.HasCompletedFirstRun() + if err != nil { + t.Fatalf("failed to get has completed first run: %v", err) + } + + if hasCompleted { + t.Error("expected has completed first run to be false by default") + } + + // Migration should still be marked as complete + migrated, err := s.db.isConfigMigrated() + if err != nil { + t.Fatalf("failed to check migration status: %v", err) + } + + if !migrated { + t.Error("expected config to be marked as migrated even with no config.json") + } +} + +const ( + v1Schema = ` + CREATE TABLE IF NOT EXISTS settings ( + id INTEGER PRIMARY KEY CHECK (id = 1), + device_id TEXT NOT NULL DEFAULT '', + has_completed_first_run BOOLEAN NOT NULL DEFAULT 0, + expose BOOLEAN NOT NULL DEFAULT 0, + browser BOOLEAN NOT NULL DEFAULT 0, + models TEXT NOT NULL DEFAULT '', + remote TEXT NOT NULL DEFAULT '', + agent BOOLEAN NOT NULL DEFAULT 0, + tools BOOLEAN NOT NULL DEFAULT 0, + working_dir TEXT NOT NULL DEFAULT '', + window_width INTEGER NOT NULL DEFAULT 0, + window_height INTEGER NOT NULL DEFAULT 0, + config_migrated BOOLEAN NOT NULL DEFAULT 0, + schema_version INTEGER NOT NULL DEFAULT 1 + ); + + -- Insert default settings row if it doesn't exist + INSERT OR IGNORE INTO settings (id) VALUES (1); + + CREATE TABLE IF NOT EXISTS chats ( + id TEXT PRIMARY KEY, + title TEXT NOT NULL DEFAULT '', + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP + ); + + CREATE TABLE IF NOT EXISTS messages ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + chat_id TEXT NOT NULL, + role TEXT NOT NULL, + content TEXT NOT NULL DEFAULT '', + thinking TEXT NOT NULL DEFAULT '', + stream BOOLEAN NOT NULL DEFAULT 0, + model_name TEXT, + model_cloud BOOLEAN, + model_ollama_host BOOLEAN, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + thinking_time_start TIMESTAMP, + thinking_time_end TIMESTAMP, + FOREIGN KEY (chat_id) REFERENCES chats(id) ON DELETE CASCADE + ); + + CREATE INDEX IF NOT EXISTS idx_messages_chat_id ON messages(chat_id); + + CREATE TABLE IF NOT EXISTS tool_calls ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + message_id INTEGER NOT NULL, + type TEXT NOT NULL, + function_name TEXT NOT NULL, + function_arguments TEXT NOT NULL, + function_result TEXT, + FOREIGN KEY (message_id) REFERENCES messages(id) ON DELETE CASCADE + ); + + CREATE INDEX IF NOT EXISTS idx_tool_calls_message_id ON tool_calls(message_id); + ` +) + +func TestMigrationFromEpoc(t *testing.T) { + tmpDir := t.TempDir() + s := Store{DBPath: filepath.Join(tmpDir, "db.sqlite")} + defer s.Close() + // Open database connection + conn, err := sql.Open("sqlite3", s.DBPath+"?_foreign_keys=on&_journal_mode=WAL") + if err != nil { + t.Fatal(err) + } + // Test the connection + if err := conn.Ping(); err != nil { + conn.Close() + t.Fatal(err) + } + s.db = &database{conn: conn} + t.Logf("DB created: %s", s.DBPath) + _, err = s.db.conn.Exec(v1Schema) + if err != nil { + t.Fatal(err) + } + version, err := s.db.getSchemaVersion() + if err != nil { + t.Fatalf("failed to get schema version: %v", err) + } + if version != 1 { + t.Fatalf("expected: %d\n got: %d", 1, version) + } + + t.Logf("v1 schema created") + if err := s.db.migrate(); err != nil { + t.Fatal(err) + } + t.Logf("migrations completed") + version, err = s.db.getSchemaVersion() + if err != nil { + t.Fatalf("failed to get schema version: %v", err) + } + if version != currentSchemaVersion { + t.Fatalf("expected: %d\n got: %d", currentSchemaVersion, version) + } +} diff --git a/app/store/schema.sql b/app/store/schema.sql new file mode 100644 index 000000000..8f944ff85 --- /dev/null +++ b/app/store/schema.sql @@ -0,0 +1,61 @@ +-- This is the version 2 schema for the app database, the first released schema to users. +-- Do not modify this file. It is used to test that the database schema stays in a consistent state between schema migrations. + +CREATE TABLE IF NOT EXISTS settings ( + id INTEGER PRIMARY KEY CHECK (id = 1), + device_id TEXT NOT NULL DEFAULT '', + has_completed_first_run BOOLEAN NOT NULL DEFAULT 0, + expose BOOLEAN NOT NULL DEFAULT 0, + survey BOOLEAN NOT NULL DEFAULT TRUE, + browser BOOLEAN NOT NULL DEFAULT 0, + models TEXT NOT NULL DEFAULT '', + remote TEXT NOT NULL DEFAULT '', + agent BOOLEAN NOT NULL DEFAULT 0, + tools BOOLEAN NOT NULL DEFAULT 0, + working_dir TEXT NOT NULL DEFAULT '', + context_length INTEGER NOT NULL DEFAULT 4096, + window_width INTEGER NOT NULL DEFAULT 0, + window_height INTEGER NOT NULL DEFAULT 0, + config_migrated BOOLEAN NOT NULL DEFAULT 0, + schema_version INTEGER NOT NULL DEFAULT 2 +); + +-- Insert default settings row if it doesn't exist +INSERT OR IGNORE INTO settings (id) VALUES (1); + +CREATE TABLE IF NOT EXISTS chats ( + id TEXT PRIMARY KEY, + title TEXT NOT NULL DEFAULT '', + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS messages ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + chat_id TEXT NOT NULL, + role TEXT NOT NULL, + content TEXT NOT NULL DEFAULT '', + thinking TEXT NOT NULL DEFAULT '', + stream BOOLEAN NOT NULL DEFAULT 0, + model_name TEXT, + model_cloud BOOLEAN, + model_ollama_host BOOLEAN, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + thinking_time_start TIMESTAMP, + thinking_time_end TIMESTAMP, + FOREIGN KEY (chat_id) REFERENCES chats(id) ON DELETE CASCADE +); + +CREATE INDEX IF NOT EXISTS idx_messages_chat_id ON messages(chat_id); + +CREATE TABLE IF NOT EXISTS tool_calls ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + message_id INTEGER NOT NULL, + type TEXT NOT NULL, + function_name TEXT NOT NULL, + function_arguments TEXT NOT NULL, + function_result TEXT, + FOREIGN KEY (message_id) REFERENCES messages(id) ON DELETE CASCADE +); + +CREATE INDEX IF NOT EXISTS idx_tool_calls_message_id ON tool_calls(message_id); diff --git a/app/store/schema_test.go b/app/store/schema_test.go new file mode 100644 index 000000000..a6d469000 --- /dev/null +++ b/app/store/schema_test.go @@ -0,0 +1,60 @@ +//go:build windows || darwin + +package store + +import ( + "path/filepath" + "testing" +) + +func TestSchemaVersioning(t *testing.T) { + tmpDir := t.TempDir() + // Override legacy config path to avoid migration logs + oldLegacyConfigPath := legacyConfigPath + legacyConfigPath = filepath.Join(tmpDir, "config.json") + defer func() { legacyConfigPath = oldLegacyConfigPath }() + + t.Run("new database has correct schema version", func(t *testing.T) { + dbPath := filepath.Join(tmpDir, "new_db.sqlite") + db, err := newDatabase(dbPath) + if err != nil { + t.Fatalf("failed to create database: %v", err) + } + defer db.Close() + + // Check schema version + version, err := db.getSchemaVersion() + if err != nil { + t.Fatalf("failed to get schema version: %v", err) + } + + if version != currentSchemaVersion { + t.Errorf("expected schema version %d, got %d", currentSchemaVersion, version) + } + }) + + t.Run("can update schema version", func(t *testing.T) { + dbPath := filepath.Join(tmpDir, "update_db.sqlite") + db, err := newDatabase(dbPath) + if err != nil { + t.Fatalf("failed to create database: %v", err) + } + defer db.Close() + + // Set a different version + testVersion := 42 + if err := db.setSchemaVersion(testVersion); err != nil { + t.Fatalf("failed to set schema version: %v", err) + } + + // Verify it was updated + version, err := db.getSchemaVersion() + if err != nil { + t.Fatalf("failed to get schema version: %v", err) + } + + if version != testVersion { + t.Errorf("expected schema version %d, got %d", testVersion, version) + } + }) +} diff --git a/app/store/store.go b/app/store/store.go index 370436c58..052fcd617 100644 --- a/app/store/store.go +++ b/app/store/store.go @@ -1,97 +1,495 @@ +//go:build windows || darwin + +// Package store provides a simple JSON file store for the desktop application +// to save and load data such as ollama server configuration, messages, +// login information and more. package store import ( "encoding/json" - "errors" "fmt" "log/slog" "os" "path/filepath" + "runtime" "sync" + "time" "github.com/google/uuid" + "github.com/ollama/ollama/app/types/not" ) +type File struct { + Filename string `json:"filename"` + Data []byte `json:"data"` +} + +type User struct { + Name string `json:"name"` + Email string `json:"email"` + Plan string `json:"plan"` + CachedAt time.Time `json:"cachedAt"` +} + +type Message struct { + Role string `json:"role"` + Content string `json:"content"` + Thinking string `json:"thinking"` + Stream bool `json:"stream"` + Model string `json:"model,omitempty"` + Attachments []File `json:"attachments,omitempty"` + ToolCalls []ToolCall `json:"tool_calls,omitempty"` + ToolCall *ToolCall `json:"tool_call,omitempty"` + ToolName string `json:"tool_name,omitempty"` + ToolResult *json.RawMessage `json:"tool_result,omitempty"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + ThinkingTimeStart *time.Time `json:"thinkingTimeStart,omitempty" ts_type:"Date | undefined" ts_transform:"__VALUE__ && new Date(__VALUE__)"` + ThinkingTimeEnd *time.Time `json:"thinkingTimeEnd,omitempty" ts_type:"Date | undefined" ts_transform:"__VALUE__ && new Date(__VALUE__)"` +} + +// MessageOptions contains optional parameters for creating a Message +type MessageOptions struct { + Model string + Attachments []File + Stream bool + Thinking string + ToolCalls []ToolCall + ToolCall *ToolCall + ToolResult *json.RawMessage + ThinkingTimeStart *time.Time + ThinkingTimeEnd *time.Time +} + +// NewMessage creates a new Message with the given options +func NewMessage(role, content string, opts *MessageOptions) Message { + now := time.Now() + msg := Message{ + Role: role, + Content: content, + CreatedAt: now, + UpdatedAt: now, + } + + if opts != nil { + msg.Model = opts.Model + msg.Attachments = opts.Attachments + msg.Stream = opts.Stream + msg.Thinking = opts.Thinking + msg.ToolCalls = opts.ToolCalls + msg.ToolCall = opts.ToolCall + msg.ToolResult = opts.ToolResult + msg.ThinkingTimeStart = opts.ThinkingTimeStart + msg.ThinkingTimeEnd = opts.ThinkingTimeEnd + } + + return msg +} + +type ToolCall struct { + Type string `json:"type"` + Function ToolFunction `json:"function"` +} + +type ToolFunction struct { + Name string `json:"name"` + Arguments string `json:"arguments"` + Result any `json:"result,omitempty"` +} + +type Model struct { + Model string `json:"model"` // Model name + Digest string `json:"digest,omitempty"` // Model digest from the registry + ModifiedAt *time.Time `json:"modified_at,omitempty"` // When the model was last modified locally +} + +type Chat struct { + ID string `json:"id"` + Messages []Message `json:"messages"` + Title string `json:"title"` + CreatedAt time.Time `json:"created_at"` + BrowserState json.RawMessage `json:"browser_state,omitempty" ts_type:"BrowserStateData"` +} + +// NewChat creates a new Chat with the ID, with CreatedAt timestamp initialized +func NewChat(id string) *Chat { + return &Chat{ + ID: id, + Messages: []Message{}, + CreatedAt: time.Now(), + } +} + +type Settings struct { + // Expose is a boolean that indicates if the ollama server should + // be exposed to the network + Expose bool + + // Browser is a boolean that indicates if the ollama server should + // be exposed to browser windows (e.g. CORS set to allow all origins) + Browser bool + + // Survey is a boolean that indicates if the user allows anonymous + // inference information to be shared with Ollama + Survey bool + + // Models is a string that contains the models to load on startup + Models string + + // TODO(parthsareen): temporary for experimentation + // Agent indicates if the app should use multi-turn tools to fulfill user requests + Agent bool + + // Tools indicates if the app should use single-turn tools to fulfill user requests + Tools bool + + // WorkingDir specifies the working directory for all agent operations + WorkingDir string + + // ContextLength specifies the context length for the ollama server (using OLLAMA_CONTEXT_LENGTH) + ContextLength int + + // AirplaneMode when true, turns off Ollama Turbo features and only uses local models + AirplaneMode bool + + // TurboEnabled indicates if Ollama Turbo features are enabled + TurboEnabled bool + + // Maps gpt-oss specific frontend name' BrowserToolEnabled' to db field 'websearch_enabled' + WebSearchEnabled bool + + // ThinkEnabled indicates if thinking is enabled + ThinkEnabled bool + + // ThinkLevel indicates the level of thinking to use for models that support multiple levels + ThinkLevel string + + // SelectedModel stores the last model that the user selected + SelectedModel string + + // SidebarOpen indicates if the chat sidebar is open + SidebarOpen bool +} + type Store struct { + // DBPath allows overriding the default database path (mainly for testing) + DBPath string + + // dbMu protects database initialization only + dbMu sync.Mutex + db *database +} + +var defaultDBPath = func() string { + switch runtime.GOOS { + case "windows": + return filepath.Join(os.Getenv("LOCALAPPDATA"), "Ollama", "db.sqlite") + case "darwin": + return filepath.Join(os.Getenv("HOME"), "Library", "Application Support", "Ollama", "db.sqlite") + default: + return filepath.Join(os.Getenv("HOME"), ".ollama", "db.sqlite") + } +}() + +// legacyConfigPath is the path to the old config.json file +var legacyConfigPath = func() string { + switch runtime.GOOS { + case "windows": + return filepath.Join(os.Getenv("LOCALAPPDATA"), "Ollama", "config.json") + case "darwin": + return filepath.Join(os.Getenv("HOME"), "Library", "Application Support", "Ollama", "config.json") + default: + return filepath.Join(os.Getenv("HOME"), ".ollama", "config.json") + } +}() + +// legacyData represents the old config.json structure (only fields we need to migrate) +type legacyData struct { ID string `json:"id"` FirstTimeRun bool `json:"first-time-run"` } -var ( - lock sync.Mutex - store Store -) - -func GetID() string { - lock.Lock() - defer lock.Unlock() - if store.ID == "" { - initStore() +func (s *Store) ensureDB() error { + // Fast path: check if db is already initialized + if s.db != nil { + return nil } - return store.ID -} -func GetFirstTimeRun() bool { - lock.Lock() - defer lock.Unlock() - if store.ID == "" { - initStore() + // Slow path: initialize database with lock + s.dbMu.Lock() + defer s.dbMu.Unlock() + + // Double-check after acquiring lock + if s.db != nil { + return nil } - return store.FirstTimeRun -} -func SetFirstTimeRun(val bool) { - lock.Lock() - defer lock.Unlock() - if store.FirstTimeRun == val { - return + dbPath := s.DBPath + if dbPath == "" { + dbPath = defaultDBPath } - store.FirstTimeRun = val - writeStore(getStorePath()) -} -// lock must be held -func initStore() { - storeFile, err := os.Open(getStorePath()) - if err == nil { - defer storeFile.Close() - err = json.NewDecoder(storeFile).Decode(&store) + // Ensure directory exists + if err := os.MkdirAll(filepath.Dir(dbPath), 0o755); err != nil { + return fmt.Errorf("create db directory: %w", err) + } + + database, err := newDatabase(dbPath) + if err != nil { + return fmt.Errorf("open database: %w", err) + } + + // Generate device ID if needed + id, err := database.getID() + if err != nil || id == "" { + // Generate new UUID for device + u, err := uuid.NewV7() if err == nil { - slog.Debug(fmt.Sprintf("loaded existing store %s - ID: %s", getStorePath(), store.ID)) - return + database.setID(u.String()) } - } else if !errors.Is(err, os.ErrNotExist) { - slog.Debug(fmt.Sprintf("unexpected error searching for store: %s", err)) } - slog.Debug("initializing new store") - store.ID = uuid.NewString() - writeStore(getStorePath()) + + s.db = database + + // Check if we need to migrate from config.json + migrated, err := database.isConfigMigrated() + if err != nil || !migrated { + if err := s.migrateFromConfig(database); err != nil { + slog.Warn("failed to migrate from config.json", "error", err) + } + } + + return nil } -func writeStore(storeFilename string) { - ollamaDir := filepath.Dir(storeFilename) - _, err := os.Stat(ollamaDir) - if errors.Is(err, os.ErrNotExist) { - if err := os.MkdirAll(ollamaDir, 0o755); err != nil { - slog.Error(fmt.Sprintf("create ollama dir %s: %v", ollamaDir, err)) - return +// migrateFromConfig attempts to migrate ID and FirstTimeRun from config.json +func (s *Store) migrateFromConfig(database *database) error { + configPath := legacyConfigPath + + // Check if config.json exists + if _, err := os.Stat(configPath); os.IsNotExist(err) { + // No config to migrate, mark as migrated + return database.setConfigMigrated(true) + } + + // Read the config file + b, err := os.ReadFile(configPath) + if err != nil { + return fmt.Errorf("read legacy config: %w", err) + } + + var legacy legacyData + if err := json.Unmarshal(b, &legacy); err != nil { + // If we can't parse it, just mark as migrated and move on + slog.Warn("failed to parse legacy config.json", "error", err) + return database.setConfigMigrated(true) + } + + // Migrate the ID if present + if legacy.ID != "" { + if err := database.setID(legacy.ID); err != nil { + return fmt.Errorf("migrate device ID: %w", err) + } + slog.Info("migrated device ID from config.json") + } + + hasCompleted := legacy.FirstTimeRun // If old FirstTimeRun is true, it means first run was completed + if err := database.setHasCompletedFirstRun(hasCompleted); err != nil { + return fmt.Errorf("migrate first time run: %w", err) + } + slog.Info("migrated first run status from config.json", "hasCompleted", hasCompleted) + + // Mark as migrated + if err := database.setConfigMigrated(true); err != nil { + return fmt.Errorf("mark config as migrated: %w", err) + } + + slog.Info("successfully migrated settings from config.json") + return nil +} + +func (s *Store) ID() (string, error) { + if err := s.ensureDB(); err != nil { + return "", err + } + + return s.db.getID() +} + +func (s *Store) HasCompletedFirstRun() (bool, error) { + if err := s.ensureDB(); err != nil { + return false, err + } + + return s.db.getHasCompletedFirstRun() +} + +func (s *Store) SetHasCompletedFirstRun(hasCompleted bool) error { + if err := s.ensureDB(); err != nil { + return err + } + + return s.db.setHasCompletedFirstRun(hasCompleted) +} + +func (s *Store) Settings() (Settings, error) { + if err := s.ensureDB(); err != nil { + return Settings{}, fmt.Errorf("load settings: %w", err) + } + + settings, err := s.db.getSettings() + if err != nil { + return Settings{}, err + } + + // Set default models directory if not set + if settings.Models == "" { + dir := os.Getenv("OLLAMA_MODELS") + if dir != "" { + settings.Models = dir + } else { + home, err := os.UserHomeDir() + if err == nil { + settings.Models = filepath.Join(home, ".ollama", "models") + } } } - payload, err := json.Marshal(store) - if err != nil { - slog.Error(fmt.Sprintf("failed to marshal store: %s", err)) - return - } - fp, err := os.OpenFile(storeFilename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o755) - if err != nil { - slog.Error(fmt.Sprintf("write store payload %s: %v", storeFilename, err)) - return - } - defer fp.Close() - if n, err := fp.Write(payload); err != nil || n != len(payload) { - slog.Error(fmt.Sprintf("write store payload %s: %d vs %d -- %v", storeFilename, n, len(payload), err)) - return - } - slog.Debug("Store contents: " + string(payload)) - slog.Info(fmt.Sprintf("wrote store: %s", storeFilename)) + + return settings, nil +} + +func (s *Store) SetSettings(settings Settings) error { + if err := s.ensureDB(); err != nil { + return err + } + + return s.db.setSettings(settings) +} + +func (s *Store) Chats() ([]Chat, error) { + if err := s.ensureDB(); err != nil { + return nil, err + } + + return s.db.getAllChats() +} + +func (s *Store) Chat(id string) (*Chat, error) { + return s.ChatWithOptions(id, true) +} + +func (s *Store) ChatWithOptions(id string, loadAttachmentData bool) (*Chat, error) { + if err := s.ensureDB(); err != nil { + return nil, err + } + + chat, err := s.db.getChatWithOptions(id, loadAttachmentData) + if err != nil { + return nil, fmt.Errorf("%w: chat %s", not.Found, id) + } + + return chat, nil +} + +func (s *Store) SetChat(chat Chat) error { + if err := s.ensureDB(); err != nil { + return err + } + + return s.db.saveChat(chat) +} + +func (s *Store) DeleteChat(id string) error { + if err := s.ensureDB(); err != nil { + return err + } + + // Delete from database + if err := s.db.deleteChat(id); err != nil { + return fmt.Errorf("%w: chat %s", not.Found, id) + } + + // Also delete associated images + chatImgDir := filepath.Join(s.ImgDir(), id) + if err := os.RemoveAll(chatImgDir); err != nil { + // Log error but don't fail the deletion + slog.Warn("failed to delete chat images", "chat_id", id, "error", err) + } + + return nil +} + +func (s *Store) WindowSize() (int, int, error) { + if err := s.ensureDB(); err != nil { + return 0, 0, err + } + + return s.db.getWindowSize() +} + +func (s *Store) SetWindowSize(width, height int) error { + if err := s.ensureDB(); err != nil { + return err + } + + return s.db.setWindowSize(width, height) +} + +func (s *Store) UpdateLastMessage(chatID string, message Message) error { + if err := s.ensureDB(); err != nil { + return err + } + + return s.db.updateLastMessage(chatID, message) +} + +func (s *Store) AppendMessage(chatID string, message Message) error { + if err := s.ensureDB(); err != nil { + return err + } + + return s.db.appendMessage(chatID, message) +} + +func (s *Store) UpdateChatBrowserState(chatID string, state json.RawMessage) error { + if err := s.ensureDB(); err != nil { + return err + } + + return s.db.updateChatBrowserState(chatID, state) +} + +func (s *Store) User() (*User, error) { + if err := s.ensureDB(); err != nil { + return nil, err + } + + return s.db.getUser() +} + +func (s *Store) SetUser(user User) error { + if err := s.ensureDB(); err != nil { + return err + } + + user.CachedAt = time.Now() + return s.db.setUser(user) +} + +func (s *Store) ClearUser() error { + if err := s.ensureDB(); err != nil { + return err + } + + return s.db.clearUser() +} + +func (s *Store) Close() error { + s.dbMu.Lock() + defer s.dbMu.Unlock() + + if s.db != nil { + return s.db.Close() + } + return nil } diff --git a/app/store/store_darwin.go b/app/store/store_darwin.go deleted file mode 100644 index e53d85258..000000000 --- a/app/store/store_darwin.go +++ /dev/null @@ -1,13 +0,0 @@ -package store - -import ( - "os" - "path/filepath" -) - -func getStorePath() string { - // TODO - system wide location? - - home := os.Getenv("HOME") - return filepath.Join(home, "Library", "Application Support", "Ollama", "config.json") -} diff --git a/app/store/store_linux.go b/app/store/store_linux.go deleted file mode 100644 index 3aac9b014..000000000 --- a/app/store/store_linux.go +++ /dev/null @@ -1,16 +0,0 @@ -package store - -import ( - "os" - "path/filepath" -) - -func getStorePath() string { - if os.Geteuid() == 0 { - // TODO where should we store this on linux for system-wide operation? - return "/etc/ollama/config.json" - } - - home := os.Getenv("HOME") - return filepath.Join(home, ".ollama", "config.json") -} diff --git a/app/store/store_test.go b/app/store/store_test.go new file mode 100644 index 000000000..dfe6435f1 --- /dev/null +++ b/app/store/store_test.go @@ -0,0 +1,192 @@ +//go:build windows || darwin + +package store + +import ( + "path/filepath" + "testing" +) + +func TestStore(t *testing.T) { + s, cleanup := setupTestStore(t) + defer cleanup() + + t.Run("default id", func(t *testing.T) { + // ID should be automatically generated + id, err := s.ID() + if err != nil { + t.Fatal(err) + } + if id == "" { + t.Error("expected non-empty ID") + } + + // Verify ID is persisted + id2, err := s.ID() + if err != nil { + t.Fatal(err) + } + if id != id2 { + t.Errorf("expected ID %s, got %s", id, id2) + } + }) + + t.Run("has completed first run", func(t *testing.T) { + // Default should be false (hasn't completed first run yet) + hasCompleted, err := s.HasCompletedFirstRun() + if err != nil { + t.Fatal(err) + } + if hasCompleted { + t.Error("expected has completed first run to be false by default") + } + + if err := s.SetHasCompletedFirstRun(true); err != nil { + t.Fatal(err) + } + + hasCompleted, err = s.HasCompletedFirstRun() + if err != nil { + t.Fatal(err) + } + if !hasCompleted { + t.Error("expected has completed first run to be true") + } + }) + + t.Run("settings", func(t *testing.T) { + sc := Settings{ + Expose: true, + Browser: true, + Survey: true, + Models: "/tmp/models", + Agent: true, + Tools: false, + WorkingDir: "/tmp/work", + } + + if err := s.SetSettings(sc); err != nil { + t.Fatal(err) + } + + loaded, err := s.Settings() + if err != nil { + t.Fatal(err) + } + // Compare fields individually since Models might get a default + if loaded.Expose != sc.Expose || loaded.Browser != sc.Browser || + loaded.Agent != sc.Agent || loaded.Survey != sc.Survey || + loaded.Tools != sc.Tools || loaded.WorkingDir != sc.WorkingDir { + t.Errorf("expected %v, got %v", sc, loaded) + } + }) + + t.Run("window size", func(t *testing.T) { + if err := s.SetWindowSize(1024, 768); err != nil { + t.Fatal(err) + } + + width, height, err := s.WindowSize() + if err != nil { + t.Fatal(err) + } + if width != 1024 || height != 768 { + t.Errorf("expected 1024x768, got %dx%d", width, height) + } + }) + + t.Run("create and retrieve chat", func(t *testing.T) { + chat := NewChat("test-chat-1") + chat.Title = "Test Chat" + + chat.Messages = append(chat.Messages, NewMessage("user", "Hello", nil)) + chat.Messages = append(chat.Messages, NewMessage("assistant", "Hi there!", &MessageOptions{ + Model: "llama4", + })) + + if err := s.SetChat(*chat); err != nil { + t.Fatalf("failed to save chat: %v", err) + } + + retrieved, err := s.Chat("test-chat-1") + if err != nil { + t.Fatalf("failed to retrieve chat: %v", err) + } + + if retrieved.ID != chat.ID { + t.Errorf("expected ID %s, got %s", chat.ID, retrieved.ID) + } + if retrieved.Title != chat.Title { + t.Errorf("expected title %s, got %s", chat.Title, retrieved.Title) + } + if len(retrieved.Messages) != 2 { + t.Fatalf("expected 2 messages, got %d", len(retrieved.Messages)) + } + if retrieved.Messages[0].Content != "Hello" { + t.Errorf("expected first message 'Hello', got %s", retrieved.Messages[0].Content) + } + if retrieved.Messages[1].Content != "Hi there!" { + t.Errorf("expected second message 'Hi there!', got %s", retrieved.Messages[1].Content) + } + }) + + t.Run("list chats", func(t *testing.T) { + chat2 := NewChat("test-chat-2") + chat2.Title = "Another Chat" + chat2.Messages = append(chat2.Messages, NewMessage("user", "Test", nil)) + + if err := s.SetChat(*chat2); err != nil { + t.Fatalf("failed to save chat: %v", err) + } + + chats, err := s.Chats() + if err != nil { + t.Fatalf("failed to list chats: %v", err) + } + + if len(chats) != 2 { + t.Fatalf("expected 2 chats, got %d", len(chats)) + } + }) + + t.Run("delete chat", func(t *testing.T) { + if err := s.DeleteChat("test-chat-1"); err != nil { + t.Fatalf("failed to delete chat: %v", err) + } + + // Verify it's gone + _, err := s.Chat("test-chat-1") + if err == nil { + t.Error("expected error retrieving deleted chat") + } + + // Verify other chat still exists + chats, err := s.Chats() + if err != nil { + t.Fatalf("failed to list chats: %v", err) + } + if len(chats) != 1 { + t.Fatalf("expected 1 chat after deletion, got %d", len(chats)) + } + }) +} + +// setupTestStore creates a temporary store for testing +func setupTestStore(t *testing.T) (*Store, func()) { + t.Helper() + + tmpDir := t.TempDir() + + // Override legacy config path to ensure no migration happens + oldLegacyConfigPath := legacyConfigPath + legacyConfigPath = filepath.Join(tmpDir, "config.json") + + s := &Store{DBPath: filepath.Join(tmpDir, "db.sqlite")} + + cleanup := func() { + s.Close() + legacyConfigPath = oldLegacyConfigPath + } + + return s, cleanup +} diff --git a/app/store/store_windows.go b/app/store/store_windows.go deleted file mode 100644 index ba06b82c4..000000000 --- a/app/store/store_windows.go +++ /dev/null @@ -1,11 +0,0 @@ -package store - -import ( - "os" - "path/filepath" -) - -func getStorePath() string { - localAppData := os.Getenv("LOCALAPPDATA") - return filepath.Join(localAppData, "Ollama", "config.json") -} diff --git a/app/store/testdata/schema.sql b/app/store/testdata/schema.sql new file mode 100644 index 000000000..8f944ff85 --- /dev/null +++ b/app/store/testdata/schema.sql @@ -0,0 +1,61 @@ +-- This is the version 2 schema for the app database, the first released schema to users. +-- Do not modify this file. It is used to test that the database schema stays in a consistent state between schema migrations. + +CREATE TABLE IF NOT EXISTS settings ( + id INTEGER PRIMARY KEY CHECK (id = 1), + device_id TEXT NOT NULL DEFAULT '', + has_completed_first_run BOOLEAN NOT NULL DEFAULT 0, + expose BOOLEAN NOT NULL DEFAULT 0, + survey BOOLEAN NOT NULL DEFAULT TRUE, + browser BOOLEAN NOT NULL DEFAULT 0, + models TEXT NOT NULL DEFAULT '', + remote TEXT NOT NULL DEFAULT '', + agent BOOLEAN NOT NULL DEFAULT 0, + tools BOOLEAN NOT NULL DEFAULT 0, + working_dir TEXT NOT NULL DEFAULT '', + context_length INTEGER NOT NULL DEFAULT 4096, + window_width INTEGER NOT NULL DEFAULT 0, + window_height INTEGER NOT NULL DEFAULT 0, + config_migrated BOOLEAN NOT NULL DEFAULT 0, + schema_version INTEGER NOT NULL DEFAULT 2 +); + +-- Insert default settings row if it doesn't exist +INSERT OR IGNORE INTO settings (id) VALUES (1); + +CREATE TABLE IF NOT EXISTS chats ( + id TEXT PRIMARY KEY, + title TEXT NOT NULL DEFAULT '', + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS messages ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + chat_id TEXT NOT NULL, + role TEXT NOT NULL, + content TEXT NOT NULL DEFAULT '', + thinking TEXT NOT NULL DEFAULT '', + stream BOOLEAN NOT NULL DEFAULT 0, + model_name TEXT, + model_cloud BOOLEAN, + model_ollama_host BOOLEAN, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + thinking_time_start TIMESTAMP, + thinking_time_end TIMESTAMP, + FOREIGN KEY (chat_id) REFERENCES chats(id) ON DELETE CASCADE +); + +CREATE INDEX IF NOT EXISTS idx_messages_chat_id ON messages(chat_id); + +CREATE TABLE IF NOT EXISTS tool_calls ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + message_id INTEGER NOT NULL, + type TEXT NOT NULL, + function_name TEXT NOT NULL, + function_arguments TEXT NOT NULL, + function_result TEXT, + FOREIGN KEY (message_id) REFERENCES messages(id) ON DELETE CASCADE +); + +CREATE INDEX IF NOT EXISTS idx_tool_calls_message_id ON tool_calls(message_id); diff --git a/app/tools/browser.go b/app/tools/browser.go new file mode 100644 index 000000000..b41c079ba --- /dev/null +++ b/app/tools/browser.go @@ -0,0 +1,863 @@ +//go:build windows || darwin + +package tools + +import ( + "context" + "fmt" + "net/url" + "regexp" + "strings" + "sync" + "time" + + "github.com/ollama/ollama/app/ui/responses" +) + +type PageType string + +const ( + PageTypeSearchResults PageType = "initial_results" + PageTypeWebpage PageType = "webpage" +) + +// DefaultViewTokens is the number of tokens to show to the model used when calling displayPage +const DefaultViewTokens = 1024 + +/* +The Browser tool provides web browsing capability for gpt-oss. +The model uses the tool by usually doing a search first and then choosing to either open a page, +find a term in a page, or do another search. + +The tool optionally may open a URL directly - especially if one is passed in. + +Each action is saved into an append-only page stack `responses.BrowserStateData` to keep +track of the history of the browsing session. + +Each `Execute()` for a tool returns the full current state of the browser. ui.go manages the +browser state representation between the tool, ui, and db. + +A new Browser object is created per request - the state is reconstructed by ui.go. +The initialization of the browser will receive a `responses.BrowserStateData` with the stitched history. +*/ + +// BrowserState manages the browsing session on a per-chat basis +type BrowserState struct { + mu sync.RWMutex + Data *responses.BrowserStateData +} +type Browser struct { + state *BrowserState +} + +// State is only accessed in a single thread, as each chat has its own browser state +func (b *Browser) State() *responses.BrowserStateData { + b.state.mu.RLock() + defer b.state.mu.RUnlock() + return b.state.Data +} + +func (b *Browser) savePage(page *responses.Page) { + b.state.Data.URLToPage[page.URL] = page + b.state.Data.PageStack = append(b.state.Data.PageStack, page.URL) +} + +func (b *Browser) getPageFromStack(url string) (*responses.Page, error) { + page, ok := b.state.Data.URLToPage[url] + if !ok { + return nil, fmt.Errorf("page not found for url %s", url) + } + return page, nil +} + +func NewBrowser(state *responses.BrowserStateData) *Browser { + if state == nil { + state = &responses.BrowserStateData{ + PageStack: []string{}, + ViewTokens: DefaultViewTokens, + URLToPage: make(map[string]*responses.Page), + } + } + b := &BrowserState{ + Data: state, + } + + return &Browser{ + state: b, + } +} + +type BrowserSearch struct { + Browser + webSearch *BrowserWebSearch +} + +// NewBrowserSearch creates a new browser search instance +func NewBrowserSearch(bb *Browser) *BrowserSearch { + if bb == nil { + bb = &Browser{ + state: &BrowserState{ + Data: &responses.BrowserStateData{ + PageStack: []string{}, + ViewTokens: DefaultViewTokens, + URLToPage: make(map[string]*responses.Page), + }, + }, + } + } + return &BrowserSearch{ + Browser: *bb, + webSearch: &BrowserWebSearch{}, + } +} + +func (b *BrowserSearch) Name() string { + return "browser.search" +} + +func (b *BrowserSearch) Description() string { + return "Search the web for information" +} + +func (b *BrowserSearch) Prompt() string { + return "" +} + +func (b *BrowserSearch) Schema() map[string]any { + return map[string]any{} +} + +func (b *BrowserSearch) Execute(ctx context.Context, args map[string]any) (any, string, error) { + query, ok := args["query"].(string) + if !ok { + return nil, "", fmt.Errorf("query parameter is required") + } + + topn, ok := args["topn"].(int) + if !ok { + topn = 5 + } + + searchArgs := map[string]any{ + "queries": []any{query}, + "max_results": topn, + } + + result, err := b.webSearch.Execute(ctx, searchArgs) + if err != nil { + return nil, "", fmt.Errorf("search error: %w", err) + } + + searchResponse, ok := result.(*WebSearchResponse) + if !ok { + return nil, "", fmt.Errorf("invalid search results format") + } + + // Build main search results page that contains all search results + searchResultsPage := b.buildSearchResultsPageCollection(query, searchResponse) + b.savePage(searchResultsPage) + cursor := len(b.state.Data.PageStack) - 1 + // cache result for each page + for _, queryResults := range searchResponse.Results { + for i, result := range queryResults { + resultPage := b.buildSearchResultsPage(&result, i+1) + // save to global only, do not add to visited stack + b.state.Data.URLToPage[resultPage.URL] = resultPage + } + } + + page := searchResultsPage + + pageText, err := b.displayPage(page, cursor, 0, -1) + if err != nil { + return nil, "", fmt.Errorf("failed to display page: %w", err) + } + + return b.state.Data, pageText, nil +} + +func (b *Browser) buildSearchResultsPageCollection(query string, results *WebSearchResponse) *responses.Page { + page := &responses.Page{ + URL: "search_results_" + query, + Title: query, + Links: make(map[int]string), + FetchedAt: time.Now(), + } + + var textBuilder strings.Builder + linkIdx := 0 + + // Add the header lines to match format + textBuilder.WriteString("\n") // L0: empty + textBuilder.WriteString("URL: \n") // L1: URL: (empty for search) + textBuilder.WriteString("# Search Results\n") // L2: # Search Results + textBuilder.WriteString("\n") // L3: empty + + for _, queryResults := range results.Results { + for _, result := range queryResults { + domain := result.URL + if u, err := url.Parse(result.URL); err == nil && u.Host != "" { + domain = u.Host + domain = strings.TrimPrefix(domain, "www.") + } + + linkFormat := fmt.Sprintf("* 【%d†%s†%s】", linkIdx, result.Title, domain) + textBuilder.WriteString(linkFormat) + + numChars := min(len(result.Content.FullText), 400) + snippet := strings.TrimSpace(result.Content.FullText[:numChars]) + textBuilder.WriteString(snippet) + textBuilder.WriteString("\n") + + page.Links[linkIdx] = result.URL + linkIdx++ + } + } + + page.Text = textBuilder.String() + page.Lines = wrapLines(page.Text, 80) + + return page +} + +func (b *Browser) buildSearchResultsPage(result *WebSearchResult, linkIdx int) *responses.Page { + page := &responses.Page{ + URL: result.URL, + Title: result.Title, + Links: make(map[int]string), + FetchedAt: time.Now(), + } + + var textBuilder strings.Builder + + // Format the individual result page (only used when no full text is available) + linkFormat := fmt.Sprintf("【%d†%s】", linkIdx, result.Title) + textBuilder.WriteString(linkFormat) + textBuilder.WriteString("\n") + textBuilder.WriteString(fmt.Sprintf("URL: %s\n", result.URL)) + numChars := min(len(result.Content.FullText), 300) + textBuilder.WriteString(result.Content.FullText[:numChars]) + textBuilder.WriteString("\n\n") + + // Only store link and snippet if we won't be processing full text later + // (full text processing will handle all links consistently) + if result.Content.FullText == "" { + page.Links[linkIdx] = result.URL + } + + // Use full text if available, otherwise use snippet + if result.Content.FullText != "" { + // Prepend the URL line to the full text + page.Text = fmt.Sprintf("URL: %s\n%s", result.URL, result.Content.FullText) + // Process markdown links in the full text + processedText, processedLinks := processMarkdownLinks(page.Text) + page.Text = processedText + page.Links = processedLinks + } else { + page.Text = textBuilder.String() + } + + page.Lines = wrapLines(page.Text, 80) + + return page +} + +// getEndLoc calculates the end location for viewport based on token limits +func (b *Browser) getEndLoc(loc, numLines, totalLines int, lines []string) int { + if numLines <= 0 { + // Auto-calculate based on viewTokens + txt := b.joinLinesWithNumbers(lines[loc:]) + + // If text is very short, no need to truncate (at least 1 char per token) + if len(txt) > b.state.Data.ViewTokens { + // Simple heuristic: approximate token counting + // Typical token is ~4 characters, but can be up to 128 chars + maxCharsPerToken := 128 + + // upper bound for text to analyze + upperBound := min((b.state.Data.ViewTokens+1)*maxCharsPerToken, len(txt)) + textToAnalyze := txt[:upperBound] + + // Simple approximation: count tokens as ~4 chars each + // This is less accurate than tiktoken but more performant + approxTokens := len(textToAnalyze) / 4 + + if approxTokens > b.state.Data.ViewTokens { + // Find the character position at viewTokens + endIdx := min(b.state.Data.ViewTokens*4, len(txt)) + + // Count newlines up to that position to get line count + numLines = strings.Count(txt[:endIdx], "\n") + 1 + } else { + numLines = totalLines + } + } else { + numLines = totalLines + } + } + + return min(loc+numLines, totalLines) +} + +// joinLinesWithNumbers creates a string with line numbers, matching Python's join_lines +func (b *Browser) joinLinesWithNumbers(lines []string) string { + var builder strings.Builder + var hadZeroLine bool + for i, line := range lines { + if i == 0 { + builder.WriteString("L0:\n") + hadZeroLine = true + } + if hadZeroLine { + builder.WriteString(fmt.Sprintf("L%d: %s\n", i+1, line)) + } else { + builder.WriteString(fmt.Sprintf("L%d: %s\n", i, line)) + } + } + return builder.String() +} + +// processMarkdownLinks finds all markdown links in the text and replaces them with the special format +// Returns the processed text and a map of link IDs to URLs +func processMarkdownLinks(text string) (string, map[int]string) { + links := make(map[int]string) + + // Always start from 0 for consistent numbering across all pages + linkID := 0 + + // First, handle multi-line markdown links by joining them + // This regex finds markdown links that might be split across lines + multiLinePattern := regexp.MustCompile(`\[([^\]]+)\]\s*\n\s*\(([^)]+)\)`) + text = multiLinePattern.ReplaceAllStringFunc(text, func(match string) string { + // Replace newlines with spaces in the match + cleaned := strings.ReplaceAll(match, "\n", " ") + // Remove extra spaces + cleaned = regexp.MustCompile(`\s+`).ReplaceAllString(cleaned, " ") + return cleaned + }) + + // Now process all markdown links (including the cleaned multi-line ones) + linkPattern := regexp.MustCompile(`\[([^\]]+)\]\(([^)]+)\)`) + + processedText := linkPattern.ReplaceAllStringFunc(text, func(match string) string { + matches := linkPattern.FindStringSubmatch(match) + if len(matches) != 3 { + return match + } + + linkText := strings.TrimSpace(matches[1]) + linkURL := strings.TrimSpace(matches[2]) + + // Extract domain from URL + domain := linkURL + if u, err := url.Parse(linkURL); err == nil && u.Host != "" { + domain = u.Host + // Remove www. prefix if present + domain = strings.TrimPrefix(domain, "www.") + } + + // Create the formatted link + formatted := fmt.Sprintf("【%d†%s†%s】", linkID, linkText, domain) + + // Store the link + links[linkID] = linkURL + linkID++ + + return formatted + }) + + return processedText, links +} + +func wrapLines(text string, width int) []string { + if width <= 0 { + width = 80 + } + + lines := strings.Split(text, "\n") + var wrapped []string + + for _, line := range lines { + if line == "" { + // Preserve empty lines + wrapped = append(wrapped, "") + } else if len(line) <= width { + wrapped = append(wrapped, line) + } else { + // Word wrapping while preserving whitespace structure + words := strings.Fields(line) + if len(words) == 0 { + // Line with only whitespace + wrapped = append(wrapped, line) + continue + } + + currentLine := "" + for _, word := range words { + // Check if adding this word would exceed width + testLine := currentLine + if testLine != "" { + testLine += " " + } + testLine += word + + if len(testLine) > width && currentLine != "" { + // Current line would be too long, wrap it + wrapped = append(wrapped, currentLine) + currentLine = word + } else { + // Add word to current line + if currentLine != "" { + currentLine += " " + } + currentLine += word + } + } + + // Add any remaining content + if currentLine != "" { + wrapped = append(wrapped, currentLine) + } + } + } + + return wrapped +} + +// displayPage formats and returns the page display for the model +func (b *Browser) displayPage(page *responses.Page, cursor, loc, numLines int) (string, error) { + totalLines := len(page.Lines) + + if loc >= totalLines { + return "", fmt.Errorf("invalid location: %d (max: %d)", loc, totalLines-1) + } + + // get viewport end location + endLoc := b.getEndLoc(loc, numLines, totalLines, page.Lines) + + var displayBuilder strings.Builder + displayBuilder.WriteString(fmt.Sprintf("[%d] %s", cursor, page.Title)) + if page.URL != "" { + displayBuilder.WriteString(fmt.Sprintf("(%s)\n", page.URL)) + } else { + displayBuilder.WriteString("\n") + } + displayBuilder.WriteString(fmt.Sprintf("**viewing lines [%d - %d] of %d**\n\n", loc, endLoc-1, totalLines-1)) + + // Content with line numbers + var hadZeroLine bool + for i := loc; i < endLoc; i++ { + if i == 0 { + displayBuilder.WriteString("L0:\n") + hadZeroLine = true + } + if hadZeroLine { + displayBuilder.WriteString(fmt.Sprintf("L%d: %s\n", i+1, page.Lines[i])) + } else { + displayBuilder.WriteString(fmt.Sprintf("L%d: %s\n", i, page.Lines[i])) + } + } + + return displayBuilder.String(), nil +} + +type BrowserOpen struct { + Browser + crawlPage *BrowserCrawler +} + +func NewBrowserOpen(bb *Browser) *BrowserOpen { + if bb == nil { + bb = &Browser{ + state: &BrowserState{ + Data: &responses.BrowserStateData{ + PageStack: []string{}, + ViewTokens: DefaultViewTokens, + URLToPage: make(map[string]*responses.Page), + }, + }, + } + } + return &BrowserOpen{ + Browser: *bb, + crawlPage: &BrowserCrawler{}, + } +} + +func (b *BrowserOpen) Name() string { + return "browser.open" +} + +func (b *BrowserOpen) Description() string { + return "Open a link in the browser" +} + +func (b *BrowserOpen) Prompt() string { + return "" +} + +func (b *BrowserOpen) Schema() map[string]any { + return map[string]any{} +} + +func (b *BrowserOpen) Execute(ctx context.Context, args map[string]any) (any, string, error) { + // Get cursor parameter first + cursor := -1 + if c, ok := args["cursor"].(float64); ok { + cursor = int(c) + } else if c, ok := args["cursor"].(int); ok { + cursor = c + } + + // Get loc parameter + loc := 0 + if l, ok := args["loc"].(float64); ok { + loc = int(l) + } else if l, ok := args["loc"].(int); ok { + loc = l + } + + // Get num_lines parameter + numLines := -1 + if n, ok := args["num_lines"].(float64); ok { + numLines = int(n) + } else if n, ok := args["num_lines"].(int); ok { + numLines = n + } + + // get page from cursor + var page *responses.Page + if cursor >= 0 { + if cursor >= len(b.state.Data.PageStack) { + return nil, "", fmt.Errorf("cursor %d is out of range (pageStack length: %d)", cursor, len(b.state.Data.PageStack)) + } + var err error + page, err = b.getPageFromStack(b.state.Data.PageStack[cursor]) + if err != nil { + return nil, "", fmt.Errorf("page not found for cursor %d: %w", cursor, err) + } + } else { + // get last page + if len(b.state.Data.PageStack) != 0 { + pageURL := b.state.Data.PageStack[len(b.state.Data.PageStack)-1] + var err error + page, err = b.getPageFromStack(pageURL) + if err != nil { + return nil, "", fmt.Errorf("page not found for cursor %d: %w", cursor, err) + } + } + } + + // Try to get id as string (URL) first + if url, ok := args["id"].(string); ok { + // Check if we already have this page cached + if existingPage, ok := b.state.Data.URLToPage[url]; ok { + // Use cached page + b.savePage(existingPage) + // Always update cursor to point to the newly added page + cursor = len(b.state.Data.PageStack) - 1 + pageText, err := b.displayPage(existingPage, cursor, loc, numLines) + if err != nil { + return nil, "", fmt.Errorf("failed to display page: %w", err) + } + return b.state.Data, pageText, nil + } + + // Page not in cache, need to crawl it + if b.crawlPage == nil { + b.crawlPage = &BrowserCrawler{} + } + crawlResponse, err := b.crawlPage.Execute(ctx, map[string]any{ + "urls": []any{url}, + "latest": false, + }) + if err != nil { + return nil, "", fmt.Errorf("failed to crawl URL %s: %w", url, err) + } + + newPage, err := b.buildPageFromCrawlResult(url, crawlResponse) + if err != nil { + return nil, "", fmt.Errorf("failed to build page from crawl result: %w", err) + } + + // Need to fall through if first search is directly an open command - no existing page + b.savePage(newPage) + // Always update cursor to point to the newly added page + cursor = len(b.state.Data.PageStack) - 1 + pageText, err := b.displayPage(newPage, cursor, loc, numLines) + if err != nil { + return nil, "", fmt.Errorf("failed to display page: %w", err) + } + return b.state.Data, pageText, nil + } + + // Try to get id as integer (link ID from current page) + if id, ok := args["id"].(float64); ok { + if page == nil { + return nil, "", fmt.Errorf("no current page to resolve link from") + } + idInt := int(id) + pageURL, ok := page.Links[idInt] + if !ok { + return nil, "", fmt.Errorf("invalid link id %d", idInt) + } + + // Check if we have the linked page cached + newPage, ok := b.state.Data.URLToPage[pageURL] + if !ok { + if b.crawlPage == nil { + b.crawlPage = &BrowserCrawler{} + } + crawlResponse, err := b.crawlPage.Execute(ctx, map[string]any{ + "urls": []any{pageURL}, + "latest": false, + }) + if err != nil { + return nil, "", fmt.Errorf("failed to crawl URL %s: %w", pageURL, err) + } + + // Create new page from crawl result + newPage, err = b.buildPageFromCrawlResult(pageURL, crawlResponse) + if err != nil { + return nil, "", fmt.Errorf("failed to build page from crawl result: %w", err) + } + } + + // Add to history stack regardless of cache status + b.savePage(newPage) + + // Always update cursor to point to the newly added page + cursor = len(b.state.Data.PageStack) - 1 + pageText, err := b.displayPage(newPage, cursor, loc, numLines) + if err != nil { + return nil, "", fmt.Errorf("failed to display page: %w", err) + } + return b.state.Data, pageText, nil + } + + // If no id provided, just display current page + if page == nil { + return nil, "", fmt.Errorf("no current page to display") + } + // Only add to PageStack without updating URLToPage + b.state.Data.PageStack = append(b.state.Data.PageStack, page.URL) + cursor = len(b.state.Data.PageStack) - 1 + + pageText, err := b.displayPage(page, cursor, loc, numLines) + if err != nil { + return nil, "", fmt.Errorf("failed to display page: %w", err) + } + return b.state.Data, pageText, nil +} + +// buildPageFromCrawlResult creates a Page from crawl API results +func (b *Browser) buildPageFromCrawlResult(requestedURL string, crawlResponse *CrawlResponse) (*responses.Page, error) { + // Initialize page with defaults + page := &responses.Page{ + URL: requestedURL, + Title: requestedURL, + Text: "", + Links: make(map[int]string), + FetchedAt: time.Now(), + } + + // Process crawl results - the API returns results grouped by URL + for url, urlResults := range crawlResponse.Results { + if len(urlResults) > 0 { + // Get the first result for this URL + result := urlResults[0] + + // Extract content + if result.Content.FullText != "" { + page.Text = result.Content.FullText + } + + // Extract title if available + if result.Title != "" { + page.Title = result.Title + } + + // Update URL to the actual URL from results + page.URL = url + + // Extract links if available from extras + for i, link := range result.Extras.Links { + if link.Href != "" { + page.Links[i] = link.Href + } else if link.URL != "" { + page.Links[i] = link.URL + } + } + + // Only process the first URL's results + break + } + } + + // If no text was extracted, set a default message + if page.Text == "" { + page.Text = "No content could be extracted from this page." + } else { + // Prepend the URL line to match Python implementation + page.Text = fmt.Sprintf("URL: %s\n%s", page.URL, page.Text) + } + + // Process markdown links in the text + processedText, processedLinks := processMarkdownLinks(page.Text) + page.Text = processedText + page.Links = processedLinks + + // Wrap lines for display + page.Lines = wrapLines(page.Text, 80) + + return page, nil +} + +type BrowserFind struct { + Browser +} + +func NewBrowserFind(bb *Browser) *BrowserFind { + return &BrowserFind{ + Browser: *bb, + } +} + +func (b *BrowserFind) Name() string { + return "browser.find" +} + +func (b *BrowserFind) Description() string { + return "Find a term in the browser" +} + +func (b *BrowserFind) Prompt() string { + return "" +} + +func (b *BrowserFind) Schema() map[string]any { + return map[string]any{} +} + +func (b *BrowserFind) Execute(ctx context.Context, args map[string]any) (any, string, error) { + pattern, ok := args["pattern"].(string) + if !ok { + return nil, "", fmt.Errorf("pattern parameter is required") + } + + // Get cursor parameter if provided, default to current page + cursor := -1 + if c, ok := args["cursor"].(float64); ok { + cursor = int(c) + } + + // Get the page to search in + var page *responses.Page + if cursor == -1 { + // Use current page + if len(b.state.Data.PageStack) == 0 { + return nil, "", fmt.Errorf("no pages to search in") + } + var err error + page, err = b.getPageFromStack(b.state.Data.PageStack[len(b.state.Data.PageStack)-1]) + if err != nil { + return nil, "", fmt.Errorf("page not found for cursor %d: %w", cursor, err) + } + } else { + // Use specific cursor + if cursor < 0 || cursor >= len(b.state.Data.PageStack) { + return nil, "", fmt.Errorf("cursor %d is out of range [0-%d]", cursor, len(b.state.Data.PageStack)-1) + } + var err error + page, err = b.getPageFromStack(b.state.Data.PageStack[cursor]) + if err != nil { + return nil, "", fmt.Errorf("page not found for cursor %d: %w", cursor, err) + } + } + + if page == nil { + return nil, "", fmt.Errorf("page not found") + } + + // Create find results page + findPage := b.buildFindResultsPage(pattern, page) + + // Add the find results page to state + b.savePage(findPage) + newCursor := len(b.state.Data.PageStack) - 1 + + pageText, err := b.displayPage(findPage, newCursor, 0, -1) + if err != nil { + return nil, "", fmt.Errorf("failed to display page: %w", err) + } + + return b.state.Data, pageText, nil +} + +func (b *Browser) buildFindResultsPage(pattern string, page *responses.Page) *responses.Page { + findPage := &responses.Page{ + Title: fmt.Sprintf("Find results for text: `%s` in `%s`", pattern, page.Title), + Links: make(map[int]string), + FetchedAt: time.Now(), + } + + findPage.URL = fmt.Sprintf("find_results_%s", pattern) + + var textBuilder strings.Builder + matchIdx := 0 + maxResults := 50 + numShowLines := 4 + patternLower := strings.ToLower(pattern) + + // Search through the page lines following the reference algorithm + var resultChunks []string + lineIdx := 0 + + for lineIdx < len(page.Lines) { + line := page.Lines[lineIdx] + lineLower := strings.ToLower(line) + + if !strings.Contains(lineLower, patternLower) { + lineIdx++ + continue + } + + // Build snippet context + endLine := min(lineIdx+numShowLines, len(page.Lines)) + + var snippetBuilder strings.Builder + for j := lineIdx; j < endLine; j++ { + snippetBuilder.WriteString(page.Lines[j]) + if j < endLine-1 { + snippetBuilder.WriteString("\n") + } + } + snippet := snippetBuilder.String() + + // Format the match + linkFormat := fmt.Sprintf("【%d†match at L%d】", matchIdx, lineIdx) + resultChunk := fmt.Sprintf("%s\n%s", linkFormat, snippet) + resultChunks = append(resultChunks, resultChunk) + + if len(resultChunks) >= maxResults { + break + } + + matchIdx++ + lineIdx += numShowLines + } + + // Build final display text + if len(resultChunks) > 0 { + textBuilder.WriteString(strings.Join(resultChunks, "\n\n")) + } + + if matchIdx == 0 { + findPage.Text = fmt.Sprintf("No `find` results for pattern: `%s`", pattern) + } else { + findPage.Text = textBuilder.String() + } + + findPage.Lines = wrapLines(findPage.Text, 80) + return findPage +} diff --git a/app/tools/browser_crawl.go b/app/tools/browser_crawl.go new file mode 100644 index 000000000..fd9c2bd4d --- /dev/null +++ b/app/tools/browser_crawl.go @@ -0,0 +1,136 @@ +//go:build windows || darwin + +package tools + +import ( + "context" + "encoding/json" + "fmt" +) + +// CrawlContent represents the content of a crawled page +type CrawlContent struct { + Snippet string `json:"snippet"` + FullText string `json:"full_text"` +} + +// CrawlExtras represents additional data from the crawl API +type CrawlExtras struct { + Links []CrawlLink `json:"links"` +} + +// CrawlLink represents a link found on a crawled page +type CrawlLink struct { + URL string `json:"url"` + Href string `json:"href"` + Text string `json:"text"` +} + +// CrawlResult represents a single crawl result +type CrawlResult struct { + Title string `json:"title"` + URL string `json:"url"` + Content CrawlContent `json:"content"` + Extras CrawlExtras `json:"extras"` +} + +// CrawlResponse represents the complete response from the crawl API +type CrawlResponse struct { + Results map[string][]CrawlResult `json:"results"` +} + +// BrowserCrawler tool for crawling web pages using ollama.com crawl API +type BrowserCrawler struct{} + +func (g *BrowserCrawler) Name() string { + return "get_webpage" +} + +func (g *BrowserCrawler) Description() string { + return "Crawl and extract text content from web pages" +} + +func (g *BrowserCrawler) Prompt() string { + return `When you need to read content from web pages, use the get_webpage tool. Simply provide the URLs you want to read and I'll fetch their content for you. + +For each URL, I'll extract the main text content in a readable format. If you need to discover links within those pages, set extract_links to true. If the user requires the latest information, set livecrawl to true. + +Only use this tool when you need to access current web content. Make sure the URLs are valid and accessible. Do not use this tool for: +- Downloading files or media +- Accessing private/authenticated pages +- Scraping data at high volumes + +Always check the returned content to ensure it's relevant before using it in your response.` +} + +func (g *BrowserCrawler) Schema() map[string]any { + schemaBytes := []byte(`{ + "type": "object", + "properties": { + "urls": { + "type": "array", + "items": { + "type": "string" + }, + "description": "List of URLs to crawl and extract content from" + } + }, + "required": ["urls"] + }`) + var schema map[string]any + if err := json.Unmarshal(schemaBytes, &schema); err != nil { + return nil + } + return schema +} + +func (g *BrowserCrawler) Execute(ctx context.Context, args map[string]any) (*CrawlResponse, error) { + urlsRaw, ok := args["urls"].([]any) + if !ok { + return nil, fmt.Errorf("urls parameter is required and must be an array of strings") + } + + urls := make([]string, 0, len(urlsRaw)) + for _, u := range urlsRaw { + if urlStr, ok := u.(string); ok { + urls = append(urls, urlStr) + } + } + + if len(urls) == 0 { + return nil, fmt.Errorf("at least one URL is required") + } + + return g.performWebCrawl(ctx, urls) +} + +// performWebCrawl handles the actual HTTP request to ollama.com crawl API +func (g *BrowserCrawler) performWebCrawl(ctx context.Context, urls []string) (*CrawlResponse, error) { + result := &CrawlResponse{Results: make(map[string][]CrawlResult, len(urls))} + + for _, targetURL := range urls { + fetchResp, err := performWebFetch(ctx, targetURL) + if err != nil { + return nil, fmt.Errorf("web_fetch failed for %q: %w", targetURL, err) + } + + links := make([]CrawlLink, 0, len(fetchResp.Links)) + for _, link := range fetchResp.Links { + links = append(links, CrawlLink{URL: link, Href: link}) + } + + snippet := truncateString(fetchResp.Content, 400) + + result.Results[targetURL] = []CrawlResult{{ + Title: fetchResp.Title, + URL: targetURL, + Content: CrawlContent{ + Snippet: snippet, + FullText: fetchResp.Content, + }, + Extras: CrawlExtras{Links: links}, + }} + } + + return result, nil +} diff --git a/app/tools/browser_test.go b/app/tools/browser_test.go new file mode 100644 index 000000000..05b5584b2 --- /dev/null +++ b/app/tools/browser_test.go @@ -0,0 +1,147 @@ +//go:build windows || darwin + +package tools + +import ( + "strings" + "testing" + "time" + + "github.com/ollama/ollama/app/ui/responses" +) + +func makeTestPage(url string) *responses.Page { + return &responses.Page{ + URL: url, + Title: "Title " + url, + Text: "Body for " + url, + Lines: []string{"line1", "line2", "line3"}, + Links: map[int]string{0: url}, + FetchedAt: time.Now(), + } +} + +func TestBrowser_Scroll_AppendsOnlyPageStack(t *testing.T) { + b := NewBrowser(&responses.BrowserStateData{PageStack: []string{}, ViewTokens: 1024, URLToPage: map[string]*responses.Page{}}) + p1 := makeTestPage("https://example.com/1") + b.savePage(p1) + initialStackLen := len(b.state.Data.PageStack) + initialMapLen := len(b.state.Data.URLToPage) + + bo := NewBrowserOpen(b) + // Scroll without id — should push only to PageStack + _, _, err := bo.Execute(t.Context(), map[string]any{"loc": float64(1), "num_lines": float64(1)}) + if err != nil { + t.Fatalf("scroll execute failed: %v", err) + } + + if got, want := len(b.state.Data.PageStack), initialStackLen+1; got != want { + t.Fatalf("page stack length = %d, want %d", got, want) + } + if got, want := len(b.state.Data.URLToPage), initialMapLen; got != want { + t.Fatalf("url_to_page length changed = %d, want %d", got, want) + } +} + +func TestBrowserOpen_UseCacheByURL(t *testing.T) { + b := NewBrowser(&responses.BrowserStateData{PageStack: []string{}, ViewTokens: 1024, URLToPage: map[string]*responses.Page{}}) + bo := NewBrowserOpen(b) + + p := makeTestPage("https://example.com/cached") + b.state.Data.URLToPage[p.URL] = p + initialStackLen := len(b.state.Data.PageStack) + initialMapLen := len(b.state.Data.URLToPage) + + _, _, err := bo.Execute(t.Context(), map[string]any{"id": p.URL}) + if err != nil { + t.Fatalf("open cached execute failed: %v", err) + } + + if got, want := len(b.state.Data.PageStack), initialStackLen+1; got != want { + t.Fatalf("page stack length = %d, want %d", got, want) + } + if got, want := len(b.state.Data.URLToPage), initialMapLen; got != want { + t.Fatalf("url_to_page length changed = %d, want %d", got, want) + } +} + +func TestDisplayPage_InvalidLoc(t *testing.T) { + b := NewBrowser(&responses.BrowserStateData{PageStack: []string{}, ViewTokens: 1024, URLToPage: map[string]*responses.Page{}}) + p := makeTestPage("https://example.com/x") + // ensure lines are set + p.Lines = []string{"a", "b"} + _, err := b.displayPage(p, 0, 10, -1) + if err == nil || !strings.Contains(err.Error(), "invalid location") { + t.Fatalf("expected invalid location error, got %v", err) + } +} + +func TestBrowserOpen_LinkId_UsesCacheAndAppends(t *testing.T) { + b := NewBrowser(&responses.BrowserStateData{PageStack: []string{}, ViewTokens: 1024, URLToPage: map[string]*responses.Page{}}) + // Seed a main page with a link id 0 to a linked URL + main := makeTestPage("https://example.com/main") + linked := makeTestPage("https://example.com/linked") + main.Links = map[int]string{0: linked.URL} + // Save the main page (adds to PageStack and URLToPage) + b.savePage(main) + // Pre-cache the linked page so open by id avoids network + b.state.Data.URLToPage[linked.URL] = linked + + initialStackLen := len(b.state.Data.PageStack) + initialMapLen := len(b.state.Data.URLToPage) + + bo := NewBrowserOpen(b) + _, _, err := bo.Execute(t.Context(), map[string]any{"id": float64(0)}) + if err != nil { + t.Fatalf("open by link id failed: %v", err) + } + + if got, want := len(b.state.Data.PageStack), initialStackLen+1; got != want { + t.Fatalf("page stack length = %d, want %d", got, want) + } + if got, want := len(b.state.Data.URLToPage), initialMapLen; got != want { + t.Fatalf("url_to_page length changed = %d, want %d", got, want) + } + if last := b.state.Data.PageStack[len(b.state.Data.PageStack)-1]; last != linked.URL { + t.Fatalf("last page in stack = %s, want %s", last, linked.URL) + } +} + +func TestWrapLines_PreserveAndWidth(t *testing.T) { + long := strings.Repeat("word ", 50) + text := "Line1\n\n" + long + "\nLine3" + lines := wrapLines(text, 40) + + // Ensure empty line preserved at index 1 + if lines[1] != "" { + t.Fatalf("expected preserved empty line at index 1, got %q", lines[1]) + } + // All lines should be <= 40 chars + for i, l := range lines { + if len(l) > 40 { + t.Fatalf("line %d exceeds width: %d > 40", i, len(l)) + } + } +} + +func TestDisplayPage_FormatHeaderAndLines(t *testing.T) { + b := NewBrowser(&responses.BrowserStateData{PageStack: []string{}, ViewTokens: 1024, URLToPage: map[string]*responses.Page{}}) + p := &responses.Page{ + URL: "https://example.com/x", + Title: "Example", + Lines: []string{"URL: https://example.com/x", "A", "B", "C"}, + } + out, err := b.displayPage(p, 3, 0, 2) + if err != nil { + t.Fatalf("displayPage failed: %v", err) + } + if !strings.HasPrefix(out, "[3] Example(") { + t.Fatalf("header not formatted as expected: %q", out) + } + if !strings.Contains(out, "L0:\n") { + t.Fatalf("missing L0 label: %q", out) + } + if !strings.Contains(out, "L1: URL: https://example.com/x\n") || !strings.Contains(out, "L2: A\n") { + t.Fatalf("missing expected line numbers/content: %q", out) + } +} diff --git a/app/tools/browser_websearch.go b/app/tools/browser_websearch.go new file mode 100644 index 000000000..dd47fb886 --- /dev/null +++ b/app/tools/browser_websearch.go @@ -0,0 +1,143 @@ +//go:build windows || darwin + +package tools + +import ( + "context" + "encoding/json" + "fmt" + "strconv" + "time" +) + +// WebSearchContent represents the content of a search result +type WebSearchContent struct { + Snippet string `json:"snippet"` + FullText string `json:"full_text"` +} + +// WebSearchMetadata represents metadata for a search result +type WebSearchMetadata struct { + PublishedDate *time.Time `json:"published_date,omitempty"` +} + +// WebSearchResult represents a single search result +type WebSearchResult struct { + Title string `json:"title"` + URL string `json:"url"` + Content WebSearchContent `json:"content"` + Metadata WebSearchMetadata `json:"metadata"` +} + +// WebSearchResponse represents the complete response from the websearch API +type WebSearchResponse struct { + Results map[string][]WebSearchResult `json:"results"` +} + +// BrowserWebSearch tool for searching the web using ollama.com search API +type BrowserWebSearch struct{} + +func (w *BrowserWebSearch) Name() string { + return "gpt_oss_web_search" +} + +func (w *BrowserWebSearch) Description() string { + return "Search the web for real-time information using ollama.com search API." +} + +func (w *BrowserWebSearch) Prompt() string { + return `Use the gpt_oss_web_search tool to search the web. +1. Come up with a list of search queries to get comprehensive information (typically 2-3 related queries work well) +2. Use the gpt_oss_web_search tool with multiple queries to get results organized by query +3. Use the search results to provide current up to date, accurate information + +Today's date is ` + time.Now().Format("January 2, 2006") + ` +Add "` + time.Now().Format("January 2, 2006") + `" for news queries and ` + strconv.Itoa(time.Now().Year()+1) + ` for other queries that need current information.` +} + +func (w *BrowserWebSearch) Schema() map[string]any { + schemaBytes := []byte(`{ + "type": "object", + "properties": { + "queries": { + "type": "array", + "items": { + "type": "string" + }, + "description": "List of search queries to look up" + }, + "max_results": { + "type": "integer", + "description": "Maximum number of results to return per query (default: 2) up to 5", + "default": 2 + } + }, + "required": ["queries"] + }`) + var schema map[string]any + if err := json.Unmarshal(schemaBytes, &schema); err != nil { + return nil + } + return schema +} + +func (w *BrowserWebSearch) Execute(ctx context.Context, args map[string]any) (any, error) { + queriesRaw, ok := args["queries"].([]any) + if !ok { + return nil, fmt.Errorf("queries parameter is required and must be an array of strings") + } + + queries := make([]string, 0, len(queriesRaw)) + for _, q := range queriesRaw { + if query, ok := q.(string); ok { + queries = append(queries, query) + } + } + + if len(queries) == 0 { + return nil, fmt.Errorf("at least one query is required") + } + + maxResults := 5 + if mr, ok := args["max_results"].(int); ok { + maxResults = mr + } + + return w.performWebSearch(ctx, queries, maxResults) +} + +// performWebSearch handles the actual HTTP request to ollama.com search API +func (w *BrowserWebSearch) performWebSearch(ctx context.Context, queries []string, maxResults int) (*WebSearchResponse, error) { + response := &WebSearchResponse{Results: make(map[string][]WebSearchResult, len(queries))} + + for _, query := range queries { + searchResp, err := performWebSearch(ctx, query, maxResults) + if err != nil { + return nil, fmt.Errorf("web_search failed for %q: %w", query, err) + } + + converted := make([]WebSearchResult, 0, len(searchResp.Results)) + for _, item := range searchResp.Results { + converted = append(converted, WebSearchResult{ + Title: item.Title, + URL: item.URL, + Content: WebSearchContent{ + Snippet: truncateString(item.Content, 400), + FullText: item.Content, + }, + Metadata: WebSearchMetadata{}, + }) + } + + response.Results[query] = converted + } + + return response, nil +} + +func truncateString(input string, limit int) string { + if limit <= 0 || len(input) <= limit { + return input + } + return input[:limit] +} diff --git a/app/tools/tools.go b/app/tools/tools.go new file mode 100644 index 000000000..b36727890 --- /dev/null +++ b/app/tools/tools.go @@ -0,0 +1,122 @@ +//go:build windows || darwin + +package tools + +import ( + "context" + "encoding/json" + "fmt" +) + +// Tool defines the interface that all tools must implement +type Tool interface { + // Name returns the unique identifier for the tool + Name() string + + // Description returns a human-readable description of what the tool does + Description() string + + // Schema returns the JSON schema for the tool's parameters + Schema() map[string]any + + // Execute runs the tool with the given arguments and returns result to store in db, and a string result for the model + Execute(ctx context.Context, args map[string]any) (any, string, error) + + // Prompt returns a prompt for the tool + Prompt() string +} + +// Registry manages the available tools and their execution +type Registry struct { + tools map[string]Tool + workingDir string // Working directory for all tool operations +} + +// NewRegistry creates a new tool registry with no tools +func NewRegistry() *Registry { + return &Registry{ + tools: make(map[string]Tool), + } +} + +// Register adds a tool to the registry +func (r *Registry) Register(tool Tool) { + r.tools[tool.Name()] = tool +} + +// Get retrieves a tool by name +func (r *Registry) Get(name string) (Tool, bool) { + tool, exists := r.tools[name] + return tool, exists +} + +// List returns all available tools +func (r *Registry) List() []Tool { + tools := make([]Tool, 0, len(r.tools)) + for _, tool := range r.tools { + tools = append(tools, tool) + } + return tools +} + +// SetWorkingDir sets the working directory for all tool operations +func (r *Registry) SetWorkingDir(dir string) { + r.workingDir = dir +} + +// Execute runs a tool with the given name and arguments +func (r *Registry) Execute(ctx context.Context, name string, args map[string]any) (any, string, error) { + tool, ok := r.tools[name] + if !ok { + return nil, "", fmt.Errorf("unknown tool: %s", name) + } + + result, text, err := tool.Execute(ctx, args) + if err != nil { + return nil, "", err + } + return result, text, nil +} + +// ToolCall represents a request to execute a tool +type ToolCall struct { + ID string `json:"id"` + Type string `json:"type"` + Function ToolFunction `json:"function"` +} + +// ToolFunction represents the function call details +type ToolFunction struct { + Name string `json:"name"` + Arguments json.RawMessage `json:"arguments"` +} + +// ToolResult represents the result of a tool execution +type ToolResult struct { + ToolCallID string `json:"tool_call_id"` + Content any `json:"content"` + Error string `json:"error,omitempty"` +} + +// ToolSchemas returns all tools as schema maps suitable for API calls +func (r *Registry) AvailableTools() []map[string]any { + schemas := make([]map[string]any, 0, len(r.tools)) + for _, tool := range r.tools { + schema := map[string]any{ + "name": tool.Name(), + "description": tool.Description(), + "schema": tool.Schema(), + } + schemas = append(schemas, schema) + } + return schemas +} + +// ToolNames returns a list of all tool names +func (r *Registry) ToolNames() []string { + names := make([]string, 0, len(r.tools)) + for name := range r.tools { + names = append(names, name) + } + return names +} diff --git a/app/tools/web_fetch.go b/app/tools/web_fetch.go new file mode 100644 index 000000000..67a582d35 --- /dev/null +++ b/app/tools/web_fetch.go @@ -0,0 +1,128 @@ +//go:build windows || darwin + +package tools + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + "net/url" + "strconv" + "strings" + "time" + + "github.com/ollama/ollama/auth" +) + +type WebFetch struct{} + +type FetchRequest struct { + URL string `json:"url"` +} + +type FetchResponse struct { + Title string `json:"title"` + Content string `json:"content"` + Links []string `json:"links"` +} + +func (w *WebFetch) Name() string { + return "web_fetch" +} + +func (w *WebFetch) Description() string { + return "Crawl and extract text content from web pages" +} + +func (g *WebFetch) Schema() map[string]any { + schemaBytes := []byte(`{ + "type": "object", + "properties": { + "url": { + "type": "string", + "description": "URL to crawl and extract content from" + } + }, + "required": ["url"] + }`) + var schema map[string]any + if err := json.Unmarshal(schemaBytes, &schema); err != nil { + return nil + } + return schema +} + +func (w *WebFetch) Prompt() string { + return "" +} + +func (w *WebFetch) Execute(ctx context.Context, args map[string]any) (any, string, error) { + urlRaw, ok := args["url"] + if !ok { + return nil, "", fmt.Errorf("url parameter is required") + } + urlStr, ok := urlRaw.(string) + if !ok || strings.TrimSpace(urlStr) == "" { + return nil, "", fmt.Errorf("url must be a non-empty string") + } + + result, err := performWebFetch(ctx, urlStr) + if err != nil { + return nil, "", err + } + + return result, "", nil +} + +func performWebFetch(ctx context.Context, targetURL string) (*FetchResponse, error) { + reqBody := FetchRequest{URL: targetURL} + jsonBody, err := json.Marshal(reqBody) + if err != nil { + return nil, fmt.Errorf("failed to marshal request body: %w", err) + } + + crawlURL, err := url.Parse("https://ollama.com/api/web_fetch") + if err != nil { + return nil, fmt.Errorf("failed to parse fetch URL: %w", err) + } + + query := crawlURL.Query() + query.Add("ts", strconv.FormatInt(time.Now().Unix(), 10)) + crawlURL.RawQuery = query.Encode() + + data := fmt.Appendf(nil, "%s,%s", http.MethodPost, crawlURL.RequestURI()) + signature, err := auth.Sign(ctx, data) + if err != nil { + return nil, fmt.Errorf("failed to sign request: %w", err) + } + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, crawlURL.String(), bytes.NewBuffer(jsonBody)) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + req.Header.Set("Content-Type", "application/json") + if signature != "" { + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", signature)) + } + + client := &http.Client{Timeout: 30 * time.Second} + resp, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to execute fetch request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("fetch API error (status %d)", resp.StatusCode) + } + + var result FetchResponse + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return nil, fmt.Errorf("failed to decode response: %w", err) + } + + return &result, nil +} diff --git a/app/tools/web_search.go b/app/tools/web_search.go new file mode 100644 index 000000000..1cb12ab76 --- /dev/null +++ b/app/tools/web_search.go @@ -0,0 +1,145 @@ +//go:build windows || darwin + +package tools + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + "net/url" + "strconv" + "strings" + "time" + + "github.com/ollama/ollama/auth" +) + +type WebSearch struct{} + +type SearchRequest struct { + Query string `json:"query"` + MaxResults int `json:"max_results,omitempty"` +} + +type SearchResult struct { + Title string `json:"title"` + URL string `json:"url"` + Content string `json:"content"` +} + +type SearchResponse struct { + Results []SearchResult `json:"results"` +} + +func (w *WebSearch) Name() string { + return "web_search" +} + +func (w *WebSearch) Description() string { + return "Search the web for real-time information using ollama.com web search API." +} + +func (w *WebSearch) Prompt() string { + return "" +} + +func (g *WebSearch) Schema() map[string]any { + schemaBytes := []byte(`{ + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "The search query to execute" + }, + "max_results": { + "type": "integer", + "description": "Maximum number of search results to return", + "default": 3 + } + }, + "required": ["query"] + }`) + var schema map[string]any + if err := json.Unmarshal(schemaBytes, &schema); err != nil { + return nil + } + return schema +} + +func (w *WebSearch) Execute(ctx context.Context, args map[string]any) (any, string, error) { + rawQuery, ok := args["query"] + if !ok { + return nil, "", fmt.Errorf("query parameter is required") + } + + queryStr, ok := rawQuery.(string) + if !ok || strings.TrimSpace(queryStr) == "" { + return nil, "", fmt.Errorf("query must be a non-empty string") + } + + maxResults := 5 + if v, ok := args["max_results"].(float64); ok && int(v) > 0 { + maxResults = int(v) + } + + result, err := performWebSearch(ctx, queryStr, maxResults) + if err != nil { + return nil, "", err + } + + return result, "", nil +} + +func performWebSearch(ctx context.Context, query string, maxResults int) (*SearchResponse, error) { + reqBody := SearchRequest{Query: query, MaxResults: maxResults} + + jsonBody, err := json.Marshal(reqBody) + if err != nil { + return nil, fmt.Errorf("failed to marshal request body: %w", err) + } + + searchURL, err := url.Parse("https://ollama.com/api/web_search") + if err != nil { + return nil, fmt.Errorf("failed to parse search URL: %w", err) + } + + q := searchURL.Query() + q.Add("ts", strconv.FormatInt(time.Now().Unix(), 10)) + searchURL.RawQuery = q.Encode() + + data := fmt.Appendf(nil, "%s,%s", http.MethodPost, searchURL.RequestURI()) + signature, err := auth.Sign(ctx, data) + if err != nil { + return nil, fmt.Errorf("failed to sign request: %w", err) + } + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, searchURL.String(), bytes.NewBuffer(jsonBody)) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + req.Header.Set("Content-Type", "application/json") + if signature != "" { + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", signature)) + } + + client := &http.Client{Timeout: 10 * time.Second} + resp, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to execute search request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("search API error (status %d)", resp.StatusCode) + } + + var result SearchResponse + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return nil, fmt.Errorf("failed to decode response: %w", err) + } + + return &result, nil +} diff --git a/app/tray/commontray/types.go b/app/tray/commontray/types.go deleted file mode 100644 index ed633dc93..000000000 --- a/app/tray/commontray/types.go +++ /dev/null @@ -1,24 +0,0 @@ -package commontray - -var ( - Title = "Ollama" - ToolTip = "Ollama" - - UpdateIconName = "tray_upgrade" - IconName = "tray" -) - -type Callbacks struct { - Quit chan struct{} - Update chan struct{} - DoFirstUse chan struct{} - ShowLogs chan struct{} -} - -type OllamaTray interface { - GetCallbacks() Callbacks - Run() - UpdateAvailable(ver string) error - DisplayFirstUseNotification() error - Quit() -} diff --git a/app/tray/tray.go b/app/tray/tray.go deleted file mode 100644 index dfa634353..000000000 --- a/app/tray/tray.go +++ /dev/null @@ -1,28 +0,0 @@ -package tray - -import ( - "fmt" - "runtime" - - "github.com/ollama/ollama/app/assets" - "github.com/ollama/ollama/app/tray/commontray" -) - -func NewTray() (commontray.OllamaTray, error) { - extension := ".png" - if runtime.GOOS == "windows" { - extension = ".ico" - } - iconName := commontray.UpdateIconName + extension - updateIcon, err := assets.GetIcon(iconName) - if err != nil { - return nil, fmt.Errorf("failed to load icon %s: %w", iconName, err) - } - iconName = commontray.IconName + extension - icon, err := assets.GetIcon(iconName) - if err != nil { - return nil, fmt.Errorf("failed to load icon %s: %w", iconName, err) - } - - return InitPlatformTray(icon, updateIcon) -} diff --git a/app/tray/tray_nonwindows.go b/app/tray/tray_nonwindows.go deleted file mode 100644 index a03d233ea..000000000 --- a/app/tray/tray_nonwindows.go +++ /dev/null @@ -1,13 +0,0 @@ -//go:build !windows - -package tray - -import ( - "errors" - - "github.com/ollama/ollama/app/tray/commontray" -) - -func InitPlatformTray(icon, updateIcon []byte) (commontray.OllamaTray, error) { - return nil, errors.New("not implemented") -} diff --git a/app/tray/tray_windows.go b/app/tray/tray_windows.go deleted file mode 100644 index 086fc7943..000000000 --- a/app/tray/tray_windows.go +++ /dev/null @@ -1,10 +0,0 @@ -package tray - -import ( - "github.com/ollama/ollama/app/tray/commontray" - "github.com/ollama/ollama/app/tray/wintray" -) - -func InitPlatformTray(icon, updateIcon []byte) (commontray.OllamaTray, error) { - return wintray.InitTray(icon, updateIcon) -} diff --git a/app/tray/wintray/eventloop.go b/app/tray/wintray/eventloop.go deleted file mode 100644 index 35608a49e..000000000 --- a/app/tray/wintray/eventloop.go +++ /dev/null @@ -1,181 +0,0 @@ -//go:build windows - -package wintray - -import ( - "fmt" - "log/slog" - "sync" - "unsafe" - - "golang.org/x/sys/windows" -) - -var quitOnce sync.Once - -func (t *winTray) Run() { - nativeLoop() -} - -func nativeLoop() { - // Main message pump. - slog.Debug("starting event handling loop") - m := &struct { - WindowHandle windows.Handle - Message uint32 - Wparam uintptr - Lparam uintptr - Time uint32 - Pt point - LPrivate uint32 - }{} - for { - ret, _, err := pGetMessage.Call(uintptr(unsafe.Pointer(m)), 0, 0, 0) - - // If the function retrieves a message other than WM_QUIT, the return value is nonzero. - // If the function retrieves the WM_QUIT message, the return value is zero. - // If there is an error, the return value is -1 - // https://msdn.microsoft.com/en-us/library/windows/desktop/ms644936(v=vs.85).aspx - switch int32(ret) { - case -1: - slog.Error(fmt.Sprintf("get message failure: %v", err)) - return - case 0: - return - default: - pTranslateMessage.Call(uintptr(unsafe.Pointer(m))) //nolint:errcheck - pDispatchMessage.Call(uintptr(unsafe.Pointer(m))) //nolint:errcheck - } - } -} - -// WindowProc callback function that processes messages sent to a window. -// https://msdn.microsoft.com/en-us/library/windows/desktop/ms633573(v=vs.85).aspx -func (t *winTray) wndProc(hWnd windows.Handle, message uint32, wParam, lParam uintptr) (lResult uintptr) { - const ( - WM_RBUTTONUP = 0x0205 - WM_LBUTTONUP = 0x0202 - WM_COMMAND = 0x0111 - WM_ENDSESSION = 0x0016 - WM_CLOSE = 0x0010 - WM_DESTROY = 0x0002 - WM_MOUSEMOVE = 0x0200 - WM_LBUTTONDOWN = 0x0201 - ) - switch message { - case WM_COMMAND: - menuItemId := int32(wParam) - // https://docs.microsoft.com/en-us/windows/win32/menurc/wm-command#menus - switch menuItemId { - case quitMenuID: - select { - case t.callbacks.Quit <- struct{}{}: - // should not happen but in case not listening - default: - slog.Error("no listener on Quit") - } - case updateMenuID: - select { - case t.callbacks.Update <- struct{}{}: - // should not happen but in case not listening - default: - slog.Error("no listener on Update") - } - case diagLogsMenuID: - select { - case t.callbacks.ShowLogs <- struct{}{}: - // should not happen but in case not listening - default: - slog.Error("no listener on ShowLogs") - } - default: - slog.Debug(fmt.Sprintf("Unexpected menu item id: %d", menuItemId)) - } - case WM_CLOSE: - boolRet, _, err := pDestroyWindow.Call(uintptr(t.window)) - if boolRet == 0 { - slog.Error(fmt.Sprintf("failed to destroy window: %s", err)) - } - err = t.wcex.unregister() - if err != nil { - slog.Error(fmt.Sprintf("failed to unregister window %s", err)) - } - case WM_DESTROY: - // same as WM_ENDSESSION, but throws 0 exit code after all - defer pPostQuitMessage.Call(uintptr(int32(0))) //nolint:errcheck - fallthrough - case WM_ENDSESSION: - t.muNID.Lock() - if t.nid != nil { - err := t.nid.delete() - if err != nil { - slog.Error(fmt.Sprintf("failed to delete nid: %s", err)) - } - } - t.muNID.Unlock() - case t.wmSystrayMessage: - switch lParam { - case WM_MOUSEMOVE, WM_LBUTTONDOWN: - // Ignore these... - case WM_RBUTTONUP, WM_LBUTTONUP: - err := t.showMenu() - if err != nil { - slog.Error(fmt.Sprintf("failed to show menu: %s", err)) - } - case 0x405: // TODO - how is this magic value derived for the notification left click - if t.pendingUpdate { - select { - case t.callbacks.Update <- struct{}{}: - // should not happen but in case not listening - default: - slog.Error("no listener on Update") - } - } else { - select { - case t.callbacks.DoFirstUse <- struct{}{}: - // should not happen but in case not listening - default: - slog.Error("no listener on DoFirstUse") - } - } - case 0x404: // Middle click or close notification - // slog.Debug("doing nothing on close of first time notification") - default: - // 0x402 also seems common - what is it? - slog.Debug(fmt.Sprintf("unmanaged app message, lParm: 0x%x", lParam)) - } - case t.wmTaskbarCreated: // on explorer.exe restarts - t.muNID.Lock() - err := t.nid.add() - if err != nil { - slog.Error(fmt.Sprintf("failed to refresh the taskbar on explorer restart: %s", err)) - } - t.muNID.Unlock() - default: - // Calls the default window procedure to provide default processing for any window messages that an application does not process. - // https://msdn.microsoft.com/en-us/library/windows/desktop/ms633572(v=vs.85).aspx - lResult, _, _ = pDefWindowProc.Call( - uintptr(hWnd), - uintptr(message), - wParam, - lParam, - ) - } - return -} - -func (t *winTray) Quit() { - quitOnce.Do(quit) -} - -func quit() { - boolRet, _, err := pPostMessage.Call( - uintptr(wt.window), - WM_CLOSE, - 0, - 0, - ) - if boolRet == 0 { - slog.Error(fmt.Sprintf("failed to post close message on shutdown %s", err)) - } -} diff --git a/app/types/not/found.go b/app/types/not/found.go new file mode 100644 index 000000000..9294e0155 --- /dev/null +++ b/app/types/not/found.go @@ -0,0 +1,28 @@ +//go:build windows || darwin + +package not + +import ( + "errors" +) + +// Found is an error that indicates that a value was not found. It +// may be used by low-level packages to signal to higher-level +// packages that a value was not found. +// +// It exists to avoid using errors.New("not found") in multiple +// packages to mean the same thing. +// +// Found should not be used directly. Instead it should be wrapped +// or joined using errors.Join or fmt.Errorf, etc. +// +// Errors wrapping Found should provide additional context, e.g. +// fmt.Errorf("%w: %s", not.Found, key) +// +//lint:ignore ST1012 This is a sentinel error intended to be read like not.Found. +var Found = errors.New("not found") + +// Available is an error that indicates that a value is not available. +// +//lint:ignore ST1012 This is a sentinel error intended to be read like not.Available. +var Available = errors.New("not available") diff --git a/app/types/not/valids.go b/app/types/not/valids.go new file mode 100644 index 000000000..d19894270 --- /dev/null +++ b/app/types/not/valids.go @@ -0,0 +1,55 @@ +//go:build windows || darwin + +package not + +import ( + "fmt" +) + +type ValidError struct { + name string + msg string + args []any +} + +// Valid returns a new validation error with the given name and message. +func Valid(name, message string, args ...any) error { + return ValidError{name, message, args} +} + +// Message returns the formatted message for the validation error. +func (e *ValidError) Message() string { + return fmt.Sprintf(e.msg, e.args...) +} + +// Error implements the error interface. +func (e ValidError) Error() string { + return fmt.Sprintf("invalid %s: %s", e.name, e.Message()) +} + +func (e ValidError) Field() string { + return e.name +} + +// Valids is for building a list of validation errors. +type Valids []ValidError + +// Addf adds a validation error to the list with a formatted message using fmt.Sprintf. +func (b *Valids) Add(name, message string, args ...any) { + *b = append(*b, ValidError{name, message, args}) +} + +func (b Valids) Error() string { + if len(b) == 0 { + return "" + } + + var result string + for i, err := range b { + if i > 0 { + result += "; " + } + result += err.Error() + } + return result +} diff --git a/app/types/not/valids_test.go b/app/types/not/valids_test.go new file mode 100644 index 000000000..4a64822e4 --- /dev/null +++ b/app/types/not/valids_test.go @@ -0,0 +1,43 @@ +//go:build windows || darwin + +package not_test + +import ( + "errors" + "fmt" + + "github.com/ollama/ollama/app/types/not" +) + +func ExampleValids() { + // This example demonstrates how to use the Valids type to create + // a list of validation errors. + // + // The Valids type is a slice of ValidError values. Each ValidError + // value represents a validation error. + // + // The Valids type has an Error method that returns a single error + // value that represents all of the validation errors in the list. + // + // The Valids type is useful for collecting multiple validation errors + // and returning them as a single error value. + + validate := func() error { + var b not.Valids + b.Add("name", "must be a valid name") + b.Add("email", "%q: must be a valid email address", "invalid.email") + return b + } + + err := validate() + var nv not.Valids + if errors.As(err, &nv) { + for _, v := range nv { + fmt.Println(v) + } + } + + // Output: + // invalid name: must be a valid name + // invalid email: "invalid.email": must be a valid email address +} diff --git a/app/ui/app.go b/app/ui/app.go new file mode 100644 index 000000000..d73df2c02 --- /dev/null +++ b/app/ui/app.go @@ -0,0 +1,44 @@ +//go:build windows || darwin + +package ui + +import ( + "bytes" + "embed" + "errors" + "io/fs" + "net/http" + "strings" + "time" +) + +//go:embed app/dist +var appFS embed.FS + +// appHandler returns an HTTP handler that serves the React SPA. +// It tries to serve real files first, then falls back to index.html for React Router. +func (s *Server) appHandler() http.Handler { + // Strip the dist prefix so URLs look clean + fsys, _ := fs.Sub(appFS, "app/dist") + fileServer := http.FileServer(http.FS(fsys)) + + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + p := strings.TrimPrefix(r.URL.Path, "/") + if _, err := fsys.Open(p); err == nil { + // Serve the file directly + fileServer.ServeHTTP(w, r) + return + } + // Fallback – serve index.html for unknown paths so React Router works + data, err := fs.ReadFile(fsys, "index.html") + if err != nil { + if errors.Is(err, fs.ErrNotExist) { + http.NotFound(w, r) + } else { + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + } + return + } + http.ServeContent(w, r, "index.html", time.Time{}, bytes.NewReader(data)) + }) +} diff --git a/app/ui/app/.gitignore b/app/ui/app/.gitignore new file mode 100644 index 000000000..9705d1e63 --- /dev/null +++ b/app/ui/app/.gitignore @@ -0,0 +1,30 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +.vite/ +.claude/ + +*storybook.log +storybook-static diff --git a/app/ui/app/.prettierignore b/app/ui/app/.prettierignore new file mode 100644 index 000000000..21c55e20d --- /dev/null +++ b/app/ui/app/.prettierignore @@ -0,0 +1 @@ +*.gen.ts \ No newline at end of file diff --git a/app/ui/app/.prettierrc b/app/ui/app/.prettierrc new file mode 100644 index 000000000..1957463f5 --- /dev/null +++ b/app/ui/app/.prettierrc @@ -0,0 +1,6 @@ +{ + "trailingComma": "all", + "semi": true, + "singleQuote": false, + "printWidth": 80 +} diff --git a/app/ui/app/codegen/gotypes.gen.ts b/app/ui/app/codegen/gotypes.gen.ts new file mode 100644 index 000000000..a077c8546 --- /dev/null +++ b/app/ui/app/codegen/gotypes.gen.ts @@ -0,0 +1,611 @@ +/* Do not change, this code is generated from Golang structs */ + + +export class ChatInfo { + id: string; + title: string; + userExcerpt: string; + createdAt: Date; + updatedAt: Date; + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.id = source["id"]; + this.title = source["title"]; + this.userExcerpt = source["userExcerpt"]; + this.createdAt = new Date(source["createdAt"]); + this.updatedAt = new Date(source["updatedAt"]); + } +} +export class ChatsResponse { + chatInfos: ChatInfo[]; + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.chatInfos = this.convertValues(source["chatInfos"], ChatInfo); + } + + convertValues(a: any, classs: any, asMap: boolean = false): any { + if (!a) { + return a; + } + if (Array.isArray(a)) { + return (a as any[]).map(elem => this.convertValues(elem, classs)); + } else if ("object" === typeof a) { + if (asMap) { + for (const key of Object.keys(a)) { + a[key] = new classs(a[key]); + } + return a; + } + return new classs(a); + } + return a; + } +} +export class Time { + + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + + } +} +export class ToolFunction { + name: string; + arguments: string; + result?: any; + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.name = source["name"]; + this.arguments = source["arguments"]; + this.result = source["result"]; + } +} +export class ToolCall { + type: string; + function: ToolFunction; + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.type = source["type"]; + this.function = this.convertValues(source["function"], ToolFunction); + } + + convertValues(a: any, classs: any, asMap: boolean = false): any { + if (!a) { + return a; + } + if (Array.isArray(a)) { + return (a as any[]).map(elem => this.convertValues(elem, classs)); + } else if ("object" === typeof a) { + if (asMap) { + for (const key of Object.keys(a)) { + a[key] = new classs(a[key]); + } + return a; + } + return new classs(a); + } + return a; + } +} +export class File { + filename: string; + data: number[]; + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.filename = source["filename"]; + this.data = source["data"]; + } +} +export class Message { + role: string; + content: string; + thinking: string; + stream: boolean; + model?: string; + attachments?: File[]; + tool_calls?: ToolCall[]; + tool_call?: ToolCall; + tool_name?: string; + tool_result?: number[]; + created_at: Time; + updated_at: Time; + thinkingTimeStart?: Date | undefined; + thinkingTimeEnd?: Date | undefined; + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.role = source["role"]; + this.content = source["content"]; + this.thinking = source["thinking"]; + this.stream = source["stream"]; + this.model = source["model"]; + this.attachments = this.convertValues(source["attachments"], File); + this.tool_calls = this.convertValues(source["tool_calls"], ToolCall); + this.tool_call = this.convertValues(source["tool_call"], ToolCall); + this.tool_name = source["tool_name"]; + this.tool_result = source["tool_result"]; + this.created_at = this.convertValues(source["created_at"], Time); + this.updated_at = this.convertValues(source["updated_at"], Time); + this.thinkingTimeStart = source["thinkingTimeStart"] && new Date(source["thinkingTimeStart"]); + this.thinkingTimeEnd = source["thinkingTimeEnd"] && new Date(source["thinkingTimeEnd"]); + } + + convertValues(a: any, classs: any, asMap: boolean = false): any { + if (!a) { + return a; + } + if (Array.isArray(a)) { + return (a as any[]).map(elem => this.convertValues(elem, classs)); + } else if ("object" === typeof a) { + if (asMap) { + for (const key of Object.keys(a)) { + a[key] = new classs(a[key]); + } + return a; + } + return new classs(a); + } + return a; + } +} +export class Chat { + id: string; + messages: Message[]; + title: string; + created_at: Time; + browser_state?: BrowserStateData; + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.id = source["id"]; + this.messages = this.convertValues(source["messages"], Message); + this.title = source["title"]; + this.created_at = this.convertValues(source["created_at"], Time); + this.browser_state = source["browser_state"]; + } + + convertValues(a: any, classs: any, asMap: boolean = false): any { + if (!a) { + return a; + } + if (Array.isArray(a)) { + return (a as any[]).map(elem => this.convertValues(elem, classs)); + } else if ("object" === typeof a) { + if (asMap) { + for (const key of Object.keys(a)) { + a[key] = new classs(a[key]); + } + return a; + } + return new classs(a); + } + return a; + } +} +export class ChatResponse { + chat: Chat; + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.chat = this.convertValues(source["chat"], Chat); + } + + convertValues(a: any, classs: any, asMap: boolean = false): any { + if (!a) { + return a; + } + if (Array.isArray(a)) { + return (a as any[]).map(elem => this.convertValues(elem, classs)); + } else if ("object" === typeof a) { + if (asMap) { + for (const key of Object.keys(a)) { + a[key] = new classs(a[key]); + } + return a; + } + return new classs(a); + } + return a; + } +} +export class Model { + model: string; + digest?: string; + modified_at?: Time; + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.model = source["model"]; + this.digest = source["digest"]; + this.modified_at = this.convertValues(source["modified_at"], Time); + } + + convertValues(a: any, classs: any, asMap: boolean = false): any { + if (!a) { + return a; + } + if (Array.isArray(a)) { + return (a as any[]).map(elem => this.convertValues(elem, classs)); + } else if ("object" === typeof a) { + if (asMap) { + for (const key of Object.keys(a)) { + a[key] = new classs(a[key]); + } + return a; + } + return new classs(a); + } + return a; + } +} +export class ModelsResponse { + models: Model[]; + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.models = this.convertValues(source["models"], Model); + } + + convertValues(a: any, classs: any, asMap: boolean = false): any { + if (!a) { + return a; + } + if (Array.isArray(a)) { + return (a as any[]).map(elem => this.convertValues(elem, classs)); + } else if ("object" === typeof a) { + if (asMap) { + for (const key of Object.keys(a)) { + a[key] = new classs(a[key]); + } + return a; + } + return new classs(a); + } + return a; + } +} +export class InferenceCompute { + library: string; + variant: string; + compute: string; + driver: string; + name: string; + vram: string; + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.library = source["library"]; + this.variant = source["variant"]; + this.compute = source["compute"]; + this.driver = source["driver"]; + this.name = source["name"]; + this.vram = source["vram"]; + } +} +export class InferenceComputeResponse { + inferenceComputes: InferenceCompute[]; + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.inferenceComputes = this.convertValues(source["inferenceComputes"], InferenceCompute); + } + + convertValues(a: any, classs: any, asMap: boolean = false): any { + if (!a) { + return a; + } + if (Array.isArray(a)) { + return (a as any[]).map(elem => this.convertValues(elem, classs)); + } else if ("object" === typeof a) { + if (asMap) { + for (const key of Object.keys(a)) { + a[key] = new classs(a[key]); + } + return a; + } + return new classs(a); + } + return a; + } +} +export class ModelCapabilitiesResponse { + capabilities: string[]; + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.capabilities = source["capabilities"]; + } +} +export class ChatEvent { + eventName: "chat" | "thinking" | "assistant_with_tools" | "tool_call" | "tool" | "tool_result" | "done" | "chat_created"; + content?: string; + thinking?: string; + thinkingTimeStart?: Date | undefined; + thinkingTimeEnd?: Date | undefined; + toolCalls?: ToolCall[]; + toolCall?: ToolCall; + toolName?: string; + toolResult?: boolean; + toolResultData?: any; + chatId?: string; + toolState?: any; + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.eventName = source["eventName"]; + this.content = source["content"]; + this.thinking = source["thinking"]; + this.thinkingTimeStart = source["thinkingTimeStart"] && new Date(source["thinkingTimeStart"]); + this.thinkingTimeEnd = source["thinkingTimeEnd"] && new Date(source["thinkingTimeEnd"]); + this.toolCalls = this.convertValues(source["toolCalls"], ToolCall); + this.toolCall = this.convertValues(source["toolCall"], ToolCall); + this.toolName = source["toolName"]; + this.toolResult = source["toolResult"]; + this.toolResultData = source["toolResultData"]; + this.chatId = source["chatId"]; + this.toolState = source["toolState"]; + } + + convertValues(a: any, classs: any, asMap: boolean = false): any { + if (!a) { + return a; + } + if (Array.isArray(a)) { + return (a as any[]).map(elem => this.convertValues(elem, classs)); + } else if ("object" === typeof a) { + if (asMap) { + for (const key of Object.keys(a)) { + a[key] = new classs(a[key]); + } + return a; + } + return new classs(a); + } + return a; + } +} +export class DownloadEvent { + eventName: "download"; + total: number; + completed: number; + done: boolean; + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.eventName = source["eventName"]; + this.total = source["total"]; + this.completed = source["completed"]; + this.done = source["done"]; + } +} +export class ErrorEvent { + eventName: "error"; + error: string; + code?: string; + details?: string; + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.eventName = source["eventName"]; + this.error = source["error"]; + this.code = source["code"]; + this.details = source["details"]; + } +} +export class Settings { + Expose: boolean; + Browser: boolean; + Survey: boolean; + Models: string; + Agent: boolean; + Tools: boolean; + WorkingDir: string; + ContextLength: number; + AirplaneMode: boolean; + TurboEnabled: boolean; + WebSearchEnabled: boolean; + ThinkEnabled: boolean; + ThinkLevel: string; + SelectedModel: string; + SidebarOpen: boolean; + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.Expose = source["Expose"]; + this.Browser = source["Browser"]; + this.Survey = source["Survey"]; + this.Models = source["Models"]; + this.Agent = source["Agent"]; + this.Tools = source["Tools"]; + this.WorkingDir = source["WorkingDir"]; + this.ContextLength = source["ContextLength"]; + this.AirplaneMode = source["AirplaneMode"]; + this.TurboEnabled = source["TurboEnabled"]; + this.WebSearchEnabled = source["WebSearchEnabled"]; + this.ThinkEnabled = source["ThinkEnabled"]; + this.ThinkLevel = source["ThinkLevel"]; + this.SelectedModel = source["SelectedModel"]; + this.SidebarOpen = source["SidebarOpen"]; + } +} +export class SettingsResponse { + settings: Settings; + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.settings = this.convertValues(source["settings"], Settings); + } + + convertValues(a: any, classs: any, asMap: boolean = false): any { + if (!a) { + return a; + } + if (Array.isArray(a)) { + return (a as any[]).map(elem => this.convertValues(elem, classs)); + } else if ("object" === typeof a) { + if (asMap) { + for (const key of Object.keys(a)) { + a[key] = new classs(a[key]); + } + return a; + } + return new classs(a); + } + return a; + } +} +export class HealthResponse { + healthy: boolean; + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.healthy = source["healthy"]; + } +} +export class User { + id: string; + name: string; + email: string; + avatarURL: string; + plan: string; + bio: string; + firstName: string; + lastName: string; + overThreshold: boolean; + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.id = source["id"]; + this.name = source["name"]; + this.email = source["email"]; + this.avatarURL = source["avatarURL"]; + this.plan = source["plan"]; + this.bio = source["bio"]; + this.firstName = source["firstName"]; + this.lastName = source["lastName"]; + this.overThreshold = source["overThreshold"]; + } +} +export class Attachment { + filename: string; + data?: string; + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.filename = source["filename"]; + this.data = source["data"]; + } +} +export class ChatRequest { + model: string; + prompt: string; + index?: number; + attachments?: Attachment[]; + web_search?: boolean; + file_tools?: boolean; + forceUpdate?: boolean; + think?: any; + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.model = source["model"]; + this.prompt = source["prompt"]; + this.index = source["index"]; + this.attachments = this.convertValues(source["attachments"], Attachment); + this.web_search = source["web_search"]; + this.file_tools = source["file_tools"]; + this.forceUpdate = source["forceUpdate"]; + this.think = source["think"]; + } + + convertValues(a: any, classs: any, asMap: boolean = false): any { + if (!a) { + return a; + } + if (Array.isArray(a)) { + return (a as any[]).map(elem => this.convertValues(elem, classs)); + } else if ("object" === typeof a) { + if (asMap) { + for (const key of Object.keys(a)) { + a[key] = new classs(a[key]); + } + return a; + } + return new classs(a); + } + return a; + } +} +export class Error { + error: string; + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.error = source["error"]; + } +} +export class ModelUpstreamResponse { + digest?: string; + pushTime: number; + error?: string; + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.digest = source["digest"]; + this.pushTime = source["pushTime"]; + this.error = source["error"]; + } +} +export class Page { + url: string; + title: string; + text: string; + lines: string[]; + links?: Record; + fetched_at: Time; + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.url = source["url"]; + this.title = source["title"]; + this.text = source["text"]; + this.lines = source["lines"]; + this.links = source["links"]; + this.fetched_at = this.convertValues(source["fetched_at"], Time); + } + + convertValues(a: any, classs: any, asMap: boolean = false): any { + if (!a) { + return a; + } + if (Array.isArray(a)) { + return (a as any[]).map(elem => this.convertValues(elem, classs)); + } else if ("object" === typeof a) { + if (asMap) { + for (const key of Object.keys(a)) { + a[key] = new classs(a[key]); + } + return a; + } + return new classs(a); + } + return a; + } +} +export class BrowserStateData { + page_stack: string[]; + view_tokens: number; + url_to_page: {[key: string]: Page}; + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.page_stack = source["page_stack"]; + this.view_tokens = source["view_tokens"]; + this.url_to_page = source["url_to_page"]; + } +} diff --git a/app/ui/app/eslint.config.js b/app/ui/app/eslint.config.js new file mode 100644 index 000000000..691c8a441 --- /dev/null +++ b/app/ui/app/eslint.config.js @@ -0,0 +1,32 @@ +// For more info, see https://github.com/storybookjs/eslint-plugin-storybook#configuration-flat-config-format +import storybook from "eslint-plugin-storybook"; + +import js from "@eslint/js"; +import globals from "globals"; +import reactHooks from "eslint-plugin-react-hooks"; +import reactRefresh from "eslint-plugin-react-refresh"; +import tseslint from "typescript-eslint"; + +export default tseslint.config( + { ignores: ["dist"] }, + { + extends: [js.configs.recommended, ...tseslint.configs.recommended], + files: ["**/*.{ts,tsx}"], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + plugins: { + "react-hooks": reactHooks, + "react-refresh": reactRefresh, + }, + rules: { + ...reactHooks.configs.recommended.rules, + "react-refresh/only-export-components": [ + "warn", + { allowConstantExport: true }, + ], + }, + }, + storybook.configs["flat/recommended"], +); diff --git a/app/ui/app/index.html b/app/ui/app/index.html new file mode 100644 index 000000000..9e74109ad --- /dev/null +++ b/app/ui/app/index.html @@ -0,0 +1,189 @@ + + + + + + + + Ollama + + +
+ + + + diff --git a/app/ui/app/package-lock.json b/app/ui/app/package-lock.json new file mode 100644 index 000000000..7877eeaef --- /dev/null +++ b/app/ui/app/package-lock.json @@ -0,0 +1,11876 @@ +{ + "name": "app", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "app", + "version": "0.0.0", + "dependencies": { + "@headlessui/react": "^2.2.4", + "@heroicons/react": "^2.2.0", + "@tanstack/react-query": "^5.80.7", + "@tanstack/react-router": "^1.120.20", + "@tanstack/react-router-devtools": "^1.120.20", + "clsx": "^2.1.1", + "framer-motion": "^12.17.0", + "katex": "^0.16.22", + "micromark-extension-llm-math": "^3.1.0", + "ollama": "^0.6.0", + "react": "^19.1.0", + "react-dom": "^19.1.0", + "rehype-katex": "^7.0.1", + "rehype-prism-plus": "^2.0.1", + "rehype-raw": "^7.0.0", + "rehype-sanitize": "^6.0.0", + "remark-math": "^6.0.0", + "unist-builder": "^4.0.0", + "unist-util-parents": "^3.0.0" + }, + "devDependencies": { + "@chromatic-com/storybook": "^4.0.1", + "@eslint/js": "^9.25.0", + "@storybook/addon-a11y": "^9.0.14", + "@storybook/addon-docs": "^9.0.14", + "@storybook/addon-onboarding": "^9.0.14", + "@storybook/addon-vitest": "^9.0.14", + "@storybook/react-vite": "^9.0.14", + "@tailwindcss/typography": "^0.5.16", + "@tailwindcss/vite": "^4.1.11", + "@tanstack/router-plugin": "^1.120.20", + "@types/node": "^24.7.2", + "@types/react": "^19.1.2", + "@types/react-dom": "^19.1.2", + "@vitejs/plugin-react": "^4.4.1", + "@vitest/browser": "^3.2.4", + "@vitest/coverage-v8": "^3.2.4", + "@vitest/ui": "^3.2.4", + "autoprefixer": "^10.4.21", + "eslint": "^9.25.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.19", + "eslint-plugin-storybook": "^9.0.14", + "globals": "^16.0.0", + "playwright": "^1.53.2", + "postcss-preset-env": "^10.2.4", + "react-markdown": "^10.1.0", + "remark": "^15.0.1", + "remark-gfm": "^4.0.1", + "remark-stringify": "^11.0.0", + "storybook": "^9.0.14", + "tailwindcss": "^4.1.9", + "typescript": "~5.8.3", + "typescript-eslint": "^8.30.1", + "vite": "^6.3.5", + "vite-tsconfig-paths": "^5.1.4", + "vitest": "^3.2.4" + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.3.tgz", + "integrity": "sha512-VQKMkwriZbaOgVCby1UDY/LDk5fIjhQicCvVPFqfe+69fWaPWydbWJ3wRt59/YzIwda1I81loas3oCoHxnqvdA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.27.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.5.tgz", + "integrity": "sha512-KiRAp/VoJaWkkte84TvUd9qjdbZAdiqyvMxrGl1N6vzFogKmaLgoM3L1kgtLicp2HP5fBJS8JrZKLVIZGVJAVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.27.4", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.4.tgz", + "integrity": "sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.27.3", + "@babel/helpers": "^7.27.4", + "@babel/parser": "^7.27.4", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.27.4", + "@babel/types": "^7.27.3", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.27.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.5.tgz", + "integrity": "sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.27.5", + "@babel/types": "^7.27.3", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", + "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.6.tgz", + "integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.27.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.5.tgz", + "integrity": "sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", + "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", + "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-syntax-typescript": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", + "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", + "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-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, + "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/runtime": { + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz", + "integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.27.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.4.tgz", + "integrity": "sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.3", + "@babel/parser": "^7.27.4", + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.3", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/types": { + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.6.tgz", + "integrity": "sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@chromatic-com/storybook": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@chromatic-com/storybook/-/storybook-4.0.1.tgz", + "integrity": "sha512-GQXe5lyZl3yLewLJQyFXEpOp2h+mfN2bPrzYaOFNCJjO4Js9deKbRHTOSaiP2FRwZqDLdQwy2+SEGeXPZ94yYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@neoconfetti/react": "^1.0.0", + "chromatic": "^12.0.0", + "filesize": "^10.0.12", + "jsonfile": "^6.1.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=20.0.0", + "yarn": ">=1.22.18" + }, + "peerDependencies": { + "storybook": "^0.0.0-0 || ^9.0.0 || ^9.1.0-0" + } + }, + "node_modules/@csstools/cascade-layer-name-parser": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@csstools/cascade-layer-name-parser/-/cascade-layer-name-parser-2.0.5.tgz", + "integrity": "sha512-p1ko5eHgV+MgXFVa4STPKpvPxr6ReS8oS2jzTukjR74i5zJNyWO1ZM1m8YKBXnzDKWfBN1ztLYlHxbVemDD88A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.0.2.tgz", + "integrity": "sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.0.10.tgz", + "integrity": "sha512-TiJ5Ajr6WRd1r8HSiwJvZBiJOqtH86aHpUjq5aEKWHiII2Qfjqd/HCWKPOW8EP4vcspXbHnXrwIDlu5savQipg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.0.2", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/media-query-list-parser": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-4.0.3.tgz", + "integrity": "sha512-HAYH7d3TLRHDOUQK4mZKf9k9Ph/m8Akstg66ywKR4SFAigjs3yBiUeZtFxywiTm5moZMAp/5W/ZuFnNXXYLuuQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/postcss-cascade-layers": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@csstools/postcss-cascade-layers/-/postcss-cascade-layers-5.0.2.tgz", + "integrity": "sha512-nWBE08nhO8uWl6kSAeCx4im7QfVko3zLrtgWZY4/bP87zrSPpSyN/3W3TDqz1jJuH+kbKOHXg5rJnK+ZVYcFFg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/selector-specificity": "^5.0.0", + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-cascade-layers/node_modules/@csstools/selector-specificity": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-5.0.0.tgz", + "integrity": "sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss-selector-parser": "^7.0.0" + } + }, + "node_modules/@csstools/postcss-cascade-layers/node_modules/postcss-selector-parser": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", + "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@csstools/postcss-color-function": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/@csstools/postcss-color-function/-/postcss-color-function-4.0.10.tgz", + "integrity": "sha512-4dY0NBu7NVIpzxZRgh/Q/0GPSz/jLSw0i/u3LTUor0BkQcz/fNhN10mSWBDsL0p9nDb0Ky1PD6/dcGbhACuFTQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.0.10", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.1.0", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-color-mix-function": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@csstools/postcss-color-mix-function/-/postcss-color-mix-function-3.0.10.tgz", + "integrity": "sha512-P0lIbQW9I4ShE7uBgZRib/lMTf9XMjJkFl/d6w4EMNHu2qvQ6zljJGEcBkw/NsBtq/6q3WrmgxSS8kHtPMkK4Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.0.10", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.1.0", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-color-mix-variadic-function-arguments": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-color-mix-variadic-function-arguments/-/postcss-color-mix-variadic-function-arguments-1.0.0.tgz", + "integrity": "sha512-Z5WhouTyD74dPFPrVE7KydgNS9VvnjB8qcdes9ARpCOItb4jTnm7cHp4FhxCRUoyhabD0WVv43wbkJ4p8hLAlQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.0.10", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.1.0", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-content-alt-text": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@csstools/postcss-content-alt-text/-/postcss-content-alt-text-2.0.6.tgz", + "integrity": "sha512-eRjLbOjblXq+byyaedQRSrAejKGNAFued+LcbzT+LCL78fabxHkxYjBbxkroONxHHYu2qxhFK2dBStTLPG3jpQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.1.0", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-exponential-functions": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/@csstools/postcss-exponential-functions/-/postcss-exponential-functions-2.0.9.tgz", + "integrity": "sha512-abg2W/PI3HXwS/CZshSa79kNWNZHdJPMBXeZNyPQFbbj8sKO3jXxOt/wF7juJVjyDTc6JrvaUZYFcSBZBhaxjw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-calc": "^2.1.4", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-font-format-keywords": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-font-format-keywords/-/postcss-font-format-keywords-4.0.0.tgz", + "integrity": "sha512-usBzw9aCRDvchpok6C+4TXC57btc4bJtmKQWOHQxOVKen1ZfVqBUuCZ/wuqdX5GHsD0NRSr9XTP+5ID1ZZQBXw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/utilities": "^2.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-gamut-mapping": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@csstools/postcss-gamut-mapping/-/postcss-gamut-mapping-2.0.10.tgz", + "integrity": "sha512-QDGqhJlvFnDlaPAfCYPsnwVA6ze+8hhrwevYWlnUeSjkkZfBpcCO42SaUD8jiLlq7niouyLgvup5lh+f1qessg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.0.10", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-gradients-interpolation-method": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/@csstools/postcss-gradients-interpolation-method/-/postcss-gradients-interpolation-method-5.0.10.tgz", + "integrity": "sha512-HHPauB2k7Oits02tKFUeVFEU2ox/H3OQVrP3fSOKDxvloOikSal+3dzlyTZmYsb9FlY9p5EUpBtz0//XBmy+aw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.0.10", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.1.0", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-hwb-function": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/@csstools/postcss-hwb-function/-/postcss-hwb-function-4.0.10.tgz", + "integrity": "sha512-nOKKfp14SWcdEQ++S9/4TgRKchooLZL0TUFdun3nI4KPwCjETmhjta1QT4ICQcGVWQTvrsgMM/aLB5We+kMHhQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.0.10", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.1.0", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-ic-unit": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@csstools/postcss-ic-unit/-/postcss-ic-unit-4.0.2.tgz", + "integrity": "sha512-lrK2jjyZwh7DbxaNnIUjkeDmU8Y6KyzRBk91ZkI5h8nb1ykEfZrtIVArdIjX4DHMIBGpdHrgP0n4qXDr7OHaKA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/postcss-progressive-custom-properties": "^4.1.0", + "@csstools/utilities": "^2.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-initial": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-initial/-/postcss-initial-2.0.1.tgz", + "integrity": "sha512-L1wLVMSAZ4wovznquK0xmC7QSctzO4D0Is590bxpGqhqjboLXYA16dWZpfwImkdOgACdQ9PqXsuRroW6qPlEsg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-is-pseudo-class": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@csstools/postcss-is-pseudo-class/-/postcss-is-pseudo-class-5.0.3.tgz", + "integrity": "sha512-jS/TY4SpG4gszAtIg7Qnf3AS2pjcUM5SzxpApOrlndMeGhIbaTzWBzzP/IApXoNWEW7OhcjkRT48jnAUIFXhAQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/selector-specificity": "^5.0.0", + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-is-pseudo-class/node_modules/@csstools/selector-specificity": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-5.0.0.tgz", + "integrity": "sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss-selector-parser": "^7.0.0" + } + }, + "node_modules/@csstools/postcss-is-pseudo-class/node_modules/postcss-selector-parser": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", + "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@csstools/postcss-light-dark-function": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/@csstools/postcss-light-dark-function/-/postcss-light-dark-function-2.0.9.tgz", + "integrity": "sha512-1tCZH5bla0EAkFAI2r0H33CDnIBeLUaJh1p+hvvsylJ4svsv2wOmJjJn+OXwUZLXef37GYbRIVKX+X+g6m+3CQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.1.0", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-logical-float-and-clear": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-float-and-clear/-/postcss-logical-float-and-clear-3.0.0.tgz", + "integrity": "sha512-SEmaHMszwakI2rqKRJgE+8rpotFfne1ZS6bZqBoQIicFyV+xT1UF42eORPxJkVJVrH9C0ctUgwMSn3BLOIZldQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-logical-overflow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-overflow/-/postcss-logical-overflow-2.0.0.tgz", + "integrity": "sha512-spzR1MInxPuXKEX2csMamshR4LRaSZ3UXVaRGjeQxl70ySxOhMpP2252RAFsg8QyyBXBzuVOOdx1+bVO5bPIzA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-logical-overscroll-behavior": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-overscroll-behavior/-/postcss-logical-overscroll-behavior-2.0.0.tgz", + "integrity": "sha512-e/webMjoGOSYfqLunyzByZj5KKe5oyVg/YSbie99VEaSDE2kimFm0q1f6t/6Jo+VVCQ/jbe2Xy+uX+C4xzWs4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-logical-resize": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-resize/-/postcss-logical-resize-3.0.0.tgz", + "integrity": "sha512-DFbHQOFW/+I+MY4Ycd/QN6Dg4Hcbb50elIJCfnwkRTCX05G11SwViI5BbBlg9iHRl4ytB7pmY5ieAFk3ws7yyg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-logical-viewport-units": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-viewport-units/-/postcss-logical-viewport-units-3.0.4.tgz", + "integrity": "sha512-q+eHV1haXA4w9xBwZLKjVKAWn3W2CMqmpNpZUk5kRprvSiBEGMgrNH3/sJZ8UA3JgyHaOt3jwT9uFa4wLX4EqQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-media-minmax": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/@csstools/postcss-media-minmax/-/postcss-media-minmax-2.0.9.tgz", + "integrity": "sha512-af9Qw3uS3JhYLnCbqtZ9crTvvkR+0Se+bBqSr7ykAnl9yKhk6895z9rf+2F4dClIDJWxgn0iZZ1PSdkhrbs2ig==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.4", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/media-query-list-parser": "^4.0.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-media-queries-aspect-ratio-number-values": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/postcss-media-queries-aspect-ratio-number-values/-/postcss-media-queries-aspect-ratio-number-values-3.0.5.tgz", + "integrity": "sha512-zhAe31xaaXOY2Px8IYfoVTB3wglbJUVigGphFLj6exb7cjZRH9A6adyE22XfFK3P2PzwRk0VDeTJmaxpluyrDg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/media-query-list-parser": "^4.0.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-nested-calc": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-nested-calc/-/postcss-nested-calc-4.0.0.tgz", + "integrity": "sha512-jMYDdqrQQxE7k9+KjstC3NbsmC063n1FTPLCgCRS2/qHUbHM0mNy9pIn4QIiQGs9I/Bg98vMqw7mJXBxa0N88A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/utilities": "^2.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-normalize-display-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-normalize-display-values/-/postcss-normalize-display-values-4.0.0.tgz", + "integrity": "sha512-HlEoG0IDRoHXzXnkV4in47dzsxdsjdz6+j7MLjaACABX2NfvjFS6XVAnpaDyGesz9gK2SC7MbNwdCHusObKJ9Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-oklab-function": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/@csstools/postcss-oklab-function/-/postcss-oklab-function-4.0.10.tgz", + "integrity": "sha512-ZzZUTDd0fgNdhv8UUjGCtObPD8LYxMH+MJsW9xlZaWTV8Ppr4PtxlHYNMmF4vVWGl0T6f8tyWAKjoI6vePSgAg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.0.10", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.1.0", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-progressive-custom-properties": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-progressive-custom-properties/-/postcss-progressive-custom-properties-4.1.0.tgz", + "integrity": "sha512-YrkI9dx8U4R8Sz2EJaoeD9fI7s7kmeEBfmO+UURNeL6lQI7VxF6sBE+rSqdCBn4onwqmxFdBU3lTwyYb/lCmxA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-random-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-random-function/-/postcss-random-function-2.0.1.tgz", + "integrity": "sha512-q+FQaNiRBhnoSNo+GzqGOIBKoHQ43lYz0ICrV+UudfWnEF6ksS6DsBIJSISKQT2Bvu3g4k6r7t0zYrk5pDlo8w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-calc": "^2.1.4", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-relative-color-syntax": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@csstools/postcss-relative-color-syntax/-/postcss-relative-color-syntax-3.0.10.tgz", + "integrity": "sha512-8+0kQbQGg9yYG8hv0dtEpOMLwB9M+P7PhacgIzVzJpixxV4Eq9AUQtQw8adMmAJU1RBBmIlpmtmm3XTRd/T00g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.0.10", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.1.0", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-scope-pseudo-class": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-scope-pseudo-class/-/postcss-scope-pseudo-class-4.0.1.tgz", + "integrity": "sha512-IMi9FwtH6LMNuLea1bjVMQAsUhFxJnyLSgOp/cpv5hrzWmrUYU5fm0EguNDIIOHUqzXode8F/1qkC/tEo/qN8Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-scope-pseudo-class/node_modules/postcss-selector-parser": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", + "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@csstools/postcss-sign-functions": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@csstools/postcss-sign-functions/-/postcss-sign-functions-1.1.4.tgz", + "integrity": "sha512-P97h1XqRPcfcJndFdG95Gv/6ZzxUBBISem0IDqPZ7WMvc/wlO+yU0c5D/OCpZ5TJoTt63Ok3knGk64N+o6L2Pg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-calc": "^2.1.4", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-stepped-value-functions": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@csstools/postcss-stepped-value-functions/-/postcss-stepped-value-functions-4.0.9.tgz", + "integrity": "sha512-h9btycWrsex4dNLeQfyU3y3w40LMQooJWFMm/SK9lrKguHDcFl4VMkncKKoXi2z5rM9YGWbUQABI8BT2UydIcA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-calc": "^2.1.4", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-text-decoration-shorthand": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@csstools/postcss-text-decoration-shorthand/-/postcss-text-decoration-shorthand-4.0.2.tgz", + "integrity": "sha512-8XvCRrFNseBSAGxeaVTaNijAu+FzUvjwFXtcrynmazGb/9WUdsPCpBX+mHEHShVRq47Gy4peYAoxYs8ltUnmzA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/color-helpers": "^5.0.2", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-trigonometric-functions": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@csstools/postcss-trigonometric-functions/-/postcss-trigonometric-functions-4.0.9.tgz", + "integrity": "sha512-Hnh5zJUdpNrJqK9v1/E3BbrQhaDTj5YiX7P61TOvUhoDHnUmsNNxcDAgkQ32RrcWx9GVUvfUNPcUkn8R3vIX6A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-calc": "^2.1.4", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-unset-value": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-unset-value/-/postcss-unset-value-4.0.0.tgz", + "integrity": "sha512-cBz3tOCI5Fw6NIFEwU3RiwK6mn3nKegjpJuzCndoGq3BZPkUjnsq7uQmIeMNeMbMk7YD2MfKcgCpZwX5jyXqCA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/utilities": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@csstools/utilities/-/utilities-2.0.0.tgz", + "integrity": "sha512-5VdOr0Z71u+Yp3ozOx8T11N703wIFGVRgOWbOZMKgglPJsWA54MRIoMNVMa7shUToIhx5J8vX4sOZgD2XiihiQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz", + "integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz", + "integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz", + "integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz", + "integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz", + "integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz", + "integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz", + "integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz", + "integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz", + "integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz", + "integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz", + "integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz", + "integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz", + "integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz", + "integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz", + "integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz", + "integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz", + "integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz", + "integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz", + "integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz", + "integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz", + "integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz", + "integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz", + "integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz", + "integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz", + "integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz", + "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.2.tgz", + "integrity": "sha512-+GPzk8PlG0sPpzdU5ZvIRMPidzAnZDl/s9L+y13iodqvb8leL53bTannOrQ/Im7UkpsmFU5Ily5U60LWixnmLg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz", + "integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "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.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.28.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.28.0.tgz", + "integrity": "sha512-fnqSjGWd/CoIp4EXIxWVK/sHA6DOHN4+8Ix2cX5ycOY7LG0UY8nHCU5pIp2eaE1Mc7Qd8kHspYNzYXT2ojPLzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.3.tgz", + "integrity": "sha512-1+WqvgNMhmlAambTvT3KPtCl/Ibr68VldY2XY40SL1CE0ZXiakFR/cbTspaF5HsnpDMvcYYoJHfl4980NBjGag==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.15.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit/node_modules/@eslint/core": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", + "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.1.tgz", + "integrity": "sha512-azI0DrjMMfIug/ExbBaeDVJXcY0a7EPvPjb2xAJPa4HeimBX+Z18HK8QQR3jb6356SnDDdxx+hinMLcJEDdOjw==", + "dependencies": { + "@floating-ui/utils": "^0.2.9" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.1.tgz", + "integrity": "sha512-cwsmW/zyw5ltYTUeeYJ60CnQuPqmGwuGVhG9w0PRaRKkAyi38BT5CKrpIbb+jtahSwUl04cWzSx9ZOIxeS6RsQ==", + "dependencies": { + "@floating-ui/core": "^1.7.1", + "@floating-ui/utils": "^0.2.9" + } + }, + "node_modules/@floating-ui/react": { + "version": "0.26.28", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.28.tgz", + "integrity": "sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw==", + "dependencies": { + "@floating-ui/react-dom": "^2.1.2", + "@floating-ui/utils": "^0.2.8", + "tabbable": "^6.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.3.tgz", + "integrity": "sha512-huMBfiU9UnQ2oBwIhgzyIiSpVgvlDstU8CX0AF+wS+KzmYMs0J2a3GwuFHV1Lz+jlrQGeC1fF+Nv0QoumyV0bA==", + "dependencies": { + "@floating-ui/dom": "^1.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz", + "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==" + }, + "node_modules/@headlessui/react": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-2.2.4.tgz", + "integrity": "sha512-lz+OGcAH1dK93rgSMzXmm1qKOJkBUqZf1L4M8TWLNplftQD3IkoEDdUFNfAn4ylsN6WOTVtWaLmvmaHOUk1dTA==", + "dependencies": { + "@floating-ui/react": "^0.26.16", + "@react-aria/focus": "^3.20.2", + "@react-aria/interactions": "^3.25.0", + "@tanstack/react-virtual": "^3.13.9", + "use-sync-external-store": "^1.5.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": "^18 || ^19 || ^19.0.0-rc", + "react-dom": "^18 || ^19 || ^19.0.0-rc" + } + }, + "node_modules/@heroicons/react": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@heroicons/react/-/react-2.2.0.tgz", + "integrity": "sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ==", + "license": "MIT", + "peerDependencies": { + "react": ">= 16 || ^19.0.0-rc" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@joshwooding/vite-plugin-react-docgen-typescript": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@joshwooding/vite-plugin-react-docgen-typescript/-/vite-plugin-react-docgen-typescript-0.6.0.tgz", + "integrity": "sha512-dPo6SE4dm8UKcgGg4LsV9iw6f5HkIeJwzMA2M2Lb+mhl5vxesbDvb3ENTzNTkGnOxS6PqJig2pfXdtYaW3S9fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob": "^10.0.0", + "magic-string": "^0.30.0", + "react-docgen-typescript": "^2.2.2" + }, + "peerDependencies": { + "typescript": ">= 4.3.x", + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mdx-js/react": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.1.0.tgz", + "integrity": "sha512-QjHtSaoameoalGnKDT3FoIl4+9RwyTmo9ZJGBdLOks/YOiWHoRDI3PUwEzOE7kEmGcV3AFcp9K6dYu9rEuKLAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdx": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=16", + "react": ">=16" + } + }, + "node_modules/@neoconfetti/react": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@neoconfetti/react/-/react-1.0.0.tgz", + "integrity": "sha512-klcSooChXXOzIm+SE5IISIAn3bYzYfPjbX7D7HoqZL84oAfgREeSg5vSIaSFH+DaGzzvImTyWe1OyrJ67vik4A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true, + "license": "MIT" + }, + "node_modules/@react-aria/focus": { + "version": "3.20.5", + "resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.20.5.tgz", + "integrity": "sha512-JpFtXmWQ0Oca7FcvkqgjSyo6xEP7v3oQOLUId6o0xTvm4AD5W0mU2r3lYrbhsJ+XxdUUX4AVR5473sZZ85kU4A==", + "dependencies": { + "@react-aria/interactions": "^3.25.3", + "@react-aria/utils": "^3.29.1", + "@react-types/shared": "^3.30.0", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/interactions": { + "version": "3.25.3", + "resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.25.3.tgz", + "integrity": "sha512-J1bhlrNtjPS/fe5uJQ+0c7/jiXniwa4RQlP+Emjfc/iuqpW2RhbF9ou5vROcLzWIyaW8tVMZ468J68rAs/aZ5A==", + "dependencies": { + "@react-aria/ssr": "^3.9.9", + "@react-aria/utils": "^3.29.1", + "@react-stately/flags": "^3.1.2", + "@react-types/shared": "^3.30.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/ssr": { + "version": "3.9.9", + "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.9.tgz", + "integrity": "sha512-2P5thfjfPy/np18e5wD4WPt8ydNXhij1jwA8oehxZTFqlgVMGXzcWKxTb4RtJrLFsqPO7RUQTiY8QJk0M4Vy2g==", + "dependencies": { + "@swc/helpers": "^0.5.0" + }, + "engines": { + "node": ">= 12" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/utils": { + "version": "3.29.1", + "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.29.1.tgz", + "integrity": "sha512-yXMFVJ73rbQ/yYE/49n5Uidjw7kh192WNN9PNQGV0Xoc7EJUlSOxqhnpHmYTyO0EotJ8fdM1fMH8durHjUSI8g==", + "dependencies": { + "@react-aria/ssr": "^3.9.9", + "@react-stately/flags": "^3.1.2", + "@react-stately/utils": "^3.10.7", + "@react-types/shared": "^3.30.0", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/flags": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@react-stately/flags/-/flags-3.1.2.tgz", + "integrity": "sha512-2HjFcZx1MyQXoPqcBGALwWWmgFVUk2TuKVIQxCbRq7fPyWXIl6VHcakCLurdtYC2Iks7zizvz0Idv48MQ38DWg==", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@react-stately/utils": { + "version": "3.10.7", + "resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.10.7.tgz", + "integrity": "sha512-cWvjGAocvy4abO9zbr6PW6taHgF24Mwy/LbQ4TC4Aq3tKdKDntxyD+sh7AkSRfJRT2ccMVaHVv2+FfHThd3PKQ==", + "dependencies": { + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/shared": { + "version": "3.30.0", + "resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.30.0.tgz", + "integrity": "sha512-COIazDAx1ncDg046cTJ8SFYsX8aS3lB/08LDnbkH/SkdYrFPWDlXMrO/sUam8j1WWM+PJ+4d1mj7tODIKNiFog==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.9", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.9.tgz", + "integrity": "sha512-e9MeMtVWo186sgvFFJOPGy7/d2j2mZhLJIdVW0C/xDluuOvymEATqz6zKsP0ZmXGzQtqlyjz5sC1sYQUoJG98w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/pluginutils": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.2.0.tgz", + "integrity": "sha512-qWJ2ZTbmumwiLFomfzTyt5Kng4hwPi9rwCYN4SHb6eaRU1KNO4ccxINHr/VhH4GgPlt1XfSTLX2LBTme8ne4Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.42.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.42.0.tgz", + "integrity": "sha512-gldmAyS9hpj+H6LpRNlcjQWbuKUtb94lodB9uCz71Jm+7BxK1VIOo7y62tZZwxhA7j1ylv/yQz080L5WkS+LoQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.42.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.42.0.tgz", + "integrity": "sha512-bpRipfTgmGFdCZDFLRvIkSNO1/3RGS74aWkJJTFJBH7h3MRV4UijkaEUeOMbi9wxtxYmtAbVcnMtHTPBhLEkaw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.42.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.42.0.tgz", + "integrity": "sha512-JxHtA081izPBVCHLKnl6GEA0w3920mlJPLh89NojpU2GsBSB6ypu4erFg/Wx1qbpUbepn0jY4dVWMGZM8gplgA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.42.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.42.0.tgz", + "integrity": "sha512-rv5UZaWVIJTDMyQ3dCEK+m0SAn6G7H3PRc2AZmExvbDvtaDc+qXkei0knQWcI3+c9tEs7iL/4I4pTQoPbNL2SA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.42.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.42.0.tgz", + "integrity": "sha512-fJcN4uSGPWdpVmvLuMtALUFwCHgb2XiQjuECkHT3lWLZhSQ3MBQ9pq+WoWeJq2PrNxr9rPM1Qx+IjyGj8/c6zQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.42.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.42.0.tgz", + "integrity": "sha512-CziHfyzpp8hJpCVE/ZdTizw58gr+m7Y2Xq5VOuCSrZR++th2xWAz4Nqk52MoIIrV3JHtVBhbBsJcAxs6NammOQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.42.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.42.0.tgz", + "integrity": "sha512-UsQD5fyLWm2Fe5CDM7VPYAo+UC7+2Px4Y+N3AcPh/LdZu23YcuGPegQly++XEVaC8XUTFVPscl5y5Cl1twEI4A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.42.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.42.0.tgz", + "integrity": "sha512-/i8NIrlgc/+4n1lnoWl1zgH7Uo0XK5xK3EDqVTf38KvyYgCU/Rm04+o1VvvzJZnVS5/cWSd07owkzcVasgfIkQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.42.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.42.0.tgz", + "integrity": "sha512-eoujJFOvoIBjZEi9hJnXAbWg+Vo1Ov8n/0IKZZcPZ7JhBzxh2A+2NFyeMZIRkY9iwBvSjloKgcvnjTbGKHE44Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.42.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.42.0.tgz", + "integrity": "sha512-/3NrcOWFSR7RQUQIuZQChLND36aTU9IYE4j+TB40VU78S+RA0IiqHR30oSh6P1S9f9/wVOenHQnacs/Byb824g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.42.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.42.0.tgz", + "integrity": "sha512-O8AplvIeavK5ABmZlKBq9/STdZlnQo7Sle0LLhVA7QT+CiGpNVe197/t8Aph9bhJqbDVGCHpY2i7QyfEDDStDg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.42.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.42.0.tgz", + "integrity": "sha512-6Qb66tbKVN7VyQrekhEzbHRxXXFFD8QKiFAwX5v9Xt6FiJ3BnCVBuyBxa2fkFGqxOCSGGYNejxd8ht+q5SnmtA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.42.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.42.0.tgz", + "integrity": "sha512-KQETDSEBamQFvg/d8jajtRwLNBlGc3aKpaGiP/LvEbnmVUKlFta1vqJqTrvPtsYsfbE/DLg5CC9zyXRX3fnBiA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.42.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.42.0.tgz", + "integrity": "sha512-qMvnyjcU37sCo/tuC+JqeDKSuukGAd+pVlRl/oyDbkvPJ3awk6G6ua7tyum02O3lI+fio+eM5wsVd66X0jQtxw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.42.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.42.0.tgz", + "integrity": "sha512-I2Y1ZUgTgU2RLddUHXTIgyrdOwljjkmcZ/VilvaEumtS3Fkuhbw4p4hgHc39Ypwvo2o7sBFNl2MquNvGCa55Iw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.42.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.42.0.tgz", + "integrity": "sha512-Gfm6cV6mj3hCUY8TqWa63DB8Mx3NADoFwiJrMpoZ1uESbK8FQV3LXkhfry+8bOniq9pqY1OdsjFWNsSbfjPugw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.42.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.42.0.tgz", + "integrity": "sha512-g86PF8YZ9GRqkdi0VoGlcDUb4rYtQKyTD1IVtxxN4Hpe7YqLBShA7oHMKU6oKTCi3uxwW4VkIGnOaH/El8de3w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.42.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.42.0.tgz", + "integrity": "sha512-+axkdyDGSp6hjyzQ5m1pgcvQScfHnMCcsXkx8pTgy/6qBmWVhtRVlgxjWwDp67wEXXUr0x+vD6tp5W4x6V7u1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.42.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.42.0.tgz", + "integrity": "sha512-F+5J9pelstXKwRSDq92J0TEBXn2nfUrQGg+HK1+Tk7VOL09e0gBqUHugZv7SW4MGrYj41oNCUe3IKCDGVlis2g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.42.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.42.0.tgz", + "integrity": "sha512-LpHiJRwkaVz/LqjHjK8LCi8osq7elmpwujwbXKNW88bM8eeGxavJIKKjkjpMHAh/2xfnrt1ZSnhTv41WYUHYmA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@storybook/addon-a11y": { + "version": "9.0.14", + "resolved": "https://registry.npmjs.org/@storybook/addon-a11y/-/addon-a11y-9.0.14.tgz", + "integrity": "sha512-xDtzD89lyyq706yynJ8iAUjBfNebb7F5OoJXSAPYPnUiHoNHAcRT9ia2HrC6Yjp3f3JX2PRIEMjD5Myz3sL04A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@storybook/global": "^5.0.0", + "axe-core": "^4.2.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^9.0.14" + } + }, + "node_modules/@storybook/addon-docs": { + "version": "9.0.14", + "resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-9.0.14.tgz", + "integrity": "sha512-vjWH2FamLzoPZXitecbhRSUvQDj27q/dDaCKXSwCIwEVziIQrqHBGDmuJPCWoroCkKxLo8s8gwMi6wk5Minaqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@mdx-js/react": "^3.0.0", + "@storybook/csf-plugin": "9.0.14", + "@storybook/icons": "^1.2.12", + "@storybook/react-dom-shim": "9.0.14", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "ts-dedent": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^9.0.14" + } + }, + "node_modules/@storybook/addon-onboarding": { + "version": "9.0.14", + "resolved": "https://registry.npmjs.org/@storybook/addon-onboarding/-/addon-onboarding-9.0.14.tgz", + "integrity": "sha512-oJdRbOp8OkmDit8KpvkcOccN7Xczqznz55WH2oxaSLg0pWqn493BQGaRvq7Tn8qxTomNg9ibqr0rsHRZQKGlbQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^9.0.14" + } + }, + "node_modules/@storybook/addon-vitest": { + "version": "9.0.14", + "resolved": "https://registry.npmjs.org/@storybook/addon-vitest/-/addon-vitest-9.0.14.tgz", + "integrity": "sha512-pWovuulbQiLCf/hT1FiBnEvH3x+yfi6iYtJt7SMY+WF8LllKQskJXOPNW5mZ+OF6YnJiaI9SHncFVur4B2y+LA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@storybook/global": "^5.0.0", + "@storybook/icons": "^1.4.0", + "prompts": "^2.4.0", + "ts-dedent": "^2.2.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "@vitest/browser": "^3.0.0", + "@vitest/runner": "^3.0.0", + "storybook": "^9.0.14", + "vitest": "^3.0.0" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + }, + "@vitest/runner": { + "optional": true + }, + "vitest": { + "optional": true + } + } + }, + "node_modules/@storybook/builder-vite": { + "version": "9.0.14", + "resolved": "https://registry.npmjs.org/@storybook/builder-vite/-/builder-vite-9.0.14.tgz", + "integrity": "sha512-pMe/RmiC98SMRNVDvfvISW/rEVbKwKLuLm3KilHSKkW1187S/BkxBQx/o61avAEnZR2AC+JgwWZC18PJGRH/pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@storybook/csf-plugin": "9.0.14", + "ts-dedent": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^9.0.14", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@storybook/csf-plugin": { + "version": "9.0.14", + "resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-9.0.14.tgz", + "integrity": "sha512-PKUmF5y/SfPOifC2bRo79YwfGv6TYISM5JK6r6FHVKMwV1nWLmj7Xx2t5aHa/5JggdBz/iGganTP7oo7QOn+0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "unplugin": "^1.3.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^9.0.14" + } + }, + "node_modules/@storybook/csf-plugin/node_modules/unplugin": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.16.1.tgz", + "integrity": "sha512-4/u/j4FrCKdi17jaxuJA0jClGxB1AvU2hw/IuayPc4ay1XGaJs/rbb4v5WKwAjNifjmXK9PIFyuPiaK8azyR9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.14.0", + "webpack-virtual-modules": "^0.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@storybook/global": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@storybook/global/-/global-5.0.0.tgz", + "integrity": "sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@storybook/icons": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@storybook/icons/-/icons-1.4.0.tgz", + "integrity": "sha512-Td73IeJxOyalzvjQL+JXx72jlIYHgs+REaHiREOqfpo3A2AYYG71AUbcv+lg7mEDIweKVCxsMQ0UKo634c8XeA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta" + } + }, + "node_modules/@storybook/react": { + "version": "9.0.14", + "resolved": "https://registry.npmjs.org/@storybook/react/-/react-9.0.14.tgz", + "integrity": "sha512-Ig4Y1xUOMcOWtQ/H73JZa4MeE0GJvYOcK16AhbfvPZMotdXCFyPbb1/pWhS209HuGwfNTVvWGz9rk7KrHmKsNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@storybook/global": "^5.0.0", + "@storybook/react-dom-shim": "9.0.14" + }, + "engines": { + "node": ">=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", + "storybook": "^9.0.14", + "typescript": ">= 4.9.x" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@storybook/react-dom-shim": { + "version": "9.0.14", + "resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-9.0.14.tgz", + "integrity": "sha512-fXMzhgFMnGZUhWm9zWiR8qOB90OykPhkB/qiebFbD/wUedPyp3H1+NAzX1/UWV2SYqr+aFK9vH1PokAYbpTRsw==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", + "storybook": "^9.0.14" + } + }, + "node_modules/@storybook/react-vite": { + "version": "9.0.14", + "resolved": "https://registry.npmjs.org/@storybook/react-vite/-/react-vite-9.0.14.tgz", + "integrity": "sha512-Qz231WFDcfRiB61P9zBv12GxX/V0CO0YiuIFNDoCNroVRAzGaBK8IYR2KKRd5V/1UGJl35YyyEIZUcA4Zt5xEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@joshwooding/vite-plugin-react-docgen-typescript": "0.6.0", + "@rollup/pluginutils": "^5.0.2", + "@storybook/builder-vite": "9.0.14", + "@storybook/react": "9.0.14", + "find-up": "^7.0.0", + "magic-string": "^0.30.0", + "react-docgen": "^8.0.0", + "resolve": "^1.22.8", + "tsconfig-paths": "^4.2.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta", + "storybook": "^9.0.14", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@storybook/react-vite/node_modules/find-up": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-7.0.0.tgz", + "integrity": "sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^7.2.0", + "path-exists": "^5.0.0", + "unicorn-magic": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@storybook/react-vite/node_modules/locate-path": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", + "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^6.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@storybook/react-vite/node_modules/p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@storybook/react-vite/node_modules/p-locate": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", + "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@storybook/react-vite/node_modules/path-exists": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", + "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/@storybook/react-vite/node_modules/yocto-queue": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.1.tgz", + "integrity": "sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@swc/helpers": { + "version": "0.5.17", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz", + "integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.11.tgz", + "integrity": "sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "enhanced-resolve": "^5.18.1", + "jiti": "^2.4.2", + "lightningcss": "1.30.1", + "magic-string": "^0.30.17", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.11" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.11.tgz", + "integrity": "sha512-Q69XzrtAhuyfHo+5/HMgr1lAiPP/G40OMFAnws7xcFEYqcypZmdW8eGXaOUIeOl1dzPJBPENXgbjsOyhg2nkrg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.4", + "tar": "^7.4.3" + }, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.11", + "@tailwindcss/oxide-darwin-arm64": "4.1.11", + "@tailwindcss/oxide-darwin-x64": "4.1.11", + "@tailwindcss/oxide-freebsd-x64": "4.1.11", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.11", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.11", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.11", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.11", + "@tailwindcss/oxide-linux-x64-musl": "4.1.11", + "@tailwindcss/oxide-wasm32-wasi": "4.1.11", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.11", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.11" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.11.tgz", + "integrity": "sha512-3IfFuATVRUMZZprEIx9OGDjG3Ou3jG4xQzNTvjDoKmU9JdmoCohQJ83MYd0GPnQIu89YoJqvMM0G3uqLRFtetg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.11.tgz", + "integrity": "sha512-ESgStEOEsyg8J5YcMb1xl8WFOXfeBmrhAwGsFxxB2CxY9evy63+AtpbDLAyRkJnxLy2WsD1qF13E97uQyP1lfQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.11.tgz", + "integrity": "sha512-EgnK8kRchgmgzG6jE10UQNaH9Mwi2n+yw1jWmof9Vyg2lpKNX2ioe7CJdf9M5f8V9uaQxInenZkOxnTVL3fhAw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.11.tgz", + "integrity": "sha512-xdqKtbpHs7pQhIKmqVpxStnY1skuNh4CtbcyOHeX1YBE0hArj2romsFGb6yUmzkq/6M24nkxDqU8GYrKrz+UcA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.11.tgz", + "integrity": "sha512-ryHQK2eyDYYMwB5wZL46uoxz2zzDZsFBwfjssgB7pzytAeCCa6glsiJGjhTEddq/4OsIjsLNMAiMlHNYnkEEeg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.11.tgz", + "integrity": "sha512-mYwqheq4BXF83j/w75ewkPJmPZIqqP1nhoghS9D57CLjsh3Nfq0m4ftTotRYtGnZd3eCztgbSPJ9QhfC91gDZQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.11.tgz", + "integrity": "sha512-m/NVRFNGlEHJrNVk3O6I9ggVuNjXHIPoD6bqay/pubtYC9QIdAMpS+cswZQPBLvVvEF6GtSNONbDkZrjWZXYNQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.11.tgz", + "integrity": "sha512-YW6sblI7xukSD2TdbbaeQVDysIm/UPJtObHJHKxDEcW2exAtY47j52f8jZXkqE1krdnkhCMGqP3dbniu1Te2Fg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.11.tgz", + "integrity": "sha512-e3C/RRhGunWYNC3aSF7exsQkdXzQ/M+aYuZHKnw4U7KQwTJotnWsGOIVih0s2qQzmEzOFIJ3+xt7iq67K/p56Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.11.tgz", + "integrity": "sha512-Xo1+/GU0JEN/C/dvcammKHzeM6NqKovG+6921MR6oadee5XPBaKOumrJCXvopJ/Qb5TH7LX/UAywbqrP4lax0g==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@emnapi/wasi-threads": "^1.0.2", + "@napi-rs/wasm-runtime": "^0.2.11", + "@tybys/wasm-util": "^0.9.0", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.11.tgz", + "integrity": "sha512-UgKYx5PwEKrac3GPNPf6HVMNhUIGuUh4wlDFR2jYYdkX6pL/rn73zTq/4pzUm8fOjAn5L8zDeHp9iXmUGOXZ+w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.11.tgz", + "integrity": "sha512-YfHoggn1j0LK7wR82TOucWc5LDCguHnoS879idHekmmiR7g9HUtMw9MI0NHatS28u/Xlkfi9w5RJWgz2Dl+5Qg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/typography": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.16.tgz", + "integrity": "sha512-0wDLwCVF5V3x3b1SGXPCDcdsbDHMBe+lkFzBRaHeLvNi+nrrnZ1lA18u+OTWO8iSWU2GxUOCvlXtDuqftc1oiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash.castarray": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.merge": "^4.6.2", + "postcss-selector-parser": "6.0.10" + }, + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.11.tgz", + "integrity": "sha512-RHYhrR3hku0MJFRV+fN2gNbDNEh3dwKvY8XJvTxCSXeMOsCRSr+uKvDWQcbizrHgjML6ZmTE5OwMrl5wKcujCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.1.11", + "@tailwindcss/oxide": "4.1.11", + "tailwindcss": "4.1.11" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7" + } + }, + "node_modules/@tanstack/history": { + "version": "1.120.17", + "resolved": "https://registry.npmjs.org/@tanstack/history/-/history-1.120.17.tgz", + "integrity": "sha512-k07LFI4Qo074IIaWzT/XjD0KlkGx2w1V3fnNtclKx0oAl8z4O9kCh6za+FPEIRe98xLgNFEiddDbJeAYGSlPtw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/query-core": { + "version": "5.80.7", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.80.7.tgz", + "integrity": "sha512-s09l5zeUKC8q7DCCCIkVSns8zZrK4ZDT6ryEjxNBFi68G4z2EBobBS7rdOY3r6W1WbUDpc1fe5oY+YO/+2UVUg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.80.7", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.80.7.tgz", + "integrity": "sha512-u2F0VK6+anItoEvB3+rfvTO9GEh2vb00Je05OwlUe/A0lkJBgW1HckiY3f9YZa+jx6IOe4dHPh10dyp9aY3iRQ==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.80.7" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@tanstack/react-router": { + "version": "1.120.20", + "resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.120.20.tgz", + "integrity": "sha512-+zNruUE9NsfGm9cHd22Xs7FRtBrBhDZe94pB69BEIjqjrEPZct6f5VhTV9WQ+bDZ6fRz8tUuxNFAgm/3Lm4AIg==", + "license": "MIT", + "dependencies": { + "@tanstack/history": "1.120.17", + "@tanstack/react-store": "^0.7.0", + "@tanstack/router-core": "1.120.19", + "jsesc": "^3.1.0", + "tiny-invariant": "^1.3.3", + "tiny-warning": "^1.0.3" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": ">=18.0.0 || >=19.0.0", + "react-dom": ">=18.0.0 || >=19.0.0" + } + }, + "node_modules/@tanstack/react-router-devtools": { + "version": "1.120.20", + "resolved": "https://registry.npmjs.org/@tanstack/react-router-devtools/-/react-router-devtools-1.120.20.tgz", + "integrity": "sha512-8wYUBdhaMQLo+f5GlJ31WK3T5gpQWetIG7bEGbhrgmd8Z6nZbUYfq10BtVnIwhJwiQa/39Fi9778/09N13l00A==", + "license": "MIT", + "dependencies": { + "@tanstack/router-devtools-core": "^1.120.19", + "solid-js": "^1.9.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@tanstack/react-router": "^1.120.20", + "react": ">=18.0.0 || >=19.0.0", + "react-dom": ">=18.0.0 || >=19.0.0" + } + }, + "node_modules/@tanstack/react-store": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@tanstack/react-store/-/react-store-0.7.1.tgz", + "integrity": "sha512-qUTEKdId6QPWGiWyKAPf/gkN29scEsz6EUSJ0C3HgLMgaqTAyBsQ2sMCfGVcqb+kkhEXAdjleCgH6LAPD6f2sA==", + "license": "MIT", + "dependencies": { + "@tanstack/store": "0.7.1", + "use-sync-external-store": "^1.5.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tanstack/react-virtual": { + "version": "3.13.10", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.10.tgz", + "integrity": "sha512-nvrzk4E9mWB4124YdJ7/yzwou7IfHxlSef6ugCFcBfRmsnsma3heciiiV97sBNxyc3VuwtZvmwXd0aB5BpucVw==", + "dependencies": { + "@tanstack/virtual-core": "3.13.10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tanstack/router-core": { + "version": "1.120.19", + "resolved": "https://registry.npmjs.org/@tanstack/router-core/-/router-core-1.120.19.tgz", + "integrity": "sha512-5JUVgkxnIM3NxMwzKt0tfz2UopZVxwq6Kl7Rp33zlFJaPjpiRs46VuRjVeAvkpJd6samo1gcH1rWqmnPUmtGcw==", + "license": "MIT", + "dependencies": { + "@tanstack/history": "1.120.17", + "@tanstack/store": "^0.7.0", + "tiny-invariant": "^1.3.3" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/router-devtools-core": { + "version": "1.120.19", + "resolved": "https://registry.npmjs.org/@tanstack/router-devtools-core/-/router-devtools-core-1.120.19.tgz", + "integrity": "sha512-B/8riYIxs5z+6BmkycfkllhZzRV0/jt8MEqlVHU/HDyAi+00luhi+4xwqQNxiAIUpFa6+0twmGPj4SI3SPA6nQ==", + "license": "MIT", + "dependencies": { + "clsx": "^2.1.1", + "goober": "^2.1.16" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@tanstack/router-core": "^1.120.19", + "csstype": "^3.0.10", + "solid-js": ">=1.9.5", + "tiny-invariant": "^1.3.3" + }, + "peerDependenciesMeta": { + "csstype": { + "optional": true + } + } + }, + "node_modules/@tanstack/router-generator": { + "version": "1.120.20", + "resolved": "https://registry.npmjs.org/@tanstack/router-generator/-/router-generator-1.120.20.tgz", + "integrity": "sha512-tv8uOjteyMnUUDepjknkiTQ90EL6sNuDGsfeUz0wXFQ6dy/cqYqV0pXzJuBRN79ToteWFdq5sJeypqUdzOC8+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tanstack/virtual-file-routes": "^1.120.17", + "prettier": "^3.5.0", + "tsx": "^4.19.2", + "zod": "^3.24.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@tanstack/react-router": "^1.120.20" + }, + "peerDependenciesMeta": { + "@tanstack/react-router": { + "optional": true + } + } + }, + "node_modules/@tanstack/router-plugin": { + "version": "1.120.20", + "resolved": "https://registry.npmjs.org/@tanstack/router-plugin/-/router-plugin-1.120.20.tgz", + "integrity": "sha512-GaDcIZSVaMoLvG6pu0yRLz8C8jtFo1avV23Q1UbZzv+1i66Uk0twcfTPy/eJbB8pgRA2P5Eu7xo7L3g5HvkGRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.26.8", + "@babel/plugin-syntax-jsx": "^7.25.9", + "@babel/plugin-syntax-typescript": "^7.25.9", + "@babel/template": "^7.26.8", + "@babel/traverse": "^7.26.8", + "@babel/types": "^7.26.8", + "@tanstack/router-core": "^1.120.19", + "@tanstack/router-generator": "^1.120.20", + "@tanstack/router-utils": "^1.120.17", + "@tanstack/virtual-file-routes": "^1.120.17", + "@types/babel__core": "^7.20.5", + "@types/babel__template": "^7.4.4", + "@types/babel__traverse": "^7.20.6", + "babel-dead-code-elimination": "^1.0.10", + "chokidar": "^3.6.0", + "unplugin": "^2.1.2", + "zod": "^3.24.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@rsbuild/core": ">=1.0.2", + "@tanstack/react-router": "^1.120.20", + "vite": ">=5.0.0 || >=6.0.0", + "vite-plugin-solid": "^2.11.2", + "webpack": ">=5.92.0" + }, + "peerDependenciesMeta": { + "@rsbuild/core": { + "optional": true + }, + "@tanstack/react-router": { + "optional": true + }, + "vite": { + "optional": true + }, + "vite-plugin-solid": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/@tanstack/router-utils": { + "version": "1.120.17", + "resolved": "https://registry.npmjs.org/@tanstack/router-utils/-/router-utils-1.120.17.tgz", + "integrity": "sha512-emgT4FthaGtTRaRg9bsr0uaq3EHdl/flS4bKLuFaetiFTt8wk8EVU2a7EZlkaaAfLLDPaiGbP1S2DDaZQ7ci+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/generator": "^7.26.8", + "@babel/parser": "^7.26.8", + "ansis": "^3.11.0", + "diff": "^7.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/store": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@tanstack/store/-/store-0.7.1.tgz", + "integrity": "sha512-PjUQKXEXhLYj2X5/6c1Xn/0/qKY0IVFxTJweopRfF26xfjVyb14yALydJrHupDh3/d+1WKmfEgZPBVCmDkzzwg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/virtual-core": { + "version": "3.13.10", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.10.tgz", + "integrity": "sha512-sPEDhXREou5HyZYqSWIqdU580rsF6FGeN7vpzijmP3KTiOGjOMZASz4Y6+QKjiFQwhWrR58OP8izYaNGVxvViA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/virtual-file-routes": { + "version": "1.120.17", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-file-routes/-/virtual-file-routes-1.120.17.tgz", + "integrity": "sha512-Ssi+yKcjG9ru02ieCpUBF7QQBEKGB7WQS1R9va3GHu+Oq9WjzmJ4rifzdugjTeKD3yfT7d1I+pOxRhoWog6CHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@testing-library/dom": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", + "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "chalk": "^4.1.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.6.3.tgz", + "integrity": "sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "chalk": "^3.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "lodash": "^4.17.21", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "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/user-event": { + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, + "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" + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz", + "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/chai": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.2.tgz", + "integrity": "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*" + } + }, + "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", + "dependencies": { + "@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/doctrine": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/@types/doctrine/-/doctrine-0.0.9.tgz", + "integrity": "sha512-eOIHzCUSH7SMfonMG1LsC2f8vxBFtho6NGBznK41R84YzPuvSBzrhEps33IsQiOW9+VL6NQ9DbjQJznk/S4uRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/katex": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@types/katex/-/katex-0.16.7.tgz", + "integrity": "sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==", + "license": "MIT" + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdx": { + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.13.tgz", + "integrity": "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.7.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.7.2.tgz", + "integrity": "sha512-/NbVmcGTP+lj5oa4yiYxxeBjRivKQ5Ns1eSZeB99ExsEQ6rX5XYU1Zy/gGxY/ilqtD4Etx9mKyrPxZRetiahhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.14.0" + } + }, + "node_modules/@types/prismjs": { + "version": "1.26.5", + "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.5.tgz", + "integrity": "sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==", + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.1.7", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.7.tgz", + "integrity": "sha512-BnsPLV43ddr05N71gaGzyZ5hzkCmGwhMvYc8zmvI8Ci1bRkkDSzDDVfAXfN2tk748OwI7ediiPX6PfT9p0QGVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.1.6", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.6.tgz", + "integrity": "sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.0.0" + } + }, + "node_modules/@types/resolve": { + "version": "1.20.6", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.6.tgz", + "integrity": "sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.34.0.tgz", + "integrity": "sha512-QXwAlHlbcAwNlEEMKQS2RCgJsgXrTJdjXT08xEgbPFa2yYQgVjBymxP5DrfrE7X7iodSzd9qBUHUycdyVJTW1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.34.0", + "@typescript-eslint/type-utils": "8.34.0", + "@typescript-eslint/utils": "8.34.0", + "@typescript-eslint/visitor-keys": "8.34.0", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.34.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.34.0.tgz", + "integrity": "sha512-vxXJV1hVFx3IXz/oy2sICsJukaBrtDEQSBiV48/YIV5KWjX1dO+bcIr/kCPrW6weKXvsaGKFNlwH0v2eYdRRbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.34.0", + "@typescript-eslint/types": "8.34.0", + "@typescript-eslint/typescript-estree": "8.34.0", + "@typescript-eslint/visitor-keys": "8.34.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.34.0.tgz", + "integrity": "sha512-iEgDALRf970/B2YExmtPMPF54NenZUf4xpL3wsCRx/lgjz6ul/l13R81ozP/ZNuXfnLCS+oPmG7JIxfdNYKELw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.34.0", + "@typescript-eslint/types": "^8.34.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.34.0.tgz", + "integrity": "sha512-9Ac0X8WiLykl0aj1oYQNcLZjHgBojT6cW68yAgZ19letYu+Hxd0rE0veI1XznSSst1X5lwnxhPbVdwjDRIomRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.34.0", + "@typescript-eslint/visitor-keys": "8.34.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.34.0.tgz", + "integrity": "sha512-+W9VYHKFIzA5cBeooqQxqNriAP0QeQ7xTiDuIOr71hzgffm3EL2hxwWBIIj4GuofIbKxGNarpKqIq6Q6YrShOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.34.0.tgz", + "integrity": "sha512-n7zSmOcUVhcRYC75W2pnPpbO1iwhJY3NLoHEtbJwJSNlVAZuwqu05zY3f3s2SDWWDSo9FdN5szqc73DCtDObAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "8.34.0", + "@typescript-eslint/utils": "8.34.0", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.34.0.tgz", + "integrity": "sha512-9V24k/paICYPniajHfJ4cuAWETnt7Ssy+R0Rbcqo5sSFr3QEZ/8TSoUi9XeXVBGXCaLtwTOKSLGcInCAvyZeMA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.34.0.tgz", + "integrity": "sha512-rOi4KZxI7E0+BMqG7emPSK1bB4RICCpF7QD3KCLXn9ZvWoESsOMlHyZPAHyG04ujVplPaHbmEvs34m+wjgtVtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.34.0", + "@typescript-eslint/tsconfig-utils": "8.34.0", + "@typescript-eslint/types": "8.34.0", + "@typescript-eslint/visitor-keys": "8.34.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.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==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "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==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.34.0.tgz", + "integrity": "sha512-8L4tWatGchV9A1cKbjaavS6mwYwp39jql8xUmIIKJdm+qiaeHy5KMKlBrf30akXAWBzn2SqKsNOtSENWUwg7XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.34.0", + "@typescript-eslint/types": "8.34.0", + "@typescript-eslint/typescript-estree": "8.34.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.34.0.tgz", + "integrity": "sha512-qHV7pW7E85A0x6qyrFn+O+q1k1p3tQCsqIZ1KZ5ESLXY57aTvUd3/a4rdPTeXisvhXn2VQG0VSKUqs8KHF2zcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.34.0", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-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/@vitejs/plugin-react": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.5.1.tgz", + "integrity": "sha512-uPZBqSI0YD4lpkIru6M35sIfylLGTyhGHvDZbNLuMA73lMlwJKz5xweH7FajfcCAc2HnINciejA9qTz0dr0M7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.26.10", + "@babel/plugin-transform-react-jsx-self": "^7.25.9", + "@babel/plugin-transform-react-jsx-source": "^7.25.9", + "@rolldown/pluginutils": "1.0.0-beta.9", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0" + } + }, + "node_modules/@vitest/browser": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-3.2.4.tgz", + "integrity": "sha512-tJxiPrWmzH8a+w9nLKlQMzAKX/7VjFs50MWgcAj7p9XQ7AQ9/35fByFYptgPELyLw+0aixTnC4pUWV+APcZ/kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@testing-library/dom": "^10.4.0", + "@testing-library/user-event": "^14.6.1", + "@vitest/mocker": "3.2.4", + "@vitest/utils": "3.2.4", + "magic-string": "^0.30.17", + "sirv": "^3.0.1", + "tinyrainbow": "^2.0.0", + "ws": "^8.18.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "playwright": "*", + "vitest": "3.2.4", + "webdriverio": "^7.0.0 || ^8.0.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "playwright": { + "optional": true + }, + "safaridriver": { + "optional": true + }, + "webdriverio": { + "optional": true + } + } + }, + "node_modules/@vitest/coverage-v8": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz", + "integrity": "sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@bcoe/v8-coverage": "^1.0.2", + "ast-v8-to-istanbul": "^0.3.3", + "debug": "^4.4.1", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.1.7", + "magic-string": "^0.30.17", + "magicast": "^0.3.5", + "std-env": "^3.9.0", + "test-exclude": "^7.0.1", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "3.2.4", + "vitest": "3.2.4" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/mocker/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/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/ui": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-3.2.4.tgz", + "integrity": "sha512-hGISOaP18plkzbWEcP/QvtRW1xDXF2+96HbEX6byqQhAUbiS5oH6/9JwW+QsQCIYON2bI6QZBF+2PvOmrRZ9wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.4", + "fflate": "^0.8.2", + "flatted": "^3.3.3", + "pathe": "^2.0.3", + "sirv": "^3.0.1", + "tinyglobby": "^0.2.14", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "vitest": "3.2.4" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "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, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "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, + "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/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", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ansis": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/ansis/-/ansis-3.17.0.tgz", + "integrity": "sha512-0qWUglt9JEqLFr3w1I1pbrChn1grhaiAR2ocX1PP/flRmxgtwTzPFFFnfIlD6aMOLQZgSuCRlidD70lvx8yhzg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "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/ast-types": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.16.1.tgz", + "integrity": "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/ast-v8-to-istanbul": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.3.tgz", + "integrity": "sha512-MuXMrSLVVoA6sYN/6Hke18vMzrT4TZNbZIj/hvh0fnYFpO+/kFXcLIaiPwXXWaQUPg4yJD8fj+lfJ7/1EBconw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "estree-walker": "^3.0.3", + "js-tokens": "^9.0.1" + } + }, + "node_modules/ast-v8-to-istanbul/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/ast-v8-to-istanbul/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/autoprefixer": { + "version": "10.4.21", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", + "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.24.4", + "caniuse-lite": "^1.0.30001702", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/axe-core": { + "version": "4.10.3", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.3.tgz", + "integrity": "sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/babel-dead-code-elimination": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/babel-dead-code-elimination/-/babel-dead-code-elimination-1.0.10.tgz", + "integrity": "sha512-DV5bdJZTzZ0zn0DC24v3jD7Mnidh6xhKa4GfKCbq3sfW8kaWhDdZjP3i81geA8T33tdYqWKw4D3fVv0CwEgKVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.23.7", + "@babel/parser": "^7.23.6", + "@babel/traverse": "^7.23.7", + "@babel/types": "^7.23.6" + } + }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/better-opn": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/better-opn/-/better-opn-3.0.2.tgz", + "integrity": "sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "open": "^8.0.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.0.tgz", + "integrity": "sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001718", + "electron-to-chromium": "^1.5.160", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001721", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001721.tgz", + "integrity": "sha512-cOuvmUVtKrtEaoKiO0rSc29jcjwMwX5tOHDy4MgVFEWiUXj4uBMJkwI8MDySkgXidpMiHUcviogAvFi4pA2hDQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chai": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz", + "integrity": "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/chromatic": { + "version": "12.2.0", + "resolved": "https://registry.npmjs.org/chromatic/-/chromatic-12.2.0.tgz", + "integrity": "sha512-GswmBW9ZptAoTns1BMyjbm55Z7EsIJnUvYKdQqXIBZIKbGErmpA+p4c0BYA+nzw5B0M+rb3Iqp1IaH8TFwIQew==", + "dev": true, + "license": "MIT", + "bin": { + "chroma": "dist/bin.js", + "chromatic": "dist/bin.js", + "chromatic-cli": "dist/bin.js" + }, + "peerDependencies": { + "@chromatic-com/cypress": "^0.*.* || ^1.0.0", + "@chromatic-com/playwright": "^0.*.* || ^1.0.0" + }, + "peerDependenciesMeta": { + "@chromatic-com/cypress": { + "optional": true + }, + "@chromatic-com/playwright": { + "optional": true + } + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "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/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/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-blank-pseudo": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-7.0.1.tgz", + "integrity": "sha512-jf+twWGDf6LDoXDUode+nc7ZlrqfaNphrBIBrcmeP3D8yw1uPaix1gCC8LUQUGQ6CycuK2opkbFFWFuq/a94ag==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/css-blank-pseudo/node_modules/postcss-selector-parser": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", + "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/css-has-pseudo": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/css-has-pseudo/-/css-has-pseudo-7.0.2.tgz", + "integrity": "sha512-nzol/h+E0bId46Kn2dQH5VElaknX2Sr0hFuB/1EomdC7j+OISt2ZzK7EHX9DZDY53WbIVAR7FYKSO2XnSf07MQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/selector-specificity": "^5.0.0", + "postcss-selector-parser": "^7.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/css-has-pseudo/node_modules/@csstools/selector-specificity": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-5.0.0.tgz", + "integrity": "sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss-selector-parser": "^7.0.0" + } + }, + "node_modules/css-has-pseudo/node_modules/postcss-selector-parser": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", + "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/css-prefers-color-scheme": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/css-prefers-color-scheme/-/css-prefers-color-scheme-10.0.0.tgz", + "integrity": "sha512-VCtXZAWivRglTZditUfB4StnsWr6YVZ2PRtuxQLKTNRdtAf8tpzaVPE9zXIF3VaSc7O70iK/j1+NXxyQCqdPjQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "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/cssdb": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-8.3.1.tgz", + "integrity": "sha512-XnDRQMXucLueX92yDe0LPKupXetWoFOgawr4O4X41l5TltgK2NVbJJVDnnOywDYfW1sTJ28AcXGKOqdRKwCcmQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + } + ], + "license": "MIT-0" + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decode-named-character-reference": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.1.0.tgz", + "integrity": "sha512-Wy+JTSbFThEOXQIR2L6mxJvEs+veIzpmqD7ynWxMXGpnk3smkHQOp6forLdHsKpAMW9iJpaBBIxz285t1n1C3w==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/diff": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", + "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "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" + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.166", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.166.tgz", + "integrity": "sha512-QPWqHL0BglzPYyJJ1zSSmwFFL6MFXhbACOCcsCdUMCkzPdS9/OIBVxg516X/Ado2qwAq8k0nJJ7phQPCqiaFAw==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/enhanced-resolve": { + "version": "5.18.2", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.2.tgz", + "integrity": "sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "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": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz", + "integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.5", + "@esbuild/android-arm": "0.25.5", + "@esbuild/android-arm64": "0.25.5", + "@esbuild/android-x64": "0.25.5", + "@esbuild/darwin-arm64": "0.25.5", + "@esbuild/darwin-x64": "0.25.5", + "@esbuild/freebsd-arm64": "0.25.5", + "@esbuild/freebsd-x64": "0.25.5", + "@esbuild/linux-arm": "0.25.5", + "@esbuild/linux-arm64": "0.25.5", + "@esbuild/linux-ia32": "0.25.5", + "@esbuild/linux-loong64": "0.25.5", + "@esbuild/linux-mips64el": "0.25.5", + "@esbuild/linux-ppc64": "0.25.5", + "@esbuild/linux-riscv64": "0.25.5", + "@esbuild/linux-s390x": "0.25.5", + "@esbuild/linux-x64": "0.25.5", + "@esbuild/netbsd-arm64": "0.25.5", + "@esbuild/netbsd-x64": "0.25.5", + "@esbuild/openbsd-arm64": "0.25.5", + "@esbuild/openbsd-x64": "0.25.5", + "@esbuild/sunos-x64": "0.25.5", + "@esbuild/win32-arm64": "0.25.5", + "@esbuild/win32-ia32": "0.25.5", + "@esbuild/win32-x64": "0.25.5" + } + }, + "node_modules/esbuild-register": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/esbuild-register/-/esbuild-register-3.6.0.tgz", + "integrity": "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "peerDependencies": { + "esbuild": ">=0.12 <1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.28.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.28.0.tgz", + "integrity": "sha512-ocgh41VhRlf9+fVpe7QKzwLj9c92fDiqOj8Y3Sd4/ZmVA4Btx4PlUYPq4pp9JDyupkf1upbEXecxL2mwNV7jPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.20.0", + "@eslint/config-helpers": "^0.2.1", + "@eslint/core": "^0.14.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.28.0", + "@eslint/plugin-kit": "^0.3.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.3.0", + "eslint-visitor-keys": "^4.2.0", + "espree": "^10.3.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.20", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.20.tgz", + "integrity": "sha512-XpbHQ2q5gUF8BGOX4dHe+71qoirYMhApEPZ7sfhF/dNnOF1UXnCMGZf79SFTBO7Bz5YEIT4TMieSlJBWhP9WBA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-plugin-storybook": { + "version": "9.0.14", + "resolved": "https://registry.npmjs.org/eslint-plugin-storybook/-/eslint-plugin-storybook-9.0.14.tgz", + "integrity": "sha512-YZsDhyFgVfeFPdvd7Xcl9ZusY7Jniq7AOAWN/cdg0a2Y+ywKKNYrQ+EfyuhXsiMjh58plYKMpJYxKVxeUwW9jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/utils": "^8.8.1" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "eslint": ">=8", + "storybook": "^9.0.14" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expect-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.1.tgz", + "integrity": "sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw==", + "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", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "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-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "dev": true, + "license": "MIT" + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/filesize": { + "version": "10.1.6", + "resolved": "https://registry.npmjs.org/filesize/-/filesize-10.1.6.tgz", + "integrity": "sha512-sJslQKU2uM33qH5nqewAwVB2QgR6w1aMNsYUp3aN5rMRyXEwJGmZvaWzeJFNTOXWlHQyBFCWrdj3fV/fsTOX8w==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 10.4.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "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==", + "dev": true, + "license": "ISC" + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/framer-motion": { + "version": "12.17.0", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.17.0.tgz", + "integrity": "sha512-2hISKgDk49yCLStwG1wf4Kdy/D6eBw9/eRNaWFIYoI9vMQ/Mqd1Fz+gzVlEtxJmtQ9y4IWnXm19/+UXD3dAYAA==", + "dependencies": { + "motion-dom": "^12.17.0", + "motion-utils": "^12.12.1", + "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", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-tsconfig": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz", + "integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globals": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.2.0.tgz", + "integrity": "sha512-O+7l9tPdHCU320IigZZPj5zmRCFG9xHmx9cU8FqU2Rp+JN714seHV+2S9+JslCpY4gJwU2vOGox0wzgae/MCEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globrex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", + "dev": true, + "license": "MIT" + }, + "node_modules/goober": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.16.tgz", + "integrity": "sha512-erjk19y1U33+XAMe1VTvIONHYoSqE4iS7BYUZfHaqeohLmnC0FdxEh7rQU+6MZ4OajItzjZFSRtVANrQwNq6/g==", + "license": "MIT", + "peerDependencies": { + "csstype": "^3.0.10" + } + }, + "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/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "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/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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-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" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "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", + "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-jsx-runtime": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-parse5": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-8.0.0.tgz", + "integrity": "sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "property-information": "^6.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-parse5/node_modules/property-information": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz", + "integrity": "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/hast-util-to-string": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/hast-util-to-string/-/hast-util-to-string-3.0.1.tgz", + "integrity": "sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.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", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "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/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/html-url-attributes": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", + "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "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.4", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.4.tgz", + "integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jiti": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", + "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "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, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/katex": { + "version": "0.16.22", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.22.tgz", + "integrity": "sha512-XCHRdUw4lf3SKBaJe4EvgqIuWwkPSo9XoeO8GjQW94Bp7TWv9hNhzZjZ+OH9yf1UmLygb7DIT5GSFQiyt16zYg==", + "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", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz", + "integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-darwin-arm64": "1.30.1", + "lightningcss-darwin-x64": "1.30.1", + "lightningcss-freebsd-x64": "1.30.1", + "lightningcss-linux-arm-gnueabihf": "1.30.1", + "lightningcss-linux-arm64-gnu": "1.30.1", + "lightningcss-linux-arm64-musl": "1.30.1", + "lightningcss-linux-x64-gnu": "1.30.1", + "lightningcss-linux-x64-musl": "1.30.1", + "lightningcss-win32-arm64-msvc": "1.30.1", + "lightningcss-win32-x64-msvc": "1.30.1" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz", + "integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz", + "integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz", + "integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz", + "integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz", + "integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz", + "integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz", + "integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz", + "integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz", + "integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz", + "integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.castarray": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz", + "integrity": "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/loupe": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.4.tgz", + "integrity": "sha512-wJzkKwJrheKtknCOKNEtDK4iqg/MxmZheEMtSTYvnzRdEYaZzmgH976nenp8WdJRdx5Vc1X/9MO0Oszl6ezeXg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "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", + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/magicast": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", + "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.4", + "@babel/types": "^7.25.4", + "source-map-js": "^1.2.0" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", + "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", + "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm/node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.0.tgz", + "integrity": "sha512-FyzMsduZZHSc3i0Px3PQcBT4WJY/X/RCtEJKuybiC6sjPqLv7h1yqAkmILZtuxMSsUyaLUWNp71+vQH2zqp5cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-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", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz", + "integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "dev": true, + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "dev": true, + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", + "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-llm-math": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-llm-math/-/micromark-extension-llm-math-3.1.0.tgz", + "integrity": "sha512-VIYHuIEk0gpHrojEtNGaxGwdpSLtdWYlLL2vu9PM4M1ilEtak10S8F9zzbNAPBNRoWFs/bjs+J7R3yUBoIQUEA==", + "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-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", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "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==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", + "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/motion-dom": { + "version": "12.17.0", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.17.0.tgz", + "integrity": "sha512-FA6/c70R9NKs3g41XDVONzmUUrEmyaifLVGCWtAmHP0usDnX9W+RN/tmbC4EUl0w6yLGvMTOwnWCFVgA5luhRg==", + "dependencies": { + "motion-utils": "^12.12.1" + } + }, + "node_modules/motion-utils": { + "version": "12.12.1", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.12.1.tgz", + "integrity": "sha512-f9qiqUHm7hWSLlNW8gS9pisnsN7CRFRD58vNjptKdsqFLpkVnX00TNeD6Q0d27V9KzT7ySFyK1TZ/DShfVOv6w==" + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "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==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ollama": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/ollama/-/ollama-0.6.0.tgz", + "integrity": "sha512-FHjdU2Ok5x2HZsxPui/MBJZ5J+HzmxoWYa/p9wk736eT+uAhS8nvIICar5YgwlG5MFNjDR6UA5F3RSKq+JseOA==", + "license": "MIT", + "dependencies": { + "whatwg-fetch": "^3.6.20" + } + }, + "node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/parse-numeric-range": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/parse-numeric-range/-/parse-numeric-range-1.3.0.tgz", + "integrity": "sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ==", + "license": "ISC" + }, + "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/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "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/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/playwright": { + "version": "1.53.2", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.53.2.tgz", + "integrity": "sha512-6K/qQxVFuVQhRQhFsVZ9fGeatxirtrpPgxzBYWyZLEXJzqYwuL4fuNmfOfD5et1tJE4GScKyPNeLhZeRwuTU3A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.53.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.53.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.53.2.tgz", + "integrity": "sha512-ox/OytMy+2w1jcYEYlOo1Hhp8hZkLCximMTUTMBXjGUA1KoFfiSZ+DU+3a739jsPY0yoKH2TFy9S2fsJas8yAw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/postcss": { + "version": "8.5.4", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.4.tgz", + "integrity": "sha512-QSa9EBe+uwlGTFmHsPKokv3B/oEMQZxfqW0QqNCyhpa6mB1afzulwn8hihglqAb2pOw+BJgNlmXQ8la2VeHB7w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-attribute-case-insensitive": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-7.0.1.tgz", + "integrity": "sha512-Uai+SupNSqzlschRyNx3kbCTWgY/2hcwtHEI/ej2LJWc9JJ77qKgGptd8DHwY1mXtZ7Aoh4z4yxfwMBue9eNgw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-attribute-case-insensitive/node_modules/postcss-selector-parser": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", + "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-clamp": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-clamp/-/postcss-clamp-4.1.0.tgz", + "integrity": "sha512-ry4b1Llo/9zz+PKC+030KUnPITTJAHeOwjfAyyB60eT0AorGLdzp52s31OsPRHRf8NchkgFoG2y6fCfn1IV1Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=7.6.0" + }, + "peerDependencies": { + "postcss": "^8.4.6" + } + }, + "node_modules/postcss-color-functional-notation": { + "version": "7.0.10", + "resolved": "https://registry.npmjs.org/postcss-color-functional-notation/-/postcss-color-functional-notation-7.0.10.tgz", + "integrity": "sha512-k9qX+aXHBiLTRrWoCJuUFI6F1iF6QJQUXNVWJVSbqZgj57jDhBlOvD8gNUGl35tgqDivbGLhZeW3Ongz4feuKA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.0.10", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.1.0", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-color-hex-alpha": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/postcss-color-hex-alpha/-/postcss-color-hex-alpha-10.0.0.tgz", + "integrity": "sha512-1kervM2cnlgPs2a8Vt/Qbe5cQ++N7rkYo/2rz2BkqJZIHQwaVuJgQH38REHrAi4uM0b1fqxMkWYmese94iMp3w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/utilities": "^2.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-color-rebeccapurple": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-10.0.0.tgz", + "integrity": "sha512-JFta737jSP+hdAIEhk1Vs0q0YF5P8fFcj+09pweS8ktuGuZ8pPlykHsk6mPxZ8awDl4TrcxUqJo9l1IhVr/OjQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/utilities": "^2.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-custom-media": { + "version": "11.0.6", + "resolved": "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-11.0.6.tgz", + "integrity": "sha512-C4lD4b7mUIw+RZhtY7qUbf4eADmb7Ey8BFA2px9jUbwg7pjTZDl4KY4bvlUV+/vXQvzQRfiGEVJyAbtOsCMInw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/cascade-layer-name-parser": "^2.0.5", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/media-query-list-parser": "^4.0.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-custom-properties": { + "version": "14.0.6", + "resolved": "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-14.0.6.tgz", + "integrity": "sha512-fTYSp3xuk4BUeVhxCSJdIPhDLpJfNakZKoiTDx7yRGCdlZrSJR7mWKVOBS4sBF+5poPQFMj2YdXx1VHItBGihQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/cascade-layer-name-parser": "^2.0.5", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/utilities": "^2.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-custom-selectors": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/postcss-custom-selectors/-/postcss-custom-selectors-8.0.5.tgz", + "integrity": "sha512-9PGmckHQswiB2usSO6XMSswO2yFWVoCAuih1yl9FVcwkscLjRKjwsjM3t+NIWpSU2Jx3eOiK2+t4vVTQaoCHHg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/cascade-layer-name-parser": "^2.0.5", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-custom-selectors/node_modules/postcss-selector-parser": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", + "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-dir-pseudo-class": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-9.0.1.tgz", + "integrity": "sha512-tRBEK0MHYvcMUrAuYMEOa0zg9APqirBcgzi6P21OhxtJyJADo/SWBwY1CAwEohQ/6HDaa9jCjLRG7K3PVQYHEA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-dir-pseudo-class/node_modules/postcss-selector-parser": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", + "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-double-position-gradients": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-double-position-gradients/-/postcss-double-position-gradients-6.0.2.tgz", + "integrity": "sha512-7qTqnL7nfLRyJK/AHSVrrXOuvDDzettC+wGoienURV8v2svNbu6zJC52ruZtHaO6mfcagFmuTGFdzRsJKB3k5Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/postcss-progressive-custom-properties": "^4.1.0", + "@csstools/utilities": "^2.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-focus-visible": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/postcss-focus-visible/-/postcss-focus-visible-10.0.1.tgz", + "integrity": "sha512-U58wyjS/I1GZgjRok33aE8juW9qQgQUNwTSdxQGuShHzwuYdcklnvK/+qOWX1Q9kr7ysbraQ6ht6r+udansalA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-focus-visible/node_modules/postcss-selector-parser": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", + "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-focus-within": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/postcss-focus-within/-/postcss-focus-within-9.0.1.tgz", + "integrity": "sha512-fzNUyS1yOYa7mOjpci/bR+u+ESvdar6hk8XNK/TRR0fiGTp2QT5N+ducP0n3rfH/m9I7H/EQU6lsa2BrgxkEjw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-focus-within/node_modules/postcss-selector-parser": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", + "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-font-variant": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-font-variant/-/postcss-font-variant-5.0.0.tgz", + "integrity": "sha512-1fmkBaCALD72CK2a9i468mA/+tr9/1cBxRRMXOUaZqO43oWPR5imcyPjXwuv7PXbCid4ndlP5zWhidQVVa3hmA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-gap-properties": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-gap-properties/-/postcss-gap-properties-6.0.0.tgz", + "integrity": "sha512-Om0WPjEwiM9Ru+VhfEDPZJAKWUd0mV1HmNXqp2C29z80aQ2uP9UVhLc7e3aYMIor/S5cVhoPgYQ7RtfeZpYTRw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-image-set-function": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-image-set-function/-/postcss-image-set-function-7.0.0.tgz", + "integrity": "sha512-QL7W7QNlZuzOwBTeXEmbVckNt1FSmhQtbMRvGGqqU4Nf4xk6KUEQhAoWuMzwbSv5jxiRiSZ5Tv7eiDB9U87znA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/utilities": "^2.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-lab-function": { + "version": "7.0.10", + "resolved": "https://registry.npmjs.org/postcss-lab-function/-/postcss-lab-function-7.0.10.tgz", + "integrity": "sha512-tqs6TCEv9tC1Riq6fOzHuHcZyhg4k3gIAMB8GGY/zA1ssGdm6puHMVE7t75aOSoFg7UD2wyrFFhbldiCMyyFTQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/css-color-parser": "^3.0.10", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/postcss-progressive-custom-properties": "^4.1.0", + "@csstools/utilities": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-logical": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/postcss-logical/-/postcss-logical-8.1.0.tgz", + "integrity": "sha512-pL1hXFQ2fEXNKiNiAgtfA005T9FBxky5zkX6s4GZM2D8RkVgRqz3f4g1JUoq925zXv495qk8UNldDwh8uGEDoA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-nesting": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-13.0.2.tgz", + "integrity": "sha512-1YCI290TX+VP0U/K/aFxzHzQWHWURL+CtHMSbex1lCdpXD1SoR2sYuxDu5aNI9lPoXpKTCggFZiDJbwylU0LEQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/selector-resolve-nested": "^3.1.0", + "@csstools/selector-specificity": "^5.0.0", + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-nesting/node_modules/@csstools/selector-resolve-nested": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-resolve-nested/-/selector-resolve-nested-3.1.0.tgz", + "integrity": "sha512-mf1LEW0tJLKfWyvn5KdDrhpxHyuxpbNwTIwOYLIvsTffeyOf85j5oIzfG0yosxDgx/sswlqBnESYUcQH0vgZ0g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss-selector-parser": "^7.0.0" + } + }, + "node_modules/postcss-nesting/node_modules/@csstools/selector-specificity": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-5.0.0.tgz", + "integrity": "sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss-selector-parser": "^7.0.0" + } + }, + "node_modules/postcss-nesting/node_modules/postcss-selector-parser": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", + "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-opacity-percentage": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-opacity-percentage/-/postcss-opacity-percentage-3.0.0.tgz", + "integrity": "sha512-K6HGVzyxUxd/VgZdX04DCtdwWJ4NGLG212US4/LA1TLAbHgmAsTWVR86o+gGIbFtnTkfOpb9sCRBx8K7HO66qQ==", + "dev": true, + "funding": [ + { + "type": "kofi", + "url": "https://ko-fi.com/mrcgrtz" + }, + { + "type": "liberapay", + "url": "https://liberapay.com/mrcgrtz" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-overflow-shorthand": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-overflow-shorthand/-/postcss-overflow-shorthand-6.0.0.tgz", + "integrity": "sha512-BdDl/AbVkDjoTofzDQnwDdm/Ym6oS9KgmO7Gr+LHYjNWJ6ExORe4+3pcLQsLA9gIROMkiGVjjwZNoL/mpXHd5Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-page-break": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/postcss-page-break/-/postcss-page-break-3.0.4.tgz", + "integrity": "sha512-1JGu8oCjVXLa9q9rFTo4MbeeA5FMe00/9C7lN4va606Rdb+HkxXtXsmEDrIraQ11fGz/WvKWa8gMuCKkrXpTsQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "postcss": "^8" + } + }, + "node_modules/postcss-place": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/postcss-place/-/postcss-place-10.0.0.tgz", + "integrity": "sha512-5EBrMzat2pPAxQNWYavwAfoKfYcTADJ8AXGVPcUZ2UkNloUTWzJQExgrzrDkh3EKzmAx1evfTAzF9I8NGcc+qw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-preset-env": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-10.2.4.tgz", + "integrity": "sha512-q+lXgqmTMdB0Ty+EQ31SuodhdfZetUlwCA/F0zRcd/XdxjzI+Rl2JhZNz5US2n/7t9ePsvuhCnEN4Bmu86zXlA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "@csstools/postcss-cascade-layers": "^5.0.2", + "@csstools/postcss-color-function": "^4.0.10", + "@csstools/postcss-color-mix-function": "^3.0.10", + "@csstools/postcss-color-mix-variadic-function-arguments": "^1.0.0", + "@csstools/postcss-content-alt-text": "^2.0.6", + "@csstools/postcss-exponential-functions": "^2.0.9", + "@csstools/postcss-font-format-keywords": "^4.0.0", + "@csstools/postcss-gamut-mapping": "^2.0.10", + "@csstools/postcss-gradients-interpolation-method": "^5.0.10", + "@csstools/postcss-hwb-function": "^4.0.10", + "@csstools/postcss-ic-unit": "^4.0.2", + "@csstools/postcss-initial": "^2.0.1", + "@csstools/postcss-is-pseudo-class": "^5.0.3", + "@csstools/postcss-light-dark-function": "^2.0.9", + "@csstools/postcss-logical-float-and-clear": "^3.0.0", + "@csstools/postcss-logical-overflow": "^2.0.0", + "@csstools/postcss-logical-overscroll-behavior": "^2.0.0", + "@csstools/postcss-logical-resize": "^3.0.0", + "@csstools/postcss-logical-viewport-units": "^3.0.4", + "@csstools/postcss-media-minmax": "^2.0.9", + "@csstools/postcss-media-queries-aspect-ratio-number-values": "^3.0.5", + "@csstools/postcss-nested-calc": "^4.0.0", + "@csstools/postcss-normalize-display-values": "^4.0.0", + "@csstools/postcss-oklab-function": "^4.0.10", + "@csstools/postcss-progressive-custom-properties": "^4.1.0", + "@csstools/postcss-random-function": "^2.0.1", + "@csstools/postcss-relative-color-syntax": "^3.0.10", + "@csstools/postcss-scope-pseudo-class": "^4.0.1", + "@csstools/postcss-sign-functions": "^1.1.4", + "@csstools/postcss-stepped-value-functions": "^4.0.9", + "@csstools/postcss-text-decoration-shorthand": "^4.0.2", + "@csstools/postcss-trigonometric-functions": "^4.0.9", + "@csstools/postcss-unset-value": "^4.0.0", + "autoprefixer": "^10.4.21", + "browserslist": "^4.25.0", + "css-blank-pseudo": "^7.0.1", + "css-has-pseudo": "^7.0.2", + "css-prefers-color-scheme": "^10.0.0", + "cssdb": "^8.3.0", + "postcss-attribute-case-insensitive": "^7.0.1", + "postcss-clamp": "^4.1.0", + "postcss-color-functional-notation": "^7.0.10", + "postcss-color-hex-alpha": "^10.0.0", + "postcss-color-rebeccapurple": "^10.0.0", + "postcss-custom-media": "^11.0.6", + "postcss-custom-properties": "^14.0.6", + "postcss-custom-selectors": "^8.0.5", + "postcss-dir-pseudo-class": "^9.0.1", + "postcss-double-position-gradients": "^6.0.2", + "postcss-focus-visible": "^10.0.1", + "postcss-focus-within": "^9.0.1", + "postcss-font-variant": "^5.0.0", + "postcss-gap-properties": "^6.0.0", + "postcss-image-set-function": "^7.0.0", + "postcss-lab-function": "^7.0.10", + "postcss-logical": "^8.1.0", + "postcss-nesting": "^13.0.2", + "postcss-opacity-percentage": "^3.0.0", + "postcss-overflow-shorthand": "^6.0.0", + "postcss-page-break": "^3.0.4", + "postcss-place": "^10.0.0", + "postcss-pseudo-class-any-link": "^10.0.1", + "postcss-replace-overflow-wrap": "^4.0.0", + "postcss-selector-not": "^8.0.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-pseudo-class-any-link": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-10.0.1.tgz", + "integrity": "sha512-3el9rXlBOqTFaMFkWDOkHUTQekFIYnaQY55Rsp8As8QQkpiSgIYEcF/6Ond93oHiDsGb4kad8zjt+NPlOC1H0Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-pseudo-class-any-link/node_modules/postcss-selector-parser": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", + "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-replace-overflow-wrap": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-4.0.0.tgz", + "integrity": "sha512-KmF7SBPphT4gPPcKZc7aDkweHiKEEO8cla/GjcBK+ckKxiZslIu3C4GCRW3DNfL0o7yW7kMQu9xlZ1kXRXLXtw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "postcss": "^8.0.3" + } + }, + "node_modules/postcss-selector-not": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/postcss-selector-not/-/postcss-selector-not-8.0.1.tgz", + "integrity": "sha512-kmVy/5PYVb2UOhy0+LqUYAhKj7DUGDpSWa5LZqlkWJaaAV+dxxsOG3+St0yNLu6vsKD7Dmqx+nWQt0iil89+WA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-selector-not/node_modules/postcss-selector-parser": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", + "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", + "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", + "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", + "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", + "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", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", + "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-docgen": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/react-docgen/-/react-docgen-8.0.0.tgz", + "integrity": "sha512-kmob/FOTwep7DUWf9KjuenKX0vyvChr3oTdvvPt09V60Iz75FJp+T/0ZeHMbAfJj2WaVWqAPP5Hmm3PYzSPPKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.18.9", + "@babel/traverse": "^7.18.9", + "@babel/types": "^7.18.9", + "@types/babel__core": "^7.18.0", + "@types/babel__traverse": "^7.18.0", + "@types/doctrine": "^0.0.9", + "@types/resolve": "^1.20.2", + "doctrine": "^3.0.0", + "resolve": "^1.22.1", + "strip-indent": "^4.0.0" + }, + "engines": { + "node": "^20.9.0 || >=22" + } + }, + "node_modules/react-docgen-typescript": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/react-docgen-typescript/-/react-docgen-typescript-2.4.0.tgz", + "integrity": "sha512-ZtAp5XTO5HRzQctjPU0ybY0RRCQO19X/8fxn3w7y2VVTUbGHDKULPTL4ky3vB05euSgG5NpALhEhDPvQ56wvXg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "typescript": ">= 4.3.x" + } + }, + "node_modules/react-dom": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", + "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.26.0" + }, + "peerDependencies": { + "react": "^19.1.0" + } + }, + "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" + }, + "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==", + "dev": true, + "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" + }, + "peerDependencies": { + "@types/react": ">=18", + "react": ">=18" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/recast": { + "version": "0.23.11", + "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.11.tgz", + "integrity": "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ast-types": "^0.16.1", + "esprima": "~4.0.0", + "source-map": "~0.6.1", + "tiny-invariant": "^1.3.3", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">= 4" + } + }, + "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", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/redent/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/refractor": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/refractor/-/refractor-4.9.0.tgz", + "integrity": "sha512-nEG1SPXFoGGx+dcjftjv8cAjEusIh6ED1xhf5DG3C0x/k+rmZ2duKnc3QLpt6qeHv5fPb8uwN3VWN2BT7fr3Og==", + "license": "MIT", + "dependencies": { + "@types/hast": "^2.0.0", + "@types/prismjs": "^1.0.0", + "hastscript": "^7.0.0", + "parse-entities": "^4.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/@types/hast": { + "version": "2.3.10", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.10.tgz", + "integrity": "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/refractor/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/refractor/node_modules/hast-util-parse-selector": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-3.1.1.tgz", + "integrity": "sha512-jdlwBjEexy1oGz0aJ2f4GKMaVKkA9jwjr4MjAAI22E5fM/TXVZHuS5OpONtdeIkRKqAaryQ2E9xNQxijoThSZA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/refractor/node_modules/hastscript": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-7.2.0.tgz", + "integrity": "sha512-TtYPq24IldU8iKoJQqvZOuhi5CyCQRAbvDOX0x1eW6rsHSxa/1i2CCiptNTotGHJ3VoHRGmqiv6/D3q113ikkw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^3.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/refractor/node_modules/property-information": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz", + "integrity": "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "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-parse": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/rehype-parse/-/rehype-parse-9.0.1.tgz", + "integrity": "sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-from-html": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-prism-plus": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/rehype-prism-plus/-/rehype-prism-plus-2.0.1.tgz", + "integrity": "sha512-Wglct0OW12tksTUseAPyWPo3srjBOY7xKlql/DPKi7HbsdZTyaLCAoO58QBKSczFQxElTsQlOY3JDOFzB/K++Q==", + "license": "MIT", + "dependencies": { + "hast-util-to-string": "^3.0.0", + "parse-numeric-range": "^1.3.0", + "refractor": "^4.8.0", + "rehype-parse": "^9.0.0", + "unist-util-filter": "^5.0.0", + "unist-util-visit": "^5.0.0" + } + }, + "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": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/remark/-/remark-15.0.1.tgz", + "integrity": "sha512-Eht5w30ruCXgFmxVUSlNWQ9iiimq07URKeFS3hNc8cUWy1llX4KDWfyEDZRycMc+znsN9Ux5/tJ/BFdgdOwA3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-gfm": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", + "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-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", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.42.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.42.0.tgz", + "integrity": "sha512-LW+Vse3BJPyGJGAJt1j8pWDKPd73QM8cRXYK1IxOBgL2AGLu7Xd2YOW0M2sLUBCkF5MshXXtMApyEAEzMVMsnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.7" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.42.0", + "@rollup/rollup-android-arm64": "4.42.0", + "@rollup/rollup-darwin-arm64": "4.42.0", + "@rollup/rollup-darwin-x64": "4.42.0", + "@rollup/rollup-freebsd-arm64": "4.42.0", + "@rollup/rollup-freebsd-x64": "4.42.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.42.0", + "@rollup/rollup-linux-arm-musleabihf": "4.42.0", + "@rollup/rollup-linux-arm64-gnu": "4.42.0", + "@rollup/rollup-linux-arm64-musl": "4.42.0", + "@rollup/rollup-linux-loongarch64-gnu": "4.42.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.42.0", + "@rollup/rollup-linux-riscv64-gnu": "4.42.0", + "@rollup/rollup-linux-riscv64-musl": "4.42.0", + "@rollup/rollup-linux-s390x-gnu": "4.42.0", + "@rollup/rollup-linux-x64-gnu": "4.42.0", + "@rollup/rollup-linux-x64-musl": "4.42.0", + "@rollup/rollup-win32-arm64-msvc": "4.42.0", + "@rollup/rollup-win32-ia32-msvc": "4.42.0", + "@rollup/rollup-win32-x64-msvc": "4.42.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/rollup/node_modules/@types/estree": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", + "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/scheduler": { + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", + "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/seroval": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.3.2.tgz", + "integrity": "sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/seroval-plugins": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.3.2.tgz", + "integrity": "sha512-0QvCV2lM3aj/U3YozDiVwx9zpH0q8A60CTWIv4Jszj/givcudPb48B+rkU5D51NJ0pTpweGMttHjboPa9/zoIQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "seroval": "^1.0" + } + }, + "node_modules/shebang-command": { + "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" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "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/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/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sirv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.1.tgz", + "integrity": "sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/solid-js": { + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.9.7.tgz", + "integrity": "sha512-/saTKi8iWEM233n5OSi1YHCCuh66ZIQ7aK2hsToPe4tqGm7qAejU1SwNuTPivbWAYq7SjuHVVYxxuZQNRbICiw==", + "license": "MIT", + "dependencies": { + "csstype": "^3.1.0", + "seroval": "~1.3.0", + "seroval-plugins": "~1.3.0" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz", + "integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==", + "dev": true, + "license": "MIT" + }, + "node_modules/storybook": { + "version": "9.0.14", + "resolved": "https://registry.npmjs.org/storybook/-/storybook-9.0.14.tgz", + "integrity": "sha512-PfVo9kSa4XsDTD2gXFvMRGix032+clBDcUMI4MhUzYxONLiZifnhwch4p/1lG+c3IVN4qkOEgGNc9PEgVMgApw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@storybook/global": "^5.0.0", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/user-event": "^14.6.1", + "@vitest/expect": "3.2.4", + "@vitest/spy": "3.2.4", + "better-opn": "^3.0.2", + "esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0", + "esbuild-register": "^3.5.0", + "recast": "^0.23.5", + "semver": "^7.6.2", + "ws": "^8.18.0" + }, + "bin": { + "storybook": "bin/index.cjs" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "prettier": "^2 || ^3" + }, + "peerDependenciesMeta": { + "prettier": { + "optional": true + } + } + }, + "node_modules/storybook/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "dev": true, + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-indent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-4.0.0.tgz", + "integrity": "sha512-mnVSV2l+Zv6BLpSD/8V87CW/y9EmmbYzGCIavsnsI6/nwn26DwffM/yztm30Z/I2DY9wdS3vXVCMnHDgZaVNoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-literal": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.0.0.tgz", + "integrity": "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/style-to-js": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.16.tgz", + "integrity": "sha512-/Q6ld50hKYPH3d/r6nr117TZkHR0w0kGGIVfpG9N6D8NymRPM9RqCUv4pRpJ62E5DqOYx2AFpbZMyCPnjQCnOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "style-to-object": "1.0.8" + } + }, + "node_modules/style-to-object": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.8.tgz", + "integrity": "sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.4" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tabbable": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", + "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==" + }, + "node_modules/tailwindcss": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz", + "integrity": "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz", + "integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tar": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", + "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/test-exclude": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", + "integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^9.0.4" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/test-exclude/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==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, + "node_modules/tiny-warning": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", + "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==", + "license": "MIT" + }, + "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": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", + "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.4.4", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.4.5", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.5.tgz", + "integrity": "sha512-4BG7puHpVsIYxZUbiUE3RqGloLaSSwzYie5jvasC4LWuBWzZawynvYouhjbQKw2JuIGYdm0DzIxl8iVidKlUEw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.3.tgz", + "integrity": "sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.10" + } + }, + "node_modules/tsconfck": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.6.tgz", + "integrity": "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==", + "dev": true, + "license": "MIT", + "bin": { + "tsconfck": "bin/tsconfck.js" + }, + "engines": { + "node": "^18 || >=20" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "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==" + }, + "node_modules/tsx": { + "version": "4.19.4", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.4.tgz", + "integrity": "sha512-gK5GVzDkJK1SI1zwHf32Mqxf2tSJkNx+eYcNly5+nHvWqXUJYUkWBQtKauoESz3ymezAI++ZwT855x5p5eop+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.25.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.34.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.34.0.tgz", + "integrity": "sha512-MRpfN7uYjTrTGigFCt8sRyNqJFhjN0WwZecldaqhWm+wy0gaRt8Edb/3cuUy0zdq2opJWT6iXINKAtewnDOltQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.34.0", + "@typescript-eslint/parser": "8.34.0", + "@typescript-eslint/utils": "8.34.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/undici-types": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.14.0.tgz", + "integrity": "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==", + "dev": true, + "license": "MIT" + }, + "node_modules/unicorn-magic": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", + "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-builder": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-builder/-/unist-builder-4.0.0.tgz", + "integrity": "sha512-wmRFnH+BLpZnTKpc5L7O67Kac89s9HMrtELpnNaE6TAobq5DTZZs5YaTQfAZBA9bFPECx2uVAPO31c+GVug8mg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-filter": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/unist-util-filter/-/unist-util-filter-5.0.1.tgz", + "integrity": "sha512-pHx7D4Zt6+TsfwylH9+lYhBhzyhEnCXs/lbq/Hstxno5z4gVdyc2WEW0asfjGKPyG4pEKrnBv5hdkO6+aRnQJw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + } + }, + "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.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-parents": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unist-util-parents/-/unist-util-parents-3.0.0.tgz", + "integrity": "sha512-3DVSfp+MkJhcJbGn/W7aOlZYVpsMQQ054cpfbPHZAqYPu/lvu5rCdzjuIt4eEMriLkCWLcnJjax97Awm1Bkhtg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "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", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", + "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", + "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unplugin": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.5.tgz", + "integrity": "sha512-RyWSb5AHmGtjjNQ6gIlA67sHOsWpsbWpwDokLwTcejVdOjEkJZh7QKu14J00gDDVSh8kGH4KYC/TNBceXFZhtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.14.1", + "picomatch": "^4.0.2", + "webpack-virtual-modules": "^0.6.2" + }, + "engines": { + "node": ">=18.12.0" + } + }, + "node_modules/unplugin/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", + "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "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.2", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz", + "integrity": "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vite": { + "version": "6.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", + "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite-tsconfig-paths": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-5.1.4.tgz", + "integrity": "sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "globrex": "^0.1.2", + "tsconfck": "^3.0.3" + }, + "peerDependencies": { + "vite": "*" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/fdir": { + "version": "6.4.5", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.5.tgz", + "integrity": "sha512-4BG7puHpVsIYxZUbiUE3RqGloLaSSwzYie5jvasC4LWuBWzZawynvYouhjbQKw2JuIGYdm0DzIxl8iVidKlUEw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vitest": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "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/webpack-virtual-modules": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", + "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/whatwg-fetch": { + "version": "3.6.20", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", + "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==", + "license": "MIT" + }, + "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" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "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", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "3.25.57", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.57.tgz", + "integrity": "sha512-6tgzLuwVST5oLUxXTmBqoinKMd3JeesgbgseXeFasKKj8Q1FCZrHnbqJOyiEvr4cVAlbug+CgIsmJ8cl/pU5FA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/app/ui/app/package.json b/app/ui/app/package.json new file mode 100644 index 000000000..052e0e625 --- /dev/null +++ b/app/ui/app/package.json @@ -0,0 +1,81 @@ +{ + "name": "app", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview", + "prettier": "prettier --write .", + "prettier:check": "prettier --check .", + "storybook": "storybook dev -p 6006", + "build-storybook": "storybook build", + "test": "vitest", + "test:ui": "vitest --ui", + "test:coverage": "vitest --coverage" + }, + "dependencies": { + "@headlessui/react": "^2.2.4", + "@heroicons/react": "^2.2.0", + "@tanstack/react-query": "^5.80.7", + "@tanstack/react-router": "^1.120.20", + "@tanstack/react-router-devtools": "^1.120.20", + "clsx": "^2.1.1", + "framer-motion": "^12.17.0", + "katex": "^0.16.22", + "micromark-extension-llm-math": "^3.1.0", + "ollama": "^0.6.0", + "react": "^19.1.0", + "react-dom": "^19.1.0", + "rehype-katex": "^7.0.1", + "rehype-prism-plus": "^2.0.1", + "rehype-raw": "^7.0.0", + "rehype-sanitize": "^6.0.0", + "remark-math": "^6.0.0", + "unist-builder": "^4.0.0", + "unist-util-parents": "^3.0.0" + }, + "devDependencies": { + "@chromatic-com/storybook": "^4.0.1", + "@eslint/js": "^9.25.0", + "@storybook/addon-a11y": "^9.0.14", + "@storybook/addon-docs": "^9.0.14", + "@storybook/addon-onboarding": "^9.0.14", + "@storybook/addon-vitest": "^9.0.14", + "@storybook/react-vite": "^9.0.14", + "@tailwindcss/typography": "^0.5.16", + "@tailwindcss/vite": "^4.1.11", + "@tanstack/router-plugin": "^1.120.20", + "@types/node": "^24.7.2", + "@types/react": "^19.1.2", + "@types/react-dom": "^19.1.2", + "@vitejs/plugin-react": "^4.4.1", + "@vitest/browser": "^3.2.4", + "@vitest/coverage-v8": "^3.2.4", + "@vitest/ui": "^3.2.4", + "autoprefixer": "^10.4.21", + "eslint": "^9.25.0", + "eslint-plugin-react-hooks": "^5.2.0", + "eslint-plugin-react-refresh": "^0.4.19", + "eslint-plugin-storybook": "^9.0.14", + "globals": "^16.0.0", + "playwright": "^1.53.2", + "postcss-preset-env": "^10.2.4", + "react-markdown": "^10.1.0", + "remark": "^15.0.1", + "remark-gfm": "^4.0.1", + "remark-stringify": "^11.0.0", + "storybook": "^9.0.14", + "tailwindcss": "^4.1.9", + "typescript": "~5.8.3", + "typescript-eslint": "^8.30.1", + "vite": "^6.3.5", + "vite-tsconfig-paths": "^5.1.4", + "vitest": "^3.2.4" + }, + "overrides": { + "mdast-util-gfm-autolink-literal": "2.0.0" + } +} diff --git a/app/ui/app/public/hello.png b/app/ui/app/public/hello.png new file mode 100644 index 000000000..fd04d9d0a Binary files /dev/null and b/app/ui/app/public/hello.png differ diff --git a/app/ui/app/src/api.ts b/app/ui/app/src/api.ts new file mode 100644 index 000000000..c8b2e1165 --- /dev/null +++ b/app/ui/app/src/api.ts @@ -0,0 +1,405 @@ +import { + ChatResponse, + ChatsResponse, + ChatEvent, + DownloadEvent, + ErrorEvent, + InferenceCompute, + InferenceComputeResponse, + ModelCapabilitiesResponse, + Model, + ChatRequest, + Settings, + User, +} from "@/gotypes"; +import { parseJsonlFromResponse } from "./util/jsonl-parsing"; +import { ollamaClient as ollama } from "./lib/ollama-client"; +import type { ModelResponse } from "ollama/browser"; + +// Extend Model class with utility methods +declare module "@/gotypes" { + interface Model { + isCloud(): boolean; + } +} + +Model.prototype.isCloud = function (): boolean { + return this.model.endsWith("cloud"); +}; + +const API_BASE = import.meta.env.DEV ? "http://127.0.0.1:3001" : ""; + +// Helper function to convert Uint8Array to base64 +function uint8ArrayToBase64(uint8Array: Uint8Array): string { + const chunkSize = 0x8000; // 32KB chunks to avoid stack overflow + let binary = ""; + + for (let i = 0; i < uint8Array.length; i += chunkSize) { + const chunk = uint8Array.subarray(i, i + chunkSize); + binary += String.fromCharCode(...chunk); + } + + return btoa(binary); +} + +export async function fetchUser(): Promise { + try { + const response = await fetch(`${API_BASE}/api/v1/me`, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + + if (response.ok) { + const userData: User = await response.json(); + return userData; + } + + return null; + } catch (error) { + console.error("Error fetching user:", error); + return null; + } +} + +export async function fetchConnectUrl(): Promise { + const response = await fetch(`${API_BASE}/api/v1/connect`, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + throw new Error("Failed to fetch connect URL"); + } + + const data = await response.json(); + return data.connect_url; +} + +export async function disconnectUser(): Promise { + const response = await fetch(`${API_BASE}/api/v1/disconnect`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + throw new Error("Failed to disconnect user"); + } +} + +export async function getChats(): Promise { + const response = await fetch(`${API_BASE}/api/v1/chats`); + const data = await response.json(); + return new ChatsResponse(data); +} + +export async function getChat(chatId: string): Promise { + const response = await fetch(`${API_BASE}/api/v1/chat/${chatId}`); + const data = await response.json(); + return new ChatResponse(data); +} + +export async function getModels(query?: string): Promise { + try { + const { models: modelsResponse } = await ollama.list(); + + let models: Model[] = modelsResponse + .filter((m: ModelResponse) => { + const families = m.details?.families; + + if (!families || families.length === 0) { + return true; + } + + const isBertOnly = families.every((family: string) => + family.toLowerCase().includes("bert"), + ); + + return !isBertOnly; + }) + .map((m: ModelResponse) => { + // Remove the latest tag from the returned model + const modelName = m.name.replace(/:latest$/, ""); + + return new Model({ + model: modelName, + digest: m.digest, + modified_at: m.modified_at ? new Date(m.modified_at) : undefined, + }); + }); + + // Filter by query if provided + if (query) { + const normalizedQuery = query.toLowerCase().trim(); + + const filteredModels = models.filter((m: Model) => { + return m.model.toLowerCase().startsWith(normalizedQuery); + }); + + let exactMatch = false; + for (const m of filteredModels) { + if (m.model.toLowerCase() === normalizedQuery) { + exactMatch = true; + break; + } + } + + // Add query if it's in the registry and not already in the list + if (!exactMatch) { + const result = await getModelUpstreamInfo(new Model({ model: query })); + const existsUpstream = !!result.digest && !result.error; + if (existsUpstream) { + filteredModels.push(new Model({ model: query })); + } + } + + models = filteredModels; + } + + return models; + } catch (err) { + throw new Error(`Failed to fetch models: ${err}`); + } +} + +export async function getModelCapabilities( + modelName: string, +): Promise { + try { + const showResponse = await ollama.show({ model: modelName }); + + return new ModelCapabilitiesResponse({ + capabilities: Array.isArray(showResponse.capabilities) + ? showResponse.capabilities + : [], + }); + } catch (error) { + // Model might not be downloaded yet, return empty capabilities + console.error(`Failed to get capabilities for ${modelName}:`, error); + return new ModelCapabilitiesResponse({ capabilities: [] }); + } +} + +export type ChatEventUnion = ChatEvent | DownloadEvent | ErrorEvent; + +export async function* sendMessage( + chatId: string, + message: string, + model: Model, + attachments?: Array<{ filename: string; data: Uint8Array }>, + signal?: AbortSignal, + index?: number, + webSearch?: boolean, + fileTools?: boolean, + forceUpdate?: boolean, + think?: boolean | string, +): AsyncGenerator { + // Convert Uint8Array to base64 for JSON serialization + const serializedAttachments = attachments?.map((att) => ({ + filename: att.filename, + data: uint8ArrayToBase64(att.data), + })); + + const response = await fetch(`${API_BASE}/api/v1/chat/${chatId}`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify( + new ChatRequest({ + model: model.model, + prompt: message, + ...(index !== undefined ? { index } : {}), + ...(serializedAttachments !== undefined + ? { attachments: serializedAttachments } + : {}), + // Always send web_search as a boolean value (default to false) + web_search: webSearch ?? false, + file_tools: fileTools ?? false, + ...(forceUpdate !== undefined ? { forceUpdate } : {}), + ...(think !== undefined ? { think } : {}), + }), + ), + signal, + }); + + for await (const event of parseJsonlFromResponse(response)) { + switch (event.eventName) { + case "download": + yield new DownloadEvent(event); + break; + case "error": + yield new ErrorEvent(event); + break; + default: + yield new ChatEvent(event); + break; + } + } +} + +export async function getSettings(): Promise<{ + settings: Settings; +}> { + const response = await fetch(`${API_BASE}/api/v1/settings`); + if (!response.ok) { + throw new Error("Failed to fetch settings"); + } + const data = await response.json(); + return { + settings: new Settings(data.settings), + }; +} + +export async function updateSettings(settings: Settings): Promise<{ + settings: Settings; +}> { + const response = await fetch(`${API_BASE}/api/v1/settings`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(settings), + }); + if (!response.ok) { + const error = await response.text(); + throw new Error(error || "Failed to update settings"); + } + const data = await response.json(); + return { + settings: new Settings(data.settings), + }; +} + +export async function renameChat(chatId: string, title: string): Promise { + const response = await fetch(`${API_BASE}/api/v1/chat/${chatId}/rename`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ title: title.trim() }), + }); + if (!response.ok) { + const error = await response.text(); + throw new Error(error || "Failed to rename chat"); + } +} + +export async function deleteChat(chatId: string): Promise { + const response = await fetch(`${API_BASE}/api/v1/chat/${chatId}`, { + method: "DELETE", + }); + if (!response.ok) { + const error = await response.text(); + throw new Error(error || "Failed to delete chat"); + } +} + +// Get upstream information for model staleness checking +export async function getModelUpstreamInfo( + model: Model, +): Promise<{ digest?: string; pushTime: number; error?: string }> { + try { + const response = await fetch(`${API_BASE}/api/v1/model/upstream`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + model: model.model, + }), + }); + + if (!response.ok) { + console.warn( + `Failed to check upstream digest for ${model.model}: ${response.status}`, + ); + return { pushTime: 0 }; + } + + const data = await response.json(); + + if (data.error) { + console.warn(`Upstream digest check: ${data.error}`); + return { error: data.error, pushTime: 0 }; + } + + return { digest: data.digest, pushTime: data.pushTime || 0 }; + } catch (error) { + console.warn(`Error checking model staleness:`, error); + return { pushTime: 0 }; + } +} + +export async function* pullModel( + modelName: string, + signal?: AbortSignal, +): AsyncGenerator<{ + status: string; + digest?: string; + total?: number; + completed?: number; + done?: boolean; +}> { + const response = await fetch(`${API_BASE}/api/v1/models/pull`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ name: modelName }), + signal, + }); + + if (!response.ok) { + throw new Error(`Failed to pull model: ${response.statusText}`); + } + + for await (const event of parseJsonlFromResponse<{ + status: string; + digest?: string; + total?: number; + completed?: number; + done?: boolean; + }>(response)) { + yield event; + } +} + +export async function getInferenceCompute(): Promise { + const response = await fetch(`${API_BASE}/api/v1/inference-compute`); + if (!response.ok) { + throw new Error( + `Failed to fetch inference compute: ${response.statusText}`, + ); + } + + const data = await response.json(); + const inferenceComputeResponse = new InferenceComputeResponse(data); + return inferenceComputeResponse.inferenceComputes || []; +} + +export async function fetchHealth(): Promise { + try { + const response = await fetch(`${API_BASE}/api/v1/health`, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }); + + if (response.ok) { + const data = await response.json(); + return data.healthy || false; + } + + return false; + } catch (error) { + console.error("Error checking health:", error); + return false; + } +} diff --git a/app/ui/app/src/components/Chat.tsx b/app/ui/app/src/components/Chat.tsx new file mode 100644 index 000000000..e0fcb4ff3 --- /dev/null +++ b/app/ui/app/src/components/Chat.tsx @@ -0,0 +1,298 @@ +import MessageList from "./MessageList"; +import ChatForm from "./ChatForm"; +import { FileUpload } from "./FileUpload"; +import { DisplayUpgrade } from "./DisplayUpgrade"; +import { DisplayStale } from "./DisplayStale"; +import { DisplayLogin } from "./DisplayLogin"; +import { + useChat, + useSendMessage, + useIsStreaming, + useIsWaitingForLoad, + useDownloadProgress, + useChatError, + useShouldShowStaleDisplay, + useDismissStaleModel, +} from "@/hooks/useChats"; +import { useHealth } from "@/hooks/useHealth"; +import { useMessageAutoscroll } from "@/hooks/useMessageAutoscroll"; +import { + useState, + useEffect, + useLayoutEffect, + useRef, + useCallback, +} from "react"; +import { useQueryClient } from "@tanstack/react-query"; +import { useNavigate } from "@tanstack/react-router"; +import { useSelectedModel } from "@/hooks/useSelectedModel"; +import { useUser } from "@/hooks/useUser"; +import { useHasVisionCapability } from "@/hooks/useModelCapabilities"; +import { Message } from "@/gotypes"; + +export default function Chat({ chatId }: { chatId: string }) { + const queryClient = useQueryClient(); + const navigate = useNavigate(); + const chatQuery = useChat(chatId === "new" ? "" : chatId); + const chatErrorQuery = useChatError(chatId === "new" ? "" : chatId); + const { selectedModel } = useSelectedModel(chatId); + const { user } = useUser(); + const hasVisionCapability = useHasVisionCapability(selectedModel?.model); + const shouldShowStaleDisplay = useShouldShowStaleDisplay(selectedModel); + const dismissStaleModel = useDismissStaleModel(); + const { isHealthy } = useHealth(); + + const [editingMessage, setEditingMessage] = useState<{ + content: string; + index: number; + originalMessage: Message; + } | null>(null); + const prevChatIdRef = useRef(chatId); + + const chatFormCallbackRef = useRef< + | (( + files: Array<{ filename: string; data: Uint8Array; type?: string }>, + errors: Array<{ filename: string; error: string }>, + ) => void) + | null + >(null); + + const handleFilesReceived = useCallback( + ( + callback: ( + files: Array<{ + filename: string; + data: Uint8Array; + type?: string; + }>, + errors: Array<{ filename: string; error: string }>, + ) => void, + ) => { + chatFormCallbackRef.current = callback; + }, + [], + ); + + const handleFilesProcessed = useCallback( + ( + files: Array<{ filename: string; data: Uint8Array; type?: string }>, + errors: Array<{ filename: string; error: string }> = [], + ) => { + chatFormCallbackRef.current?.(files, errors); + }, + [], + ); + + const allMessages = chatQuery?.data?.chat?.messages ?? []; + // TODO(parthsareen): will need to consolidate when used with more tools with state + const browserToolResult = chatQuery?.data?.chat?.browser_state; + const chatError = chatErrorQuery.data; + + const messages = allMessages; + const isStreaming = useIsStreaming(chatId); + const isWaitingForLoad = useIsWaitingForLoad(chatId); + const downloadProgress = useDownloadProgress(chatId); + const isDownloadingModel = downloadProgress && !downloadProgress.done; + const isDisabled = !isHealthy; + + // Clear editing state when navigating to a different chat + useEffect(() => { + setEditingMessage(null); + }, [chatId]); + + const sendMessageMutation = useSendMessage(chatId); + + const { containerRef, handleNewUserMessage, spacerHeight } = + useMessageAutoscroll({ + messages, + isStreaming, + chatId, + }); + + // Scroll to bottom only when switching to a different existing chat + useLayoutEffect(() => { + // Only scroll if the chatId actually changed (not just messages updating) + if ( + prevChatIdRef.current !== chatId && + containerRef.current && + messages.length > 0 && + chatId !== "new" + ) { + // Always scroll to the bottom when opening a chat + containerRef.current.scrollTop = containerRef.current.scrollHeight; + } + prevChatIdRef.current = chatId; + }, [chatId, messages.length]); + + // Simplified submit handler - ChatForm handles all the attachment logic + const handleChatFormSubmit = ( + message: string, + options: { + attachments?: Array<{ filename: string; data: Uint8Array }>; + index?: number; + webSearch?: boolean; + fileTools?: boolean; + think?: boolean | string; + }, + ) => { + // Clear any existing errors when sending a new message + sendMessageMutation.reset(); + if (chatError) { + clearChatError(); + } + + // Prepare attachments for backend + const allAttachments = (options.attachments || []).map((att) => ({ + filename: att.filename, + data: att.data.length === 0 ? new Uint8Array(0) : att.data, + })); + + sendMessageMutation.mutate({ + message, + attachments: allAttachments, + index: editingMessage ? editingMessage.index : options.index, + webSearch: options.webSearch, + fileTools: options.fileTools, + think: options.think, + onChatEvent: (event) => { + if (event.eventName === "chat_created" && event.chatId) { + navigate({ + to: "/c/$chatId", + params: { + chatId: event.chatId, + }, + }); + } + }, + }); + + // Clear edit mode after submission + setEditingMessage(null); + handleNewUserMessage(); + }; + + const handleEditMessage = (content: string, index: number) => { + setEditingMessage({ + content, + index, + originalMessage: messages[index], + }); + }; + + const handleCancelEdit = () => { + setEditingMessage(null); + if (chatError) { + clearChatError(); + } + }; + + const clearChatError = () => { + queryClient.setQueryData( + ["chatError", chatId === "new" ? "" : chatId], + null, + ); + }; + + const isWindows = navigator.platform.toLowerCase().includes("win"); + + return chatId === "new" || chatQuery ? ( + + {chatId === "new" ? ( +
+
+ +
+
+ ) : ( +
+
+ { + handleEditMessage(content, index); + }} + editingMessageIndex={editingMessage?.index} + error={chatError} + browserToolResult={browserToolResult} + /> +
+ +
+ {selectedModel && shouldShowStaleDisplay && ( +
+ + dismissStaleModel(selectedModel?.model || "") + } + chatId={chatId} + onScrollToBottom={() => { + if (containerRef.current) { + containerRef.current.scrollTo({ + top: containerRef.current.scrollHeight, + behavior: "smooth", + }); + } + }} + /> +
+ )} + {chatError && chatError.code === "usage_limit_upgrade" && ( +
+ +
+ )} + {chatError && chatError.code === "cloud_unauthorized" && ( +
+ +
+ )} + 0} + onSubmit={handleChatFormSubmit} + chatId={chatId} + autoFocus={true} + editingMessage={editingMessage} + onCancelEdit={handleCancelEdit} + isDisabled={isDisabled} + isDownloadingModel={isDownloadingModel} + onFilesReceived={handleFilesReceived} + /> +
+
+ )} +
+ ) : ( +
Loading...
+ ); +} diff --git a/app/ui/app/src/components/ChatForm.tsx b/app/ui/app/src/components/ChatForm.tsx new file mode 100644 index 000000000..b13ebd802 --- /dev/null +++ b/app/ui/app/src/components/ChatForm.tsx @@ -0,0 +1,984 @@ +import Logo from "@/components/Logo"; +import { ModelPicker } from "@/components/ModelPicker"; +import { WebSearchButton } from "@/components/WebSearchButton"; +import { ImageThumbnail } from "@/components/ImageThumbnail"; +import { isImageFile } from "@/utils/imageUtils"; +import { + useRef, + useState, + useEffect, + useLayoutEffect, + useCallback, +} from "react"; +import { + useSendMessage, + useIsStreaming, + useCancelMessage, +} from "@/hooks/useChats"; +import { useNavigate } from "@tanstack/react-router"; +import { useSelectedModel } from "@/hooks/useSelectedModel"; +import { useHasVisionCapability } from "@/hooks/useModelCapabilities"; +import { useUser } from "@/hooks/useUser"; +import { DisplayLogin } from "@/components/DisplayLogin"; +import { ErrorEvent, Message } from "@/gotypes"; +import { useSettings } from "@/hooks/useSettings"; +import { ThinkButton } from "./ThinkButton"; +import { ErrorMessage } from "./ErrorMessage"; +import { processFiles } from "@/utils/fileValidation"; +import type { ImageData } from "@/types/webview"; +import { PlusIcon } from "@heroicons/react/24/outline"; + +export type ThinkingLevel = "low" | "medium" | "high"; + +interface FileAttachment { + filename: string; + data: Uint8Array; + type?: string; // MIME type +} + +interface MessageInput { + content: string; + attachments: Array<{ + id: string; + filename: string; + data?: Uint8Array; // undefined for existing files from editing + }>; + fileErrors: Array<{ filename: string; error: string }>; +} + +interface ChatFormProps { + hasMessages: boolean; + onSubmit?: ( + message: string, + options: { + attachments?: FileAttachment[]; + index?: number; + webSearch?: boolean; + fileTools?: boolean; + think?: boolean | string; + }, + ) => void; + autoFocus?: boolean; + chatId?: string; + isDownloadingModel?: boolean; + isDisabled?: boolean; + // Editing props - when provided, ChatForm enters edit mode + editingMessage?: { + content: string; + index: number; + originalMessage: Message; + } | null; + onCancelEdit?: () => void; + onFilesReceived?: ( + callback: ( + files: Array<{ filename: string; data: Uint8Array; type?: string }>, + errors: Array<{ filename: string; error: string }>, + ) => void, + ) => void; +} + +function ChatForm({ + hasMessages, + onSubmit, + autoFocus = false, + chatId = "new", + isDownloadingModel = false, + isDisabled = false, + editingMessage, + onCancelEdit, + onFilesReceived, +}: ChatFormProps) { + const [message, setMessage] = useState({ + content: "", + attachments: [], + fileErrors: [], + }); + const [isEditing, setIsEditing] = useState(false); + const compositionEndTimeoutRef = useRef(null); + const fileInputRef = useRef(null); + const textareaRef = useRef(null); + const thinkButtonRef = useRef(null); + const thinkingLevelButtonRef = useRef(null); + const webSearchButtonRef = useRef(null); + const modelPickerRef = useRef(null); + const submitButtonRef = useRef(null); + + const { mutate: sendMessageMutation } = useSendMessage(chatId); + const navigate = useNavigate(); + const isStreaming = useIsStreaming(chatId); + const cancelMessage = useCancelMessage(); + const isDownloading = isDownloadingModel; + const { selectedModel } = useSelectedModel(); + const hasVisionCapability = useHasVisionCapability(selectedModel?.model); + const { isAuthenticated, isLoading: isLoadingUser } = useUser(); + const [loginPromptFeature, setLoginPromptFeature] = useState< + "webSearch" | "turbo" | null + >(null); + const [fileUploadError, setFileUploadError] = useState( + null, + ); + + const handleThinkingLevelDropdownToggle = (isOpen: boolean) => { + if ( + isOpen && + modelPickerRef.current && + (modelPickerRef.current as any).closeDropdown + ) { + (modelPickerRef.current as any).closeDropdown(); + } + }; + + const handleModelPickerDropdownToggle = (isOpen: boolean) => { + if ( + isOpen && + thinkingLevelButtonRef.current && + (thinkingLevelButtonRef.current as any).closeDropdown + ) { + (thinkingLevelButtonRef.current as any).closeDropdown(); + } + }; + + const { + settings: { + webSearchEnabled, + airplaneMode, + thinkEnabled, + thinkLevel: settingsThinkLevel, + }, + setSettings, + } = useSettings(); + + // current supported models for web search + const modelLower = selectedModel?.model.toLowerCase() || ""; + const supportsWebSearch = + modelLower.startsWith("gpt-oss") || + modelLower.startsWith("qwen3") || + modelLower.startsWith("deepseek-v3"); + // Use per-chat thinking level instead of global + const thinkLevel: ThinkingLevel = + settingsThinkLevel === "none" || !settingsThinkLevel + ? "medium" + : (settingsThinkLevel as ThinkingLevel); + const setThinkingLevel = (newLevel: ThinkingLevel) => { + setSettings({ ThinkLevel: newLevel }); + }; + + const modelSupportsThinkingLevels = + selectedModel?.model.toLowerCase().startsWith("gpt-oss") || false; + const supportsThinkToggling = + selectedModel?.model.toLowerCase().startsWith("deepseek-v3.1") || false; + + useEffect(() => { + if (supportsThinkToggling && thinkEnabled && webSearchEnabled) { + setSettings({ WebSearchEnabled: false }); + } + }, [ + selectedModel?.model, + supportsThinkToggling, + thinkEnabled, + webSearchEnabled, + setSettings, + ]); + + const removeFile = (index: number) => { + setMessage((prev) => ({ + ...prev, + attachments: prev.attachments.filter((_, i) => i !== index), + })); + }; + + const removeFileError = (index: number) => { + setMessage((prev) => ({ + ...prev, + fileErrors: prev.fileErrors.filter((_, i) => i !== index), + })); + }; + + // Create stable callback for file handling + const handleFilesReceived = useCallback( + ( + files: Array<{ filename: string; data: Uint8Array; type?: string }>, + errors: Array<{ filename: string; error: string }> = [], + ) => { + if (files.length > 0) { + setFileUploadError(null); + + const newAttachments = files.map((file) => ({ + id: crypto.randomUUID(), + filename: file.filename, + data: file.data, + })); + + setMessage((prev) => ({ + ...prev, + attachments: [...prev.attachments, ...newAttachments], + })); + } + + // Add validation errors to form state + if (errors.length > 0) { + setMessage((prev) => ({ + ...prev, + fileErrors: [...prev.fileErrors, ...errors], + })); + } + }, + [], + ); + + useEffect(() => { + if (onFilesReceived) { + onFilesReceived(handleFilesReceived); + } + }, [onFilesReceived, handleFilesReceived]); + + // Determine if login banner should be shown + const shouldShowLoginBanner = + !isLoadingUser && + !isAuthenticated && + ((webSearchEnabled && supportsWebSearch) || + (selectedModel?.isCloud() && !airplaneMode)); + + // Determine which feature to highlight in the banner + const getActiveFeatureForBanner = () => { + if (!isAuthenticated) { + if (loginPromptFeature) return loginPromptFeature; + if (webSearchEnabled && selectedModel?.isCloud() && !airplaneMode) + return "webSearch"; + if (webSearchEnabled) return "webSearch"; + if (selectedModel?.isCloud() && !airplaneMode) return "turbo"; + } + return null; + }; + + const activeFeatureForBanner = getActiveFeatureForBanner(); + + const resetChatForm = () => { + setMessage({ + content: "", + attachments: [], + fileErrors: [], + }); + + if (textareaRef.current) { + textareaRef.current.style.height = "auto"; + } + }; + + // Clear loginPromptFeature when user becomes authenticated or no features are enabled + useEffect(() => { + if ( + isAuthenticated || + (!webSearchEnabled && !!selectedModel?.isCloud() && !airplaneMode) + ) { + setLoginPromptFeature(null); + } + }, [isAuthenticated, webSearchEnabled, selectedModel, airplaneMode]); + + // When entering edit mode, populate the composition with existing data + useEffect(() => { + if (!editingMessage) { + // Clear composition and reset textarea height when not editing + resetChatForm(); + return; + } + + const existingAttachments = + editingMessage.originalMessage?.attachments || []; + setMessage({ + content: editingMessage.content, + attachments: existingAttachments.map((att) => ({ + id: crypto.randomUUID(), + filename: att.filename, + // No data for existing files - backend will handle them + })), + fileErrors: [], + }); + }, [editingMessage]); + + // Focus and setup textarea when editing + useLayoutEffect(() => { + if (editingMessage && textareaRef.current) { + textareaRef.current.focus(); + textareaRef.current.style.transition = + "height 0.2s ease-out, opacity 0.3s ease-in"; + textareaRef.current.style.height = "auto"; + textareaRef.current.style.height = + Math.min(textareaRef.current.scrollHeight, 24 * 8) + "px"; + } + }, [editingMessage]); + + // Clear composition and reset textarea height when chatId changes + useEffect(() => { + resetChatForm(); + }, [chatId]); + + // Auto-focus textarea when autoFocus is true or when streaming completes (but not when editing) + useEffect(() => { + if ((autoFocus || !isStreaming) && textareaRef.current && !editingMessage) { + const timer = setTimeout( + () => { + textareaRef.current?.focus(); + }, + autoFocus ? 0 : 100, + ); + return () => clearTimeout(timer); + } + }, [autoFocus, isStreaming, editingMessage]); + + const focusChatFormInput = () => { + // Focus textarea after model selection or navigation + if (textareaRef.current) { + setTimeout(() => { + textareaRef.current?.focus(); + }, 100); + } + }; + + // Navigation helper function + const navigateToNextElement = useCallback( + (current: HTMLElement, direction: "next" | "prev") => { + const elements = [ + textareaRef, + modelSupportsThinkingLevels ? thinkingLevelButtonRef : thinkButtonRef, + webSearchButtonRef, + modelPickerRef, + submitButtonRef, + ] + .map((ref) => ref.current) + .filter(Boolean) as HTMLElement[]; + const index = elements.indexOf(current); + if (index === -1) return; + const nextIndex = + direction === "next" + ? (index + 1) % elements.length + : (index - 1 + elements.length) % elements.length; + elements[nextIndex].focus(); + }, + [], + ); + + // Focus textarea when navigating to a chat (when chatId changes) + useEffect(() => { + if (chatId !== "new") { + focusChatFormInput(); + } + }, [chatId]); + + // Global keyboard and paste event handlers + useEffect(() => { + const focusTextareaIfAppropriate = (target: HTMLElement) => { + if ( + !textareaRef.current || + textareaRef.current === document.activeElement + ) { + return; + } + + const isEditableTarget = + target.tagName === "INPUT" || + target.tagName === "TEXTAREA" || + target.contentEditable === "true" || + target.closest("input") || + target.closest("textarea") || + target.closest("[contenteditable='true']"); + + if (!isEditableTarget) { + textareaRef.current.focus(); + } + }; + + const handleKeyDown = (e: KeyboardEvent) => { + // Handle escape key for canceling + if (e.key === "Escape") { + e.preventDefault(); + if (editingMessage && onCancelEdit) { + handleCancelEdit(); + } else if (isStreaming) { + handleCancel(); + } + return; + } + + // Handle Tab navigation between controls + if (e.key === "Tab" && e.target !== textareaRef.current) { + const target = e.target as HTMLElement; + const focusableElements = [ + modelSupportsThinkingLevels + ? thinkingLevelButtonRef.current + : thinkButtonRef.current, + webSearchButtonRef.current, + modelPickerRef.current, + submitButtonRef.current, + ].filter(Boolean) as HTMLElement[]; + + if (focusableElements.includes(target)) { + e.preventDefault(); + if (e.shiftKey) { + navigateToNextElement(target, "prev"); + } else { + navigateToNextElement(target, "next"); + } + return; + } + } + + // Handle paste shortcuts + const isPasteShortcut = (e.ctrlKey || e.metaKey) && e.key === "v"; + if (isPasteShortcut) { + focusTextareaIfAppropriate(e.target as HTMLElement); + return; + } + + // Handle auto-focus when typing printable characters + const target = e.target as HTMLElement; + const isInInputField = + target.tagName === "INPUT" || + target.tagName === "TEXTAREA" || + target.contentEditable === "true"; + + if ( + !isInInputField && + e.key.length === 1 && + !e.ctrlKey && + !e.metaKey && + !e.altKey && + textareaRef.current + ) { + textareaRef.current.focus(); + } + }; + + const handlePaste = (e: ClipboardEvent) => { + focusTextareaIfAppropriate(e.target as HTMLElement); + }; + + window.addEventListener("keydown", handleKeyDown); + document.addEventListener("paste", handlePaste); + + return () => { + window.removeEventListener("keydown", handleKeyDown); + document.removeEventListener("paste", handlePaste); + }; + }, [isStreaming, editingMessage, onCancelEdit, navigateToNextElement]); + + const handleSubmit = async () => { + if (!message.content.trim() || isStreaming || isDownloading) return; + + // Check if cloud mode is enabled but user is not authenticated + if (shouldShowLoginBanner) { + return; + } + + // Prepare attachments for submission + const attachmentsToSend: FileAttachment[] = message.attachments.map( + (att) => ({ + filename: att.filename, + data: att.data || new Uint8Array(0), // Empty data for existing files + }), + ); + + const useWebSearch = supportsWebSearch && webSearchEnabled && !airplaneMode; + const useThink = modelSupportsThinkingLevels + ? thinkLevel + : supportsThinkToggling + ? thinkEnabled + : undefined; + + if (onSubmit) { + onSubmit(message.content, { + attachments: attachmentsToSend, + index: undefined, + webSearch: useWebSearch, + think: useThink, + }); + } else { + sendMessageMutation({ + message: message.content, + attachments: attachmentsToSend, + webSearch: useWebSearch, + think: useThink, + onChatEvent: (event) => { + if (event.eventName === "chat_created" && event.chatId) { + navigate({ + to: "/c/$chatId", + params: { + chatId: event.chatId, + }, + }); + } + }, + }); + } + + // Clear composition after successful submission + setMessage({ + content: "", + attachments: [], + fileErrors: [], + }); + + // Reset textarea height and refocus after submit + setTimeout(() => { + if (textareaRef.current) { + textareaRef.current.style.height = "auto"; + textareaRef.current.focus(); + } + }, 100); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + // Handle Enter to submit + if (e.key === "Enter" && !e.shiftKey && !isEditing) { + e.preventDefault(); + if (!isStreaming && !isDownloading) { + handleSubmit(); + } + return; + } + + // Handle Tab navigation + if (e.key === "Tab") { + e.preventDefault(); + const focusableElements = [ + modelSupportsThinkingLevels + ? thinkingLevelButtonRef.current + : thinkButtonRef.current, + webSearchButtonRef.current, + modelPickerRef.current, + submitButtonRef.current, + ].filter(Boolean); + + if (e.shiftKey) { + // Shift+Tab: focus last focusable element + const lastElement = focusableElements[focusableElements.length - 1]; + lastElement?.focus(); + } else { + // Tab: focus first focusable element + const firstElement = focusableElements[0]; + firstElement?.focus(); + } + return; + } + }; + + const handleCompositionStart = () => { + if (compositionEndTimeoutRef.current) { + window.clearTimeout(compositionEndTimeoutRef.current); + } + setIsEditing(true); + }; + + const handleCompositionEnd = () => { + // Add a small delay to handle the timing issue where Enter keydown + // fires immediately after composition end + compositionEndTimeoutRef.current = window.setTimeout(() => { + setIsEditing(false); + }, 10); + }; + + const handleCancel = () => { + cancelMessage(chatId); + }; + + const handleCancelEdit = () => { + // Clear composition and call parent callback + setMessage({ + content: "", + attachments: [], + fileErrors: [], + }); + + onCancelEdit?.(); + + // Focus the textarea after canceling edit mode + setTimeout(() => { + textareaRef.current?.focus(); + }, 0); + }; + + const handleFileInputChange = (e: React.ChangeEvent) => { + const files = e.target.files; + if (!files) return; + + Array.from(files).forEach((file) => { + const reader = new FileReader(); + reader.readAsDataURL(file); + }); + + // Reset file input + if (e.target) { + e.target.value = ""; + } + }; + + // Auto-resize textarea function + const handleTextareaChange = (e: React.ChangeEvent) => { + setMessage((prev) => ({ ...prev, content: e.target.value })); + + // Reset height to auto to get the correct scrollHeight, then cap at 8 lines + e.target.style.height = "auto"; + e.target.style.height = Math.min(e.target.scrollHeight, 24 * 8) + "px"; + }; + + const handleFilesUpload = async () => { + try { + setFileUploadError(null); + + const results = await window.webview?.selectMultipleFiles(); + if (results && results.length > 0) { + // Convert native dialog results to File objects + const files = results + .map((result: ImageData) => { + if (result.dataURL) { + // Convert dataURL back to File object + const base64Data = result.dataURL.split(",")[1]; + const mimeType = result.dataURL.split(";")[0].split(":")[1]; + const binaryString = atob(base64Data); + const bytes = new Uint8Array(binaryString.length); + for (let i = 0; i < binaryString.length; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + + const blob = new Blob([bytes], { type: mimeType }); + const file = new File([blob], result.filename, { + type: mimeType, + }); + return file; + } + return null; + }) + .filter(Boolean) as File[]; + + if (files.length > 0) { + const { validFiles, errors } = await processFiles(files, { + selectedModel, + hasVisionCapability, + }); + + // Send processed files and errors to the same handler as FileUpload + if (validFiles.length > 0 || errors.length > 0) { + handleFilesReceived(validFiles, errors); + } + } + } + } catch (error) { + console.error("Error selecting multiple files:", error); + + const errorEvent = new ErrorEvent({ + eventName: "error" as const, + error: + error instanceof Error ? error.message : "Failed to select files", + code: "file_selection_error", + details: + "An error occurred while trying to open the file selection dialog. Please try again.", + }); + + setFileUploadError(errorEvent); + } + }; + return ( +
+ {chatId === "new" && } + + {shouldShowLoginBanner && ( + { + // Disable the active features when dismissing + if (webSearchEnabled) setSettings({ WebSearchEnabled: false }); + setLoginPromptFeature(null); + }} + /> + )} + + {/* File upload error message */} + {fileUploadError && } +
+ {isDisabled && ( + // overlay to block interaction +
+ )} + {editingMessage && ( +
+

+ Press ESC to cancel editing +

+
+ )} + {(message.attachments.length > 0 || message.fileErrors.length > 0) && ( +
+ {message.attachments.map((attachment, index) => ( +
+ {isImageFile(attachment.filename) ? ( + + ) : ( + + + + )} + + {attachment.filename} + + +
+ ))} + {message.fileErrors.map((fileError, index) => ( +
+ + + + + {fileError.filename} + + + • {fileError.error} + + +
+ ))} +
+ )} + +
+