From d3b4b9970a5e2759fcfc6a632968ecc771ac6e77 Mon Sep 17 00:00:00 2001 From: Daniel Hiltgen Date: Tue, 4 Nov 2025 11:40:17 -0800 Subject: [PATCH] app: add code for macOS and Windows apps under 'app' (#12933) * app: add code for macOS and Windows apps under 'app' * app: add readme * app: windows and linux only for now * ci: fix ui CI validation --------- Co-authored-by: jmorganca --- .github/workflows/release.yaml | 167 +- .github/workflows/test.yaml | 77 +- CMakePresets.json | 23 +- Dockerfile | 14 +- app/.gitignore | 10 + app/README.md | 107 +- app/assets/assets.go | 2 + app/assets/background.png | Bin 0 -> 15741 bytes app/assets/tray.ico | Bin 91014 -> 118170 bytes app/assets/tray_upgrade.ico | Bin 92898 -> 118876 bytes app/auth/connect.go | 26 + app/cmd/app/AppDelegate.h | 7 + app/cmd/app/app.go | 478 + app/cmd/app/app_darwin.go | 269 + app/cmd/app/app_darwin.h | 43 + app/cmd/app/app_darwin.m | 1125 + app/cmd/app/app_windows.go | 439 + app/cmd/app/menu.h | 27 + app/cmd/app/webview.go | 528 + app/cmd/squirrel/Info.plist | 40 + app/darwin/Ollama.app/Contents/Info.plist | 51 + .../LaunchAgents/com.ollama.ollama.plist | 25 + .../Ollama.app/Contents/Resources/icon.icns | Bin 0 -> 223821 bytes .../Ollama.app/Contents/Resources/ollama.png | Bin 0 -> 374 bytes .../Contents/Resources/ollama@2x.png | Bin 0 -> 661 bytes .../Contents/Resources/ollamaDark.png | Bin 0 -> 363 bytes .../Contents/Resources/ollamaDark@2x.png | Bin 0 -> 745 bytes .../Contents/Resources/ollamaUpdate.png | Bin 0 -> 381 bytes .../Contents/Resources/ollamaUpdate@2x.png | Bin 0 -> 648 bytes .../Contents/Resources/ollamaUpdateDark.png | Bin 0 -> 412 bytes .../Resources/ollamaUpdateDark@2x.png | Bin 0 -> 771 bytes app/dialog/LICENSE | 15 + app/dialog/cocoa/dlg.h | 43 + app/dialog/cocoa/dlg.m | 195 + app/dialog/cocoa/dlg_darwin.go | 183 + app/dialog/dlgs.go | 182 + app/dialog/dlgs_darwin.go | 82 + app/dialog/dlgs_windows.go | 241 + app/dialog/util.go | 12 + app/format/field.go | 30 + app/format/field_test.go | 34 + app/lifecycle/getstarted_nonwindows.go | 9 - app/lifecycle/getstarted_windows.go | 43 - app/lifecycle/lifecycle.go | 94 - app/lifecycle/logging.go | 62 - app/lifecycle/logging_nonwindows.go | 9 - app/lifecycle/logging_test.go | 44 - app/lifecycle/logging_windows.go | 19 - app/lifecycle/paths.go | 84 - app/lifecycle/server.go | 186 - app/lifecycle/server_unix.go | 38 - app/lifecycle/server_windows.go | 91 - app/lifecycle/updater_nonwindows.go | 12 - app/lifecycle/updater_windows.go | 74 - app/logrotate/logrotate.go | 45 + app/logrotate/logrotate_test.go | 70 + app/main.go | 12 - app/ollama.iss | 202 +- app/ollama_welcome.ps1 | 8 - app/server/server.go | 357 + app/server/server_test.go | 249 + app/server/server_unix.go | 104 + app/server/server_windows.go | 149 + app/store/database.go | 1222 + app/store/database_test.go | 407 + app/store/image.go | 128 + app/store/migration_test.go | 231 + app/store/schema.sql | 61 + app/store/schema_test.go | 60 + app/store/store.go | 528 +- app/store/store_darwin.go | 13 - app/store/store_linux.go | 16 - app/store/store_test.go | 192 + app/store/store_windows.go | 11 - app/store/testdata/schema.sql | 61 + app/tools/browser.go | 863 + app/tools/browser_crawl.go | 136 + app/tools/browser_test.go | 147 + app/tools/browser_websearch.go | 143 + app/tools/tools.go | 122 + app/tools/web_fetch.go | 128 + app/tools/web_search.go | 145 + app/tray/commontray/types.go | 24 - app/tray/tray.go | 28 - app/tray/tray_nonwindows.go | 13 - app/tray/tray_windows.go | 10 - app/tray/wintray/eventloop.go | 181 - app/types/not/found.go | 28 + app/types/not/valids.go | 55 + app/types/not/valids_test.go | 43 + app/ui/app.go | 44 + app/ui/app/.gitignore | 30 + app/ui/app/.prettierignore | 1 + app/ui/app/.prettierrc | 6 + app/ui/app/codegen/gotypes.gen.ts | 611 + app/ui/app/eslint.config.js | 32 + app/ui/app/index.html | 189 + app/ui/app/package-lock.json | 11876 ++++ app/ui/app/package.json | 81 + app/ui/app/public/hello.png | Bin 0 -> 21771 bytes app/ui/app/src/api.ts | 405 + app/ui/app/src/components/Chat.tsx | 298 + app/ui/app/src/components/ChatForm.tsx | 984 + app/ui/app/src/components/ChatSidebar.tsx | 380 + app/ui/app/src/components/CopyButton.tsx | 95 + app/ui/app/src/components/DisplayLogin.tsx | 74 + app/ui/app/src/components/DisplayStale.tsx | 56 + app/ui/app/src/components/DisplayUpgrade.tsx | 44 + app/ui/app/src/components/Downloading.tsx | 56 + app/ui/app/src/components/ErrorMessage.tsx | 73 + app/ui/app/src/components/FileUpload.tsx | 251 + app/ui/app/src/components/ImageThumbnail.tsx | 131 + app/ui/app/src/components/Logo.tsx | 63 + app/ui/app/src/components/Message.stories.tsx | 419 + app/ui/app/src/components/Message.tsx | 990 + app/ui/app/src/components/MessageList.tsx | 168 + app/ui/app/src/components/ModelPicker.tsx | 364 + app/ui/app/src/components/Settings.tsx | 546 + .../StreamingMarkdownContent.stories.tsx | 614 + .../StreamingMarkdownContent.test.tsx | 522 + .../components/StreamingMarkdownContent.tsx | 327 + app/ui/app/src/components/ThinkButton.tsx | 161 + app/ui/app/src/components/Thinking.tsx | 159 + app/ui/app/src/components/WebSearchButton.tsx | 40 + app/ui/app/src/components/layout/layout.tsx | 81 + app/ui/app/src/components/ui/badge.tsx | 97 + app/ui/app/src/components/ui/button.tsx | 219 + app/ui/app/src/components/ui/display.tsx | 120 + app/ui/app/src/components/ui/fieldset.tsx | 121 + app/ui/app/src/components/ui/input.tsx | 104 + app/ui/app/src/components/ui/link.tsx | 17 + app/ui/app/src/components/ui/slider.tsx | 113 + app/ui/app/src/components/ui/switch.tsx | 198 + app/ui/app/src/components/ui/text.tsx | 60 + app/ui/app/src/contexts/StreamingContext.tsx | 68 + app/ui/app/src/hooks/useChats.ts | 743 + app/ui/app/src/hooks/useDeleteChat.ts | 24 + app/ui/app/src/hooks/useDownloadModel.ts | 114 + app/ui/app/src/hooks/useHealth.ts | 22 + app/ui/app/src/hooks/useMessageAutoscroll.ts | 347 + app/ui/app/src/hooks/useModelCapabilities.ts | 22 + app/ui/app/src/hooks/useModels.ts | 55 + app/ui/app/src/hooks/useQueryBatcher.ts | 130 + app/ui/app/src/hooks/useRenameChat.ts | 20 + app/ui/app/src/hooks/useSelectedModel.ts | 201 + app/ui/app/src/hooks/useSettings.ts | 83 + app/ui/app/src/hooks/useUser.ts | 67 + app/ui/app/src/index.css | 817 + app/ui/app/src/lib/ollama-client.ts | 15 + app/ui/app/src/main.tsx | 73 + app/ui/app/src/routeTree.gen.ts | 134 + app/ui/app/src/routes/__root.tsx | 24 + app/ui/app/src/routes/c.$chatId.tsx | 71 + app/ui/app/src/routes/index.tsx | 13 + app/ui/app/src/routes/settings.tsx | 6 + app/ui/app/src/types/webview.d.ts | 49 + app/ui/app/src/util/jsonl-parsing.ts | 57 + app/ui/app/src/utils/fileValidation.ts | 154 + app/ui/app/src/utils/imageUtils.ts | 4 + app/ui/app/src/utils/mergeModels.test.ts | 128 + app/ui/app/src/utils/mergeModels.ts | 101 + .../app/src/utils/processStreamingMarkdown.ts | 24 + app/ui/app/src/utils/remarkCitationParser.ts | 103 + .../app/src/utils/remarkStreamingMarkdown.ts | 447 + app/ui/app/src/utils/vram.test.ts | 121 + app/ui/app/src/utils/vram.ts | 29 + app/ui/app/src/vite-env.d.ts | 1 + app/ui/app/tailwind.config.js | 18 + app/ui/app/tsconfig.app.json | 34 + app/ui/app/tsconfig.json | 8 + app/ui/app/tsconfig.node.json | 25 + app/ui/app/tsconfig.stories.json | 8 + app/ui/app/vite.config.ts | 67 + app/ui/app/vitest.config.ts | 21 + app/ui/app/vitest.shims.d.ts | 1 + app/ui/extract.go | 84 + app/ui/responses/types.go | 156 + app/ui/ui.go | 1824 + app/ui/ui_test.go | 423 + app/{lifecycle => updater}/updater.go | 114 +- app/updater/updater_darwin.go | 446 + app/updater/updater_darwin.h | 16 + app/updater/updater_darwin.m | 182 + app/updater/updater_darwin_test.go | 326 + app/updater/updater_test.go | 101 + app/updater/updater_windows.go | 238 + app/updater/updater_windows_test.go | 16 + app/version/version.go | 5 + app/webview/.gitignore | 15 + app/webview/README.md | 5 + app/webview/WebView2.h | 57256 ++++++++++++++++ app/webview/glue.c | 36 + app/webview/webview.cc | 1 + app/webview/webview.go | 368 + app/webview/webview.h | 3997 ++ app/wintray/eventloop.go | 331 + app/{tray => }/wintray/menus.go | 33 +- app/{tray => }/wintray/messages.go | 2 + app/{tray => }/wintray/notifyicon.go | 0 app/{tray => }/wintray/tray.go | 80 +- app/{tray => }/wintray/w32api.go | 21 + app/{tray => }/wintray/winclass.go | 0 discover/runner_test.go | 7 +- go.mod | 12 +- go.sum | 14 +- scripts/.this-is-the-create-dmg-repo | 2 + scripts/build_darwin.sh | 132 +- scripts/build_windows.ps1 | 295 +- scripts/create-dmg.sh | 636 + scripts/support/eula-resources-template.xml | 105 + scripts/support/template.applescript | 74 + server/sched_test.go | 4 +- 212 files changed, 102976 insertions(+), 1482 deletions(-) create mode 100644 app/assets/background.png create mode 100644 app/auth/connect.go create mode 100644 app/cmd/app/AppDelegate.h create mode 100644 app/cmd/app/app.go create mode 100644 app/cmd/app/app_darwin.go create mode 100644 app/cmd/app/app_darwin.h create mode 100644 app/cmd/app/app_darwin.m create mode 100644 app/cmd/app/app_windows.go create mode 100644 app/cmd/app/menu.h create mode 100644 app/cmd/app/webview.go create mode 100644 app/cmd/squirrel/Info.plist create mode 100644 app/darwin/Ollama.app/Contents/Info.plist create mode 100644 app/darwin/Ollama.app/Contents/Library/LaunchAgents/com.ollama.ollama.plist create mode 100644 app/darwin/Ollama.app/Contents/Resources/icon.icns create mode 100644 app/darwin/Ollama.app/Contents/Resources/ollama.png create mode 100644 app/darwin/Ollama.app/Contents/Resources/ollama@2x.png create mode 100644 app/darwin/Ollama.app/Contents/Resources/ollamaDark.png create mode 100644 app/darwin/Ollama.app/Contents/Resources/ollamaDark@2x.png create mode 100644 app/darwin/Ollama.app/Contents/Resources/ollamaUpdate.png create mode 100644 app/darwin/Ollama.app/Contents/Resources/ollamaUpdate@2x.png create mode 100644 app/darwin/Ollama.app/Contents/Resources/ollamaUpdateDark.png create mode 100644 app/darwin/Ollama.app/Contents/Resources/ollamaUpdateDark@2x.png create mode 100644 app/dialog/LICENSE create mode 100644 app/dialog/cocoa/dlg.h create mode 100644 app/dialog/cocoa/dlg.m create mode 100644 app/dialog/cocoa/dlg_darwin.go create mode 100644 app/dialog/dlgs.go create mode 100644 app/dialog/dlgs_darwin.go create mode 100644 app/dialog/dlgs_windows.go create mode 100644 app/dialog/util.go create mode 100644 app/format/field.go create mode 100644 app/format/field_test.go delete mode 100644 app/lifecycle/getstarted_nonwindows.go delete mode 100644 app/lifecycle/getstarted_windows.go delete mode 100644 app/lifecycle/lifecycle.go delete mode 100644 app/lifecycle/logging.go delete mode 100644 app/lifecycle/logging_nonwindows.go delete mode 100644 app/lifecycle/logging_test.go delete mode 100644 app/lifecycle/logging_windows.go delete mode 100644 app/lifecycle/paths.go delete mode 100644 app/lifecycle/server.go delete mode 100644 app/lifecycle/server_unix.go delete mode 100644 app/lifecycle/server_windows.go delete mode 100644 app/lifecycle/updater_nonwindows.go delete mode 100644 app/lifecycle/updater_windows.go create mode 100644 app/logrotate/logrotate.go create mode 100644 app/logrotate/logrotate_test.go delete mode 100644 app/main.go delete mode 100644 app/ollama_welcome.ps1 create mode 100644 app/server/server.go create mode 100644 app/server/server_test.go create mode 100644 app/server/server_unix.go create mode 100644 app/server/server_windows.go create mode 100644 app/store/database.go create mode 100644 app/store/database_test.go create mode 100644 app/store/image.go create mode 100644 app/store/migration_test.go create mode 100644 app/store/schema.sql create mode 100644 app/store/schema_test.go delete mode 100644 app/store/store_darwin.go delete mode 100644 app/store/store_linux.go create mode 100644 app/store/store_test.go delete mode 100644 app/store/store_windows.go create mode 100644 app/store/testdata/schema.sql create mode 100644 app/tools/browser.go create mode 100644 app/tools/browser_crawl.go create mode 100644 app/tools/browser_test.go create mode 100644 app/tools/browser_websearch.go create mode 100644 app/tools/tools.go create mode 100644 app/tools/web_fetch.go create mode 100644 app/tools/web_search.go delete mode 100644 app/tray/commontray/types.go delete mode 100644 app/tray/tray.go delete mode 100644 app/tray/tray_nonwindows.go delete mode 100644 app/tray/tray_windows.go delete mode 100644 app/tray/wintray/eventloop.go create mode 100644 app/types/not/found.go create mode 100644 app/types/not/valids.go create mode 100644 app/types/not/valids_test.go create mode 100644 app/ui/app.go create mode 100644 app/ui/app/.gitignore create mode 100644 app/ui/app/.prettierignore create mode 100644 app/ui/app/.prettierrc create mode 100644 app/ui/app/codegen/gotypes.gen.ts create mode 100644 app/ui/app/eslint.config.js create mode 100644 app/ui/app/index.html create mode 100644 app/ui/app/package-lock.json create mode 100644 app/ui/app/package.json create mode 100644 app/ui/app/public/hello.png create mode 100644 app/ui/app/src/api.ts create mode 100644 app/ui/app/src/components/Chat.tsx create mode 100644 app/ui/app/src/components/ChatForm.tsx create mode 100644 app/ui/app/src/components/ChatSidebar.tsx create mode 100644 app/ui/app/src/components/CopyButton.tsx create mode 100644 app/ui/app/src/components/DisplayLogin.tsx create mode 100644 app/ui/app/src/components/DisplayStale.tsx create mode 100644 app/ui/app/src/components/DisplayUpgrade.tsx create mode 100644 app/ui/app/src/components/Downloading.tsx create mode 100644 app/ui/app/src/components/ErrorMessage.tsx create mode 100644 app/ui/app/src/components/FileUpload.tsx create mode 100644 app/ui/app/src/components/ImageThumbnail.tsx create mode 100644 app/ui/app/src/components/Logo.tsx create mode 100644 app/ui/app/src/components/Message.stories.tsx create mode 100644 app/ui/app/src/components/Message.tsx create mode 100644 app/ui/app/src/components/MessageList.tsx create mode 100644 app/ui/app/src/components/ModelPicker.tsx create mode 100644 app/ui/app/src/components/Settings.tsx create mode 100644 app/ui/app/src/components/StreamingMarkdownContent.stories.tsx create mode 100644 app/ui/app/src/components/StreamingMarkdownContent.test.tsx create mode 100644 app/ui/app/src/components/StreamingMarkdownContent.tsx create mode 100644 app/ui/app/src/components/ThinkButton.tsx create mode 100644 app/ui/app/src/components/Thinking.tsx create mode 100644 app/ui/app/src/components/WebSearchButton.tsx create mode 100644 app/ui/app/src/components/layout/layout.tsx create mode 100644 app/ui/app/src/components/ui/badge.tsx create mode 100644 app/ui/app/src/components/ui/button.tsx create mode 100644 app/ui/app/src/components/ui/display.tsx create mode 100644 app/ui/app/src/components/ui/fieldset.tsx create mode 100644 app/ui/app/src/components/ui/input.tsx create mode 100644 app/ui/app/src/components/ui/link.tsx create mode 100644 app/ui/app/src/components/ui/slider.tsx create mode 100644 app/ui/app/src/components/ui/switch.tsx create mode 100644 app/ui/app/src/components/ui/text.tsx create mode 100644 app/ui/app/src/contexts/StreamingContext.tsx create mode 100644 app/ui/app/src/hooks/useChats.ts create mode 100644 app/ui/app/src/hooks/useDeleteChat.ts create mode 100644 app/ui/app/src/hooks/useDownloadModel.ts create mode 100644 app/ui/app/src/hooks/useHealth.ts create mode 100644 app/ui/app/src/hooks/useMessageAutoscroll.ts create mode 100644 app/ui/app/src/hooks/useModelCapabilities.ts create mode 100644 app/ui/app/src/hooks/useModels.ts create mode 100644 app/ui/app/src/hooks/useQueryBatcher.ts create mode 100644 app/ui/app/src/hooks/useRenameChat.ts create mode 100644 app/ui/app/src/hooks/useSelectedModel.ts create mode 100644 app/ui/app/src/hooks/useSettings.ts create mode 100644 app/ui/app/src/hooks/useUser.ts create mode 100644 app/ui/app/src/index.css create mode 100644 app/ui/app/src/lib/ollama-client.ts create mode 100644 app/ui/app/src/main.tsx create mode 100644 app/ui/app/src/routeTree.gen.ts create mode 100644 app/ui/app/src/routes/__root.tsx create mode 100644 app/ui/app/src/routes/c.$chatId.tsx create mode 100644 app/ui/app/src/routes/index.tsx create mode 100644 app/ui/app/src/routes/settings.tsx create mode 100644 app/ui/app/src/types/webview.d.ts create mode 100644 app/ui/app/src/util/jsonl-parsing.ts create mode 100644 app/ui/app/src/utils/fileValidation.ts create mode 100644 app/ui/app/src/utils/imageUtils.ts create mode 100644 app/ui/app/src/utils/mergeModels.test.ts create mode 100644 app/ui/app/src/utils/mergeModels.ts create mode 100644 app/ui/app/src/utils/processStreamingMarkdown.ts create mode 100644 app/ui/app/src/utils/remarkCitationParser.ts create mode 100644 app/ui/app/src/utils/remarkStreamingMarkdown.ts create mode 100644 app/ui/app/src/utils/vram.test.ts create mode 100644 app/ui/app/src/utils/vram.ts create mode 100644 app/ui/app/src/vite-env.d.ts create mode 100644 app/ui/app/tailwind.config.js create mode 100644 app/ui/app/tsconfig.app.json create mode 100644 app/ui/app/tsconfig.json create mode 100644 app/ui/app/tsconfig.node.json create mode 100644 app/ui/app/tsconfig.stories.json create mode 100644 app/ui/app/vite.config.ts create mode 100644 app/ui/app/vitest.config.ts create mode 100644 app/ui/app/vitest.shims.d.ts create mode 100644 app/ui/extract.go create mode 100644 app/ui/responses/types.go create mode 100644 app/ui/ui.go create mode 100644 app/ui/ui_test.go rename app/{lifecycle => updater}/updater.go (64%) create mode 100644 app/updater/updater_darwin.go create mode 100644 app/updater/updater_darwin.h create mode 100644 app/updater/updater_darwin.m create mode 100644 app/updater/updater_darwin_test.go create mode 100644 app/updater/updater_test.go create mode 100644 app/updater/updater_windows.go create mode 100644 app/updater/updater_windows_test.go create mode 100644 app/version/version.go create mode 100644 app/webview/.gitignore create mode 100644 app/webview/README.md create mode 100644 app/webview/WebView2.h create mode 100644 app/webview/glue.c create mode 100644 app/webview/webview.cc create mode 100644 app/webview/webview.go create mode 100644 app/webview/webview.h create mode 100644 app/wintray/eventloop.go rename app/{tray => }/wintray/menus.go (63%) rename app/{tray => }/wintray/messages.go (83%) rename app/{tray => }/wintray/notifyicon.go (100%) rename app/{tray => }/wintray/tray.go (89%) rename app/{tray => }/wintray/w32api.go (82%) rename app/{tray => }/wintray/winclass.go (100%) create mode 100644 scripts/.this-is-the-create-dmg-repo create mode 100755 scripts/create-dmg.sh create mode 100644 scripts/support/eula-resources-template.xml create mode 100644 scripts/support/template.applescript 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 0000000000000000000000000000000000000000..bbfc5879ffbc4ce92ceac7a9c51463b27d6601c3 GIT binary patch literal 15741 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 literal 0 HcmV?d00001 diff --git a/app/assets/tray.ico b/app/assets/tray.ico index e63616c5738a8803c6c9f04ccfd4ab00e22cc88d..578669824a89dd74f7571d7fcb2ddc0b090800e7 100644 GIT binary patch literal 118170 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 diff --git a/app/assets/tray_upgrade.ico b/app/assets/tray_upgrade.ico index d208305186492ac3bb4e35de40c42775d15443b2..4cb604c9d452953bf7fd66e8005ac20e1c045613 100644 GIT binary patch literal 118876 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 AsQ>@~ literal 92898 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 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 0000000000000000000000000000000000000000..e25734dc969ac85db39af9b7792f9203e4aea254 GIT binary patch literal 223821 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

1A}BiiyJQmRUUuWpNa+N)OclrK-{He7#+ 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

*|e_Fhb?!}n6-O+hEljEevz%jKv7z~_Wg7F= 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 X9GO2hh4jRDZ1|3o)W69;<>vnYg@iTQ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..8daa976076cf66eb39cc12d4ebf46597e4e447e0 GIT binary patch literal 374 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`oCO|{#S9FrogmDZn=$tnP>``W z$lZxy-8q?;Kn_c~qpu?a!^VE@KZ&eBIc`rE$B+uf-pPAQ4><_5u!?u!D>RaNPB~x_LC5`*4e$Bq#IJ-=A{hmc$8LYhbPO#+Z=bX*1ye}1fSYBWJ zd|b0WYx}QDAs_d4>6$6jKVc~k3SS;j6>-u2&c>U+mnTh~uDD34;`~Fs-rPL%iMM5P z0_MKfPvkd$JEdsH(z_-bZeR1d9)GRAL$b`$s$$JS8#CtM&Mj{&UjKi{lhkbWY=K?( R4qz}bc)I$ztaD0e0szDXmG1xm literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..d226d0f16752988df9f69e0cd5f728e3b3073bd3 GIT binary patch literal 661 zcmV;G0&4wU(%bOYD`m;gFKVS=_3Buvn3P@;?8#dYn*`Mf)wkR!>m z^>G2Z@lw-Pc`8J=sOc3WA&!m?JrA&1VFU^#Thl>lo?s(-Sr3~aESeL%tNf<2?P2rL zWpj-|dz_wRGgl;|3AT5JweRm(71_ zdZ_72=|R^f#S%^azR={fKzCKMP71aAQaQ%}JmIvR=(r0-z;1<};N}N4HcBb&G*{hB zZ5hM@i7;sUH^X@Jo(Gce)Tx(>6?&wCIUF>-A`o8(1ZpTW2Mn5)a@#OdxpW-LnpIf_ z&bPOrK|8!?o0L*H{l?pz1u4kU=5(vNJRRGOw`d(d8(r%Jof8&q3-WL2cRxidXHy1{ zU+^0Rhsm=Sgls6M;6o$wtI7=KLv;*JubXZCjszIQ+s3_S0jC|2ibpN{;9@Eg+kD7jz?mVs|pzrEn}iVd1{#O8jEwstzfYK_eSUb8>% zw}}7nm^(TrxC$s2YH)+gV5*3+LtpAZQgQ1pUSl{yHPRcih-3eBV?lo<5(4;DzQlB3 zVeV6uE!xR9#Vr`LMQc)x4t)(SfN#mRufaE_16$krA3`-ir6~j8{|Q-207iL<>9A9N vhlBuFc%ysVLDbjmf^yQYmqgIlpa0<>rgXaVm4SF+00000NkvXXu0mjfjN>5O literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..34a8f898e0df314da42d0b47e2b0cf1fa7ce7c04 GIT binary patch literal 363 zcmV-x0hIoUP)j4$7WXb+ka-`frSj^*4hab2=kL-V- zsF}z+IFUWpOOwo5TTGDq*JzRQ6ZYgj9_@fExldX<;F~<3;0Dy}Osn5Vwu}G(002ov JPDHLkV1ks5nF;^^ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..f2436e4d10a6416545e7a76d5c6e448c9b1a99bd GIT binary patch literal 745 zcmV0(TIs}Nb()#44G_Z?(XDHKKL=SvuF04Ju`b|OUPFz*-!2wPUp!M@(6XR zN+!sTh&in~=S!a9{G9u{NwfBkptw$IyPCA1jsTjSek+R0&DPdhh6_+zO-_G9=3WC? zuWcVp%B{9J{gtSpybSzv`n}GaGvot#LLQPqa@w)I>GZ#$4#^rGf?3B#9lHwiwmvR9 zL2e=kNx7DTdB|5AIZW1(W#mAP{S7is-jYk?6=o@M){XGn7Ucr+m)fga37lK5AX4oK zom-A0La*l_fO|eizD>B6e9VjuVdie`C(p=In6G4~b59(8CTlWtmXjaI(Pm7K{G5)T z(Ca%epO{FBOoR3%hyk@Zs$&;3&njn~`F3aa0O{9um|PjhW}koMA+$8~#tJ4d$e? zsr^g-U}~w#bD>8ISQ>;57MhQ##sJ_C0(iB z{yhS}W}yr$gef$sBwyBOOaProE2$A3hzMX8(yJ0#^jd<50IEo@ bH@?A15o0&Kjl&Hi00000NkvXXu0mjfI5kkJ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..2e9aab35cfa75be7f42436ca6303148a378a6a5c GIT binary patch literal 381 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`oCO|{#S9FrogmDZn=$tnP>``W z$lZxy-8q?;Kn_c~qpu?a!^VE@KZ&eBIU!FM$B+ufsX@M%4mt?r-cRorK5>apJt4DQ z_=MmchiTLD1fsQ{vfQ}YD%>Jmz#KPQ{Hi49)6l(M&%d7kQqu1s$2gD%>Dj_H#7U-#LlVp z!3hq9=O%|;Iqwsu%M|^HttMdk-9u*sCaXQVw(P%cR@UoT#rckZss$C9OLx>Reyt|t zudNfn*1P+2!yVCcIlt9Ide^RB`$;j(e8PV~x8?9^d&V Z>>a)iYiHLkYXOE9gQu&X%Q~loCIBcfm}LL} literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..471fe130d59f624dcc541dc67c10f9f5c64e2886 GIT binary patch literal 648 zcmV;30(bq1P)eu{2p%CDkWN50Xf~i5G#iu++6~%HP?(@FL9#)!0Yn$+;<$GFNP+K8XY5+G zVm)jCKbuZ2+=fDP>rS?q2`%)k$vQ!&!VGGbypt=-B0z`cWf3}7a`a!Y^9Y^2 zPv;446t`d8esY^}yW)jg{_UjcoHU)4HlAca%WcCg`Pe*CGOE=2tHQ#7WJ12ksY0;1%O^l7)TL7QG6Q$mUm8RxBauwBWfJ z>xvO81fnqHZ-)6q*8}o*Xw?g)LWC>Che_4T0j+g~C5CKuK%q}5Jq)Dc?;ORobRv zKrykd6DcNZ!wA}tzCtb<>TkI(#_V~H!uYr)t#=4O!JRhxF%welgaYEM8 zi8mGHN##RwUBt!NFmx1%Qx6g!E!=O0U>+R{OCbI_HUmDmE%8qVNQzPM_>)txK|x3X zqmV|4+bpq3X;YTV=ZJI^?5S!#@03k>J!M@4@)C;LoCx_3XpRxyr{10?-s3$;lrftV iQ+L4}BWUxRd*TO_G}T9HSu~9R0000n*7-cPt5@kV2I z0KT~z9ZLdTfd|m#&4v+p2KSPnoHzBMkDpBjrwI&VUi61s>iL%IFKB=&cw`AV&;y@5 zbE!>qWBXhupaxptU7{XjK^t^leK~#7FwU%9o zjX@a4|2~U+tXenR9IcWf+6x!7Ik{9m?i9t6xDxJ^i`@$)S8QAfhf?n4qkQZ+No{SE zE2YT0YzR4QSWDsmG|%abF|+f|dC&gqS2Od@GxN+mGtWCfTs`C-nTIqTBm-!ZZj(Rc zNP^95lBrz@ZP1twR!y+ksy4!bSm)`H?@P#hEw3Z9kWXc+ygb%9=EMT4JeMqWzOU2? zJDm2_R(UZBIx%6DGxAQ$JIN?{3KOPJqyn>ZqMMf87qmgUVYalf+CDVn?edZ=(tceq zyR<4c!8F7r+qqu{n?ZI&`adA=$y;)V+(1DcCEpNUc^)34z%}hFIC&^cxwWi~4m3E8 zL{+B12?=Y5Z?e!8?RM&Q^22F+MXo9}&ls6gmps#h1bskr1rK4IEs_XLHo;umD%l8g%`$}>%aK;@)f=*c8 zODIl~N4k;{`KsRr$#wEZ>jS#5jGQ7v);N+k<{&|xYUCa+Ny}%g`g)kd_FU_-TMNl$ zkL~f$WT9|;B;%fT$(UnS{g6)BrFA(=atj(cp+>HdCrXlb!GF#Ab%JMB+c=tGYlQ0( z60&4yC$qr=9c*?!*?PJ;~g6LJj}_%3WoMgi-Et%Jr;z#*q9_NBBCI5Yb?w6GBH$ zN9x)h)%_Nn2)!YDTX8^iu>~iD@+6Lk1KWKc{{lxBNyKB}Q$qj%002ovPDHLkV1fX) BTBra3 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..fd04d9d0a28b93dedbd261d0b65b5b5885f7bd1d GIT binary patch literal 21771 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 Lbgtag>%sp3eSbZq literal 0 HcmV?d00001 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} + + +
+ ))} +
+ )} + +
+