From 83537993d7ceb504ae552a8045867870d342aa27 Mon Sep 17 00:00:00 2001 From: Daniel Hiltgen Date: Fri, 31 Oct 2025 09:54:25 -0700 Subject: [PATCH 01/37] logs: catch rocm errors (#12888) This will help bubble up more crash errors --- llm/status.go | 1 + 1 file changed, 1 insertion(+) diff --git a/llm/status.go b/llm/status.go index 80f44e65..fdb94954 100644 --- a/llm/status.go +++ b/llm/status.go @@ -23,6 +23,7 @@ func NewStatusWriter(out *os.File) *StatusWriter { var errorPrefixes = []string{ "error:", "CUDA error", + "ROCm error", "cudaMalloc failed", "\"ERR\"", "error loading model", From 3bee3af6ed6acaeece2f4ffe9a5093c23d26f989 Mon Sep 17 00:00:00 2001 From: Daniel Hiltgen Date: Fri, 31 Oct 2025 14:37:29 -0700 Subject: [PATCH 02/37] cpu: always ensure LibOllamaPath included (#12890) In CPU only setups the LibOllamaPath was omitted causing us not to load the ggml-cpu-XXX libraries during inference. --- discover/runner.go | 12 ++++++------ ml/device.go | 2 +- {discover => ml}/path.go | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) rename {discover => ml}/path.go (98%) diff --git a/discover/runner.go b/discover/runner.go index caaef222..9ae5b3ff 100644 --- a/discover/runner.go +++ b/discover/runner.go @@ -53,7 +53,7 @@ func GPUDevices(ctx context.Context, runners []ml.FilteredRunnerDiscovery) []ml. if eval, err := filepath.EvalSymlinks(exe); err == nil { exe = eval } - files, err := filepath.Glob(filepath.Join(LibOllamaPath, "*", "*ggml-*")) + files, err := filepath.Glob(filepath.Join(ml.LibOllamaPath, "*", "*ggml-*")) if err != nil { slog.Debug("unable to lookup runner library directories", "error", err) } @@ -64,7 +64,7 @@ func GPUDevices(ctx context.Context, runners []ml.FilteredRunnerDiscovery) []ml. // Our current packaging model places ggml-hip in the main directory // but keeps rocm in an isolated directory. We have to add it to // the [LD_LIBRARY_]PATH so ggml-hip will load properly - rocmDir = filepath.Join(LibOllamaPath, "rocm") + rocmDir = filepath.Join(ml.LibOllamaPath, "rocm") if _, err := os.Stat(rocmDir); err != nil { rocmDir = "" } @@ -95,9 +95,9 @@ func GPUDevices(ctx context.Context, runners []ml.FilteredRunnerDiscovery) []ml. } } if dir == "" { - dirs = []string{LibOllamaPath} + dirs = []string{ml.LibOllamaPath} } else { - dirs = []string{LibOllamaPath, dir} + dirs = []string{ml.LibOllamaPath, dir} } // ROCm can take a long time on some systems, so give it more time before giving up @@ -249,7 +249,7 @@ func GPUDevices(ctx context.Context, runners []ml.FilteredRunnerDiscovery) []ml. libDirs = make(map[string]struct{}) for _, dev := range devices { dir := dev.LibraryPath[len(dev.LibraryPath)-1] - if dir != LibOllamaPath { + if dir != ml.LibOllamaPath { libDirs[dir] = struct{}{} } } @@ -339,7 +339,7 @@ func GPUDevices(ctx context.Context, runners []ml.FilteredRunnerDiscovery) []ml. devFilter := ml.GetVisibleDevicesEnv(devices) for dir := range libDirs { - updatedDevices := bootstrapDevices(ctx, []string{LibOllamaPath, dir}, devFilter) + updatedDevices := bootstrapDevices(ctx, []string{ml.LibOllamaPath, dir}, devFilter) for _, u := range updatedDevices { for i := range devices { if u.DeviceID == devices[i].DeviceID && u.PCIID == devices[i].PCIID { diff --git a/ml/device.go b/ml/device.go index 57c3976b..1fbe365e 100644 --- a/ml/device.go +++ b/ml/device.go @@ -361,7 +361,7 @@ func ByLibrary(l []DeviceInfo) [][]DeviceInfo { } func LibraryPaths(l []DeviceInfo) []string { - var gpuLibs []string + gpuLibs := []string{LibOllamaPath} for _, gpu := range l { for _, dir := range gpu.LibraryPath { needed := true diff --git a/discover/path.go b/ml/path.go similarity index 98% rename from discover/path.go rename to ml/path.go index 68e63009..ac93af40 100644 --- a/discover/path.go +++ b/ml/path.go @@ -1,4 +1,4 @@ -package discover +package ml import ( "os" From 392a270261dfb1d1cee1de3713836b503a7526ce Mon Sep 17 00:00:00 2001 From: Jesse Gross Date: Fri, 31 Oct 2025 14:16:20 -0700 Subject: [PATCH 03/37] ggml: Avoid cudaMemsetAsync during memory fitting We pass invalid pointers when we check the size of the required compute graph before fitting. Some CUDA APIs validate these pointers but we can just skip them during this phase. cudaMemsetAsync is one of these that we weren't skipping but never took the code path that used it. Now that we have enabled op_offload, we can hit it in memory pressured situations. --- llama/patches/0022-ggml-No-alloc-mode.patch | 26 +++++++++++++------ ml/backend/ggml/ggml/src/ggml-cuda/common.cuh | 10 +++++++ 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/llama/patches/0022-ggml-No-alloc-mode.patch b/llama/patches/0022-ggml-No-alloc-mode.patch index d03c6c84..6e2599b3 100644 --- a/llama/patches/0022-ggml-No-alloc-mode.patch +++ b/llama/patches/0022-ggml-No-alloc-mode.patch @@ -11,9 +11,9 @@ must be recreated with no-alloc set to false before loading data. ggml/include/ggml-backend.h | 1 + ggml/src/ggml-backend-impl.h | 16 +++ ggml/src/ggml-backend.cpp | 72 ++++++++++- - ggml/src/ggml-cuda/common.cuh | 48 ++++++- + ggml/src/ggml-cuda/common.cuh | 58 ++++++++- ggml/src/ggml-cuda/ggml-cuda.cu | 217 ++++++++++++++++++++++++++------ - 5 files changed, 310 insertions(+), 44 deletions(-) + 5 files changed, 320 insertions(+), 44 deletions(-) diff --git a/ggml/include/ggml-backend.h b/ggml/include/ggml-backend.h index 2763f2bd6..b3b5b356a 100644 @@ -219,10 +219,10 @@ index 41eef3b5f..c81a2e48a 100644 void ggml_backend_sched_set_tensor_backend(ggml_backend_sched_t sched, struct ggml_tensor * node, ggml_backend_t backend) { diff --git a/ggml/src/ggml-cuda/common.cuh b/ggml/src/ggml-cuda/common.cuh -index e0abde542..28d6bcd71 100644 +index e0abde542..e98044bd8 100644 --- a/ggml/src/ggml-cuda/common.cuh +++ b/ggml/src/ggml-cuda/common.cuh -@@ -35,6 +35,31 @@ +@@ -35,6 +35,41 @@ #include "vendors/cuda.h" #endif // defined(GGML_USE_HIP) @@ -246,15 +246,25 @@ index e0abde542..28d6bcd71 100644 + } +} + ++static cudaError_t cudaMemsetAsyncReserve ( void* devPtr, int value, size_t count, cudaStream_t stream = 0 ) { ++ if (!reserving_graph) { ++ return cudaMemsetAsync(devPtr, value, count, stream); ++ } else { ++ return cudaSuccess; ++ } ++} ++ +#undef cudaMemcpyAsync +#define cudaMemcpyAsync cudaMemcpyAsyncReserve +#undef cudaMemcpy2DAsync +#define cudaMemcpy2DAsync cudaMemcpy2DAsyncReserve ++#undef cudaMemsetAsync ++#define cudaMemsetAsync cudaMemsetAsyncReserve + #define STRINGIZE_IMPL(...) #__VA_ARGS__ #define STRINGIZE(...) STRINGIZE_IMPL(__VA_ARGS__) -@@ -856,6 +881,9 @@ struct ggml_cuda_pool { +@@ -856,6 +891,9 @@ struct ggml_cuda_pool { virtual void * alloc(size_t size, size_t * actual_size) = 0; virtual void free(void * ptr, size_t size) = 0; @@ -264,7 +274,7 @@ index e0abde542..28d6bcd71 100644 }; template -@@ -999,11 +1027,11 @@ struct ggml_backend_cuda_context { +@@ -999,11 +1037,11 @@ struct ggml_backend_cuda_context { // pool std::unique_ptr pools[GGML_CUDA_MAX_DEVICES]; @@ -278,7 +288,7 @@ index e0abde542..28d6bcd71 100644 } return *pools[device]; } -@@ -1011,4 +1039,20 @@ struct ggml_backend_cuda_context { +@@ -1011,4 +1049,20 @@ struct ggml_backend_cuda_context { ggml_cuda_pool & pool() { return pool(device); } @@ -300,7 +310,7 @@ index e0abde542..28d6bcd71 100644 + } }; diff --git a/ggml/src/ggml-cuda/ggml-cuda.cu b/ggml/src/ggml-cuda/ggml-cuda.cu -index f4d4a4267..ac70dcac8 100644 +index c555cd30f..eb3db0f19 100644 --- a/ggml/src/ggml-cuda/ggml-cuda.cu +++ b/ggml/src/ggml-cuda/ggml-cuda.cu @@ -350,6 +350,8 @@ const ggml_cuda_device_info & ggml_cuda_info() { diff --git a/ml/backend/ggml/ggml/src/ggml-cuda/common.cuh b/ml/backend/ggml/ggml/src/ggml-cuda/common.cuh index 28d6bcd7..e98044bd 100644 --- a/ml/backend/ggml/ggml/src/ggml-cuda/common.cuh +++ b/ml/backend/ggml/ggml/src/ggml-cuda/common.cuh @@ -55,10 +55,20 @@ static cudaError_t cudaMemcpy2DAsyncReserve ( void* dst, size_t dpitch, const vo } } +static cudaError_t cudaMemsetAsyncReserve ( void* devPtr, int value, size_t count, cudaStream_t stream = 0 ) { + if (!reserving_graph) { + return cudaMemsetAsync(devPtr, value, count, stream); + } else { + return cudaSuccess; + } +} + #undef cudaMemcpyAsync #define cudaMemcpyAsync cudaMemcpyAsyncReserve #undef cudaMemcpy2DAsync #define cudaMemcpy2DAsync cudaMemcpy2DAsyncReserve +#undef cudaMemsetAsync +#define cudaMemsetAsync cudaMemsetAsyncReserve #define STRINGIZE_IMPL(...) #__VA_ARGS__ #define STRINGIZE(...) STRINGIZE_IMPL(__VA_ARGS__) From 9a50fd584ccac3e7035d8a8112980167e37a60de Mon Sep 17 00:00:00 2001 From: Attogram Project Date: Mon, 3 Nov 2025 00:44:56 +0100 Subject: [PATCH 04/37] readme: add Ollama Bash Lib to community integrations (#12235) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index e8846128..46bcc8ab 100644 --- a/README.md +++ b/README.md @@ -546,6 +546,7 @@ See the [API documentation](./docs/api.md) for all endpoints. - [any-agent](https://github.com/mozilla-ai/any-agent) (A single interface to use and evaluate different agent frameworks by [mozilla.ai](https://www.mozilla.ai/)) - [Neuro SAN](https://github.com/cognizant-ai-lab/neuro-san-studio) (Data-driven multi-agent orchestration framework) with [example](https://github.com/cognizant-ai-lab/neuro-san-studio/blob/main/docs/user_guide.md#ollama) - [achatbot-go](https://github.com/ai-bot-pro/achatbot-go) a multimodal(text/audio/image) chatbot. +- [Ollama Bash Lib](https://github.com/attogram/ollama-bash-lib) - A Bash Library for Ollama. Run LLM prompts straight from your shell, and more ### Mobile From 60829f7ec6ba12f8b06aa917bdba26c82f054e1f Mon Sep 17 00:00:00 2001 From: Ryan Coleman Date: Sun, 2 Nov 2025 16:01:28 -0800 Subject: [PATCH 05/37] readme: add Strands Agents to community integrations (#11740) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 46bcc8ab..6743e73d 100644 --- a/README.md +++ b/README.md @@ -492,6 +492,7 @@ See the [API documentation](./docs/api.md) for all endpoints. - [Firebase Genkit](https://firebase.google.com/docs/genkit/plugins/ollama) - [crewAI](https://github.com/crewAIInc/crewAI) - [Yacana](https://remembersoftwares.github.io/yacana/) (User-friendly multi-agent framework for brainstorming and executing predetermined flows with built-in tool integration) +- [Strands Agents](https://github.com/strands-agents/sdk-python) (A model-driven approach to building AI agents in just a few lines of code) - [Spring AI](https://github.com/spring-projects/spring-ai) with [reference](https://docs.spring.io/spring-ai/reference/api/chat/ollama-chat.html) and [example](https://github.com/tzolov/ollama-tools) - [LangChainGo](https://github.com/tmc/langchaingo/) with [example](https://github.com/tmc/langchaingo/tree/main/examples/ollama-completion-example) - [LangChain4j](https://github.com/langchain4j/langchain4j) with [example](https://github.com/langchain4j/langchain4j-examples/tree/main/ollama-examples/src/main/java) From ce3eb0a3156ea2ed5069fd19897b6f104c387e69 Mon Sep 17 00:00:00 2001 From: Michael Yang Date: Mon, 3 Nov 2025 11:27:15 -0800 Subject: [PATCH 06/37] chore(gptoss): cleanup dead code (#12932) --- model/models/gptoss/model.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/model/models/gptoss/model.go b/model/models/gptoss/model.go index 08bf753d..c10920f1 100644 --- a/model/models/gptoss/model.go +++ b/model/models/gptoss/model.go @@ -32,7 +32,6 @@ func (m *Transformer) Forward(ctx ml.Context, batch input.Batch) (ml.Tensor, err hiddenStates := m.TokenEmbedding.Forward(ctx, batch.Inputs) positions := ctx.Input().FromInts(batch.Positions, len(batch.Positions)) - one := ctx.Input().FromFloats([]float32{1}, 1) for i, block := range m.TransformerBlocks { m.Cache.SetLayer(i) if c, ok := m.Cache.(*kvcache.WrapperCache); ok { @@ -45,7 +44,7 @@ func (m *Transformer) Forward(ctx ml.Context, batch input.Batch) (ml.Tensor, err outputs = batch.Outputs } - hiddenStates = block.Forward(ctx, hiddenStates, positions, outputs, one, m.Cache, &m.Options) + hiddenStates = block.Forward(ctx, hiddenStates, positions, outputs, m.Cache, &m.Options) } hiddenStates = m.OutputNorm.Forward(ctx, hiddenStates, m.eps) @@ -90,13 +89,13 @@ type TransformerBlock struct { MLP *MLPBlock } -func (d *TransformerBlock) Forward(ctx ml.Context, hiddenStates, positions, outputs, one ml.Tensor, cache kvcache.Cache, opts *Options) ml.Tensor { +func (d *TransformerBlock) Forward(ctx ml.Context, hiddenStates, positions, outputs ml.Tensor, cache kvcache.Cache, opts *Options) ml.Tensor { hiddenStates = d.Attention.Forward(ctx, hiddenStates, positions, cache, opts) if outputs != nil { hiddenStates = hiddenStates.Rows(ctx, outputs) } - hiddenStates = d.MLP.Forward(ctx, hiddenStates, one, opts) + hiddenStates = d.MLP.Forward(ctx, hiddenStates, opts) return hiddenStates } @@ -177,7 +176,7 @@ type MLPBlock struct { Down *nn.LinearBatch `gguf:"ffn_down_exps"` } -func (mlp *MLPBlock) Forward(ctx ml.Context, hiddenStates, one ml.Tensor, opts *Options) ml.Tensor { +func (mlp *MLPBlock) Forward(ctx ml.Context, hiddenStates ml.Tensor, opts *Options) ml.Tensor { hiddenDim, sequenceLength, batchSize := hiddenStates.Dim(0), hiddenStates.Dim(1), hiddenStates.Dim(2) residual := hiddenStates From d2158ca6f4ef64968e639b4474e156ff7c92883a Mon Sep 17 00:00:00 2001 From: Rajath Bail Date: Tue, 4 Nov 2025 02:25:04 +0530 Subject: [PATCH 07/37] readme: add Hillnote to community integrations (#12929) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 6743e73d..18cc405b 100644 --- a/README.md +++ b/README.md @@ -415,6 +415,7 @@ See the [API documentation](./docs/api.md) for all endpoints. - [Andes](https://github.com/aqerd/andes) (A Visual Studio Code extension that provides a local UI interface for Ollama models) - [Clueless](https://github.com/KashyapTan/clueless) (Open Source & Local Cluely: A desktop application LLM assistant to help you talk to anything on your screen using locally served Ollama models. Also undetectable to screenshare) - [ollama-co2](https://github.com/carbonatedWaterOrg/ollama-co2) (FastAPI web interface for monitoring and managing local and remote Ollama servers with real-time model monitoring and concurrent downloads) +- [Hillnote](https://hillnote.com) (A Markdown-first workspace designed to supercharge your AI workflow. Create documents ready to integrate with Claude, ChatGPT, Gemini, Cursor, and more - all while keeping your work on your device.) ### Cloud From ef549d513ce37729b0b29984eb738856da7d6b89 Mon Sep 17 00:00:00 2001 From: Jesse Gross Date: Thu, 30 Oct 2025 14:30:31 -0700 Subject: [PATCH 08/37] ggml: Increase maximum graph size The initial implementation of qwen3-vl:235b exceeded the maximum graph size based on the number of tensors. Although this was later fixed through the use of the mrope operation, we are close to the limit in some cases. This updates to track the current llama.cpp usage of GGML. --- ml/backend/ggml/ggml.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ml/backend/ggml/ggml.go b/ml/backend/ggml/ggml.go index eb02c3b1..c02926b3 100644 --- a/ml/backend/ggml/ggml.go +++ b/ml/backend/ggml/ggml.go @@ -378,7 +378,7 @@ func New(modelPath string, params ml.BackendParams) (ml.Backend, error) { } } - maxGraphNodes := max(8192, len(meta.Tensors().Items())*5) + maxGraphNodes := max(1024, len(meta.Tensors().Items())*8) sched := C.ggml_backend_sched_new_ext( (*C.ggml_backend_t)(unsafe.Pointer(&schedBackends[0])), From a4770107a6ea6b4f5adc235d37d08417dc3b9184 Mon Sep 17 00:00:00 2001 From: Daniel Hiltgen Date: Tue, 4 Nov 2025 10:31:22 -0800 Subject: [PATCH 09/37] vulkan: enable flash attention (#12937) Also adjusts the vulkan windows build pattern to match recent changes in other backends so incremental builds are faster. --- ml/device.go | 3 ++- scripts/build_windows.ps1 | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/ml/device.go b/ml/device.go index 1fbe365e..70e0c6a3 100644 --- a/ml/device.go +++ b/ml/device.go @@ -432,7 +432,8 @@ func FlashAttentionSupported(l []DeviceInfo) bool { supportsFA := gpu.Library == "cpu" || gpu.Name == "Metal" || gpu.Library == "Metal" || (gpu.Library == "CUDA" && gpu.DriverMajor >= 7 && !(gpu.ComputeMajor == 7 && gpu.ComputeMinor == 2)) || - gpu.Library == "ROCm" + gpu.Library == "ROCm" || + gpu.Library == "Vulkan" if !supportsFA { return false diff --git a/scripts/build_windows.ps1 b/scripts/build_windows.ps1 index 548545cb..3c885b98 100644 --- a/scripts/build_windows.ps1 +++ b/scripts/build_windows.ps1 @@ -187,11 +187,11 @@ function buildROCm() { function buildVulkan(){ if ($env:VULKAN_SDK) { write-host "Building Vulkan backend libraries" - & cmake --fresh --preset Vulkan --install-prefix $script:DIST_DIR -DOLLAMA_RUNNER_DIR="vulkan" + & cmake -B build\vulkan --preset Vulkan --install-prefix $script:DIST_DIR -DOLLAMA_RUNNER_DIR="vulkan" if ($LASTEXITCODE -ne 0) { exit($LASTEXITCODE)} - & cmake --build --preset Vulkan --config Release --parallel $script:JOBS + & cmake --build build\vulkan --target ggml-vulkan --config Release --parallel $script:JOBS if ($LASTEXITCODE -ne 0) { exit($LASTEXITCODE)} - & cmake --install build --component Vulkan --strip + & cmake --install build\vulkan --component Vulkan --strip if ($LASTEXITCODE -ne 0) { exit($LASTEXITCODE)} } } From d3b4b9970a5e2759fcfc6a632968ecc771ac6e77 Mon Sep 17 00:00:00 2001 From: Daniel Hiltgen Date: Tue, 4 Nov 2025 11:40:17 -0800 Subject: [PATCH 10/37] 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 33ce2007..195203f1 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 12ee7135..d74da923 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 72417ade..6fcdf4d2 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 dbc9207e..c56c229a 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 0aa24794..a83eb70a 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 433ee44e..fcacc07a 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 6fed2d0e..1146e41c 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 00000000..2d3297e1 --- /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 00000000..b7582523 --- /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 00000000..1a4cd568 --- /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 00000000..2018ce8e --- /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 00000000..4a5ba055 --- /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 00000000..4e1d52f7 --- /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 00000000..51cf1f92 --- /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 00000000..1ff94cbb --- /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 00000000..983fba9d --- /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 00000000..14de76d2 --- /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 00000000..5d9b4ca6 --- /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 00000000..563f7bd9 --- /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 00000000..75bff9f6 --- /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 00000000..302cd2e2 --- /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 00000000..7ee22a0d --- /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 00000000..a71275cf --- /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 00000000..700f79fc --- /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 00000000..8d0f08d6 --- /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 00000000..c5b175ca --- /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 00000000..2848c479 --- /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 00000000..090bdf7d --- /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 00000000..9ea84318 --- /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 2af87ab9..00000000 --- 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 f39dc31c..00000000 --- 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 c24fe646..00000000 --- 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 22e3de19..00000000 --- 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 205e47d7..00000000 --- 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 8d5cdf6e..00000000 --- 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 8f20337f..00000000 --- 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 42ae8267..00000000 --- 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 f7aa2026..00000000 --- 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 70573913..00000000 --- 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 5f9fe124..00000000 --- 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 1d2dda80..00000000 --- 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 293dd603..00000000 --- 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 00000000..df8ba9c0 --- /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 00000000..5eef5b4f --- /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 db829795..00000000 --- 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 d575fc7f..2f2aa230 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 e9695748..00000000 --- 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 00000000..64b96b1f --- /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 00000000..f533073d --- /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 00000000..2c50716b --- /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 00000000..c2e7f4b9 --- /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 00000000..0f268c6f --- /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 00000000..1b037a75 --- /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 00000000..c7e6f9fd --- /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 00000000..57b37b70 --- /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 00000000..8f944ff8 --- /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 00000000..a6d46900 --- /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 370436c5..052fcd61 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 e53d8525..00000000 --- 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 3aac9b01..00000000 --- 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 00000000..dfe6435f --- /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 ba06b82c..00000000 --- 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 00000000..8f944ff8 --- /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 00000000..b41c079b --- /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 00000000..fd9c2bd4 --- /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 00000000..05b5584b --- /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 00000000..dd47fb88 --- /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 00000000..b3672789 --- /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 00000000..67a582d3 --- /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 00000000..1cb12ab7 --- /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 ed633dc9..00000000 --- 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 dfa63435..00000000 --- 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 a03d233e..00000000 --- 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 086fc794..00000000 --- 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 35608a49..00000000 --- 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 00000000..9294e015 --- /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 00000000..d1989427 --- /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 00000000..4a64822e --- /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 00000000..d73df2c0 --- /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 00000000..9705d1e6 --- /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 00000000..21c55e20 --- /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 00000000..1957463f --- /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 00000000..a077c854 --- /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 00000000..691c8a44 --- /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 00000000..9e74109a --- /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 00000000..7877eeae --- /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 00000000..052e0e62 --- /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 00000000..c8b2e116 --- /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 00000000..e0fcb4ff --- /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 00000000..b13ebd80 --- /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} + + +
+ ))} +
+ )} + +
+